SqlAlchemy 2.0 中文文档(三十二)(2)

简介: SqlAlchemy 2.0 中文文档(三十二)

SqlAlchemy 2.0 中文文档(三十二)(1)https://developer.aliyun.com/article/1562539


混合属性

原文:docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html

定义在 ORM 映射类上具有“混合”行为的属性。

“混合”意味着属性在类级别和实例级别具有不同的行为。

hybrid 扩展提供了一种特殊形式的方法装饰器,并且对 SQLAlchemy 的其余部分具有最小的依赖性。 它的基本操作理论可以与任何基于描述符的表达式系统一起使用。

考虑一个映射 Interval,表示整数 startend 值。 我们可以在映射类上定义更高级别的函数,这些函数在类级别生成 SQL 表达式,并在实例级别进行 Python 表达式评估。 下面,每个使用 hybrid_methodhybrid_property 装饰的函数可能会接收 self 作为类的实例,或者直接接收类,具体取决于上下文:

from __future__ import annotations
from sqlalchemy.ext.hybrid import hybrid_method
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
class Base(DeclarativeBase):
    pass
class Interval(Base):
    __tablename__ = 'interval'
    id: Mapped[int] = mapped_column(primary_key=True)
    start: Mapped[int]
    end: Mapped[int]
    def __init__(self, start: int, end: int):
        self.start = start
        self.end = end
    @hybrid_property
    def length(self) -> int:
        return self.end - self.start
    @hybrid_method
    def contains(self, point: int) -> bool:
        return (self.start <= point) & (point <= self.end)
    @hybrid_method
    def intersects(self, other: Interval) -> bool:
        return self.contains(other.start) | self.contains(other.end)

上面,length 属性返回 endstart 属性之间的差异。 对于 Interval 的实例,这个减法在 Python 中发生,使用正常的 Python 描述符机制:

>>> i1 = Interval(5, 10)
>>> i1.length
5

处理 Interval 类本身时,hybrid_property 描述符将函数体评估为给定 Interval 类作为参数,当使用 SQLAlchemy 表达式机制评估时,将返回新的 SQL 表达式:

>>> from sqlalchemy import select
>>> print(select(Interval.length))
SELECT  interval."end"  -  interval.start  AS  length
FROM  interval
>>> print(select(Interval).filter(Interval.length > 10))
SELECT  interval.id,  interval.start,  interval."end"
FROM  interval
WHERE  interval."end"  -  interval.start  >  :param_1 

过滤方法如 Select.filter_by() 也支持混合属性:

>>> print(select(Interval).filter_by(length=5))
SELECT  interval.id,  interval.start,  interval."end"
FROM  interval
WHERE  interval."end"  -  interval.start  =  :param_1 

Interval 类示例还说明了两种方法,contains()intersects(),使用 hybrid_method 装饰。 这个装饰器将相同的思想应用于方法,就像 hybrid_property 将其应用于属性一样。 这些方法返回布尔值,并利用 Python 的 |& 位运算符产生等效的实例级和 SQL 表达式级布尔行为:

>>> i1.contains(6)
True
>>> i1.contains(15)
False
>>> i1.intersects(Interval(7, 18))
True
>>> i1.intersects(Interval(25, 29))
False
>>> print(select(Interval).filter(Interval.contains(15)))
SELECT  interval.id,  interval.start,  interval."end"
FROM  interval
WHERE  interval.start  <=  :start_1  AND  interval."end"  >  :end_1
>>> ia = aliased(Interval)
>>> print(select(Interval, ia).filter(Interval.intersects(ia)))
SELECT  interval.id,  interval.start,
interval."end",  interval_1.id  AS  interval_1_id,
interval_1.start  AS  interval_1_start,  interval_1."end"  AS  interval_1_end
FROM  interval,  interval  AS  interval_1
WHERE  interval.start  <=  interval_1.start
  AND  interval."end"  >  interval_1.start
  OR  interval.start  <=  interval_1."end"
  AND  interval."end"  >  interval_1."end" 

定义与属性行为不同的表达行为

在前一节中,我们在 Interval.containsInterval.intersects 方法中使用 &| 按位运算符是幸运的,考虑到我们的函数操作两个布尔值以返回一个新值。在许多情况下,Python 函数的构建和 SQLAlchemy SQL 表达式有足够的差异,因此应该定义两个独立的 Python 表达式。hybrid 装饰器为此目的定义了一个 修饰符 hybrid_property.expression()。作为示例,我们将定义区间的半径,这需要使用绝对值函数:

from sqlalchemy import ColumnElement
from sqlalchemy import Float
from sqlalchemy import func
from sqlalchemy import type_coerce
class Interval(Base):
    # ...
    @hybrid_property
    def radius(self) -> float:
        return abs(self.length) / 2
    @radius.inplace.expression
    @classmethod
    def _radius_expression(cls) -> ColumnElement[float]:
        return type_coerce(func.abs(cls.length) / 2, Float)

在上述示例中,首先分配给名称 Interval.radiushybrid_property 在后续使用 Interval._radius_expression 方法进行修改,使用装饰器 @radius.inplace.expression,将两个修饰符 hybrid_property.inplacehybrid_property.expression 连接在一起。使用 hybrid_property.inplace 指示 hybrid_property.expression() 修饰符应在原地突变现有的混合对象 Interval.radius,而不是创建一个新对象。有关此修饰符及其基本原理的注释将在下一节 使用 inplace 创建符合 pep-484 的混合属性 中讨论。使用 @classmethod 是可选的,严格来说是为了给类型提示工具一个提示,即这种情况下 cls 应该是 Interval 类,而不是 Interval 的实例。

注意

hybrid_property.inplace 以及使用 @classmethod 进行正确类型支持的功能在 SQLAlchemy 2.0.4 中可用,之前的版本不支持。

Interval.radius 现在包含一个表达式元素,当在类级别访问 Interval.radius 时,会返回 SQL 函数 ABS()

>>> from sqlalchemy import select
>>> print(select(Interval).filter(Interval.radius > 5))
SELECT  interval.id,  interval.start,  interval."end"
FROM  interval
WHERE  abs(interval."end"  -  interval.start)  /  :abs_1  >  :param_1 
```## 使用 `inplace` 创建符合 pep-484 的混合属性
在前一节中,说明了一个`hybrid_property`装饰器,其中包含两个独立的方法级函数被装饰,都用于生成一个称为`Interval.radius`的单个对象属性。实际上,我们可以使用几种不同的修饰符来修饰`hybrid_property`,包括`hybrid_property.expression()`、`hybrid_property.setter()`和`hybrid_property.update_expression()`。
SQLAlchemy 的`hybrid_property`装饰器意味着可以以与 Python 内置的`@property`装饰器相同的方式添加这些方法,其中惯用的用法是继续重定义属性,每次都使用**相同的属性名称**,就像下面的示例中演示的那样,说明了使用`hybrid_property.setter()`和`hybrid_property.expression()`来描述`Interval.radius`的用法:
```py
# correct use, however is not accepted by pep-484 tooling
class Interval(Base):
    # ...
    @hybrid_property
    def radius(self):
        return abs(self.length) / 2
    @radius.setter
    def radius(self, value):
        self.length = value * 2
    @radius.expression
    def radius(cls):
        return type_coerce(func.abs(cls.length) / 2, Float)

如上所述,有三个Interval.radius方法,但由于每个都被hybrid_property装饰器和@radius名称本身装饰,因此最终效果是Interval.radius是一个具有三个不同功能的单个属性。这种使用方式取自于Python 文档中对@property 的使用。值得注意的是,@property以及hybrid_property的工作方式,每次都会复制描述符。也就是说,每次调用@radius.expression@radius.setter等都会完全创建一个新对象。这允许在子类中重新定义属性而无需问题(请参阅本节稍后的在子类中重用混合属性的使用方式)。

然而,上述方法与 mypy 和 pyright 等类型工具不兼容。 Python 自己的@property装饰器之所以没有此限制,只是因为这些工具硬编码了@property 的行为,这意味着此语法不符合PEP 484的要求。

为了产生合理的语法,同时保持类型兼容性,hybrid_property.inplace装饰器允许使用不同的方法名重复使用相同的装饰器,同时仍然在一个名称下生成一个装饰器:

# correct use which is also accepted by pep-484 tooling
class Interval(Base):
    # ...
    @hybrid_property
    def radius(self) -> float:
        return abs(self.length) / 2
    @radius.inplace.setter
    def _radius_setter(self, value: float) -> None:
        # for example only
        self.length = value * 2
    @radius.inplace.expression
    @classmethod
    def _radius_expression(cls) -> ColumnElement[float]:
        return type_coerce(func.abs(cls.length) / 2, Float)

使用hybrid_property.inplace进一步限定了应该不制作新副本的装饰器的使用,从而保持了Interval.radius名称,同时允许其他方法Interval._radius_setterInterval._radius_expression命名不同。

2.0.4 版中的新功能:添加了hybrid_property.inplace,以允许更少冗长的构造复合hybrid_property对象,同时无需使用重复的方法名称。此外,允许在hybrid_property.expressionhybrid_property.update_expressionhybrid_property.comparator内使用@classmethod,以允许类型工具将cls识别为类而不是方法签名中的实例。

定义设置器

hybrid_property.setter()修饰符允许构造自定义的设置器方法,可以修改对象上的值:

class Interval(Base):
    # ...
    @hybrid_property
    def length(self) -> int:
        return self.end - self.start
    @length.inplace.setter
    def _length_setter(self, value: int) -> None:
        self.end = self.start + value

现在,在设置时调用length(self, value)方法:

>>> i1 = Interval(5, 10)
>>> i1.length
5
>>> i1.length = 12
>>> i1.end
17

允许批量 ORM 更新

一个混合可以为启用 ORM 的更新定义自定义的“UPDATE”处理程序,从而允许混合用于更新的 SET 子句中。

通常,在使用带有update()的混合时,SQL 表达式被用作作为 SET 目标的列。如果我们的Interval类有一个混合start_point,它链接到Interval.start,这可以直接替换:

from sqlalchemy import update
stmt = update(Interval).values({Interval.start_point: 10})

但是,当使用类似Interval.length的复合混合类型时,此混合类型表示不止一个列。我们可以设置一个处理程序,该处理程序将适应传递给 VALUES 表达式的值,这可能会影响到这一点,使用hybrid_property.update_expression()装饰器。一个类似于我们的设置器的处理程序将是:

from typing import List, Tuple, Any
class Interval(Base):
    # ...
    @hybrid_property
    def length(self) -> int:
        return self.end - self.start
    @length.inplace.setter
    def _length_setter(self, value: int) -> None:
        self.end = self.start + value
    @length.inplace.update_expression
    def _length_update_expression(cls, value: Any) -> List[Tuple[Any, Any]]:
        return [
            (cls.end, cls.start + value)
        ]

以上,如果我们在 UPDATE 表达式中使用Interval.length,我们将得到一个混合 SET 表达式:

>>> from sqlalchemy import update
>>> print(update(Interval).values({Interval.length: 25}))
UPDATE  interval  SET  "end"=(interval.start  +  :start_1) 

这个 SET 表达式会被 ORM 自动处理。

另请参阅

ORM-启用的 INSERT、UPDATE 和 DELETE 语句 - 包括 ORM 启用的 UPDATE 语句的背景信息

处理关系

创建与基于列的数据不同的混合对象时,本质上没有区别。对于不同的表达式的需求往往更大。我们将展示的两种变体是“连接依赖”混合和“相关子查询”混合。

连接依赖关系混合

考虑以下将 UserSavingsAccount 关联的声明性映射:

from __future__ import annotations
from decimal import Decimal
from typing import cast
from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import Numeric
from sqlalchemy import String
from sqlalchemy import SQLColumnExpression
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
    pass
class SavingsAccount(Base):
    __tablename__ = 'account'
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))
    balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))
    owner: Mapped[User] = relationship(back_populates="accounts")
class User(Base):
    __tablename__ = 'user'
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    accounts: Mapped[List[SavingsAccount]] = relationship(
        back_populates="owner", lazy="selectin"
    )
    @hybrid_property
    def balance(self) -> Optional[Decimal]:
        if self.accounts:
            return self.accounts[0].balance
        else:
            return None
    @balance.inplace.setter
    def _balance_setter(self, value: Optional[Decimal]) -> None:
        assert value is not None
        if not self.accounts:
            account = SavingsAccount(owner=self)
        else:
            account = self.accounts[0]
        account.balance = value
    @balance.inplace.expression
    @classmethod
    def _balance_expression(cls) -> SQLColumnExpression[Optional[Decimal]]:
        return cast("SQLColumnExpression[Optional[Decimal]]", SavingsAccount.balance)

上面的混合属性 balance 与此用户的账户列表中的第一个 SavingsAccount 条目一起工作。在 Python 中的 getter/setter 方法可以将 accounts 视为可在 self 上使用的 Python 列表。

提示

在上面的例子中,User.balance 的 getter 方法访问了 self.accounts 集合,通常会通过配置在 User.balancerelationship() 上的 selectinload() 加载策略来加载。当在 relationship() 上没有另外指定时,默认的加载策略是 lazyload(),它会按需发出 SQL。在使用 asyncio 时,像 lazyload() 这样的按需加载器不受支持,因此在使用 asyncio 时,应确保 self.accounts 集合对这个混合访问器是可访问的。

在表达式级别,预期 User 类将在适当的上下文中使用,以便存在与 SavingsAccount 的适当连接:

>>> from sqlalchemy import select
>>> print(select(User, User.balance).
...       join(User.accounts).filter(User.balance > 5000))
SELECT  "user".id  AS  user_id,  "user".name  AS  user_name,
account.balance  AS  account_balance
FROM  "user"  JOIN  account  ON  "user".id  =  account.user_id
WHERE  account.balance  >  :balance_1 

但需要注意的是,尽管实例级别的访问器需要担心 self.accounts 是否存在,但在 SQL 表达式级别,这个问题表现得不同,我们基本上会使用外连接:

>>> from sqlalchemy import select
>>> from sqlalchemy import or_
>>> print (select(User, User.balance).outerjoin(User.accounts).
...         filter(or_(User.balance < 5000, User.balance == None)))
SELECT  "user".id  AS  user_id,  "user".name  AS  user_name,
account.balance  AS  account_balance
FROM  "user"  LEFT  OUTER  JOIN  account  ON  "user".id  =  account.user_id
WHERE  account.balance  <  :balance_1  OR  account.balance  IS  NULL 

相关子查询关系混合

当然,我们可以放弃依赖于包含查询中连接的使用,而选择相关子查询,它可以被打包成一个单列表达式。相关子查询更具可移植性,但在 SQL 层面通常性能较差。使用在 使用 column_property 中展示的相同技术,我们可以调整我们的 SavingsAccount 示例来聚合所有账户的余额,并使用相关子查询作为列表达式:

from __future__ import annotations
from decimal import Decimal
from typing import List
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Numeric
from sqlalchemy import select
from sqlalchemy import SQLColumnExpression
from sqlalchemy import String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
class Base(DeclarativeBase):
    pass
class SavingsAccount(Base):
    __tablename__ = 'account'
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey('user.id'))
    balance: Mapped[Decimal] = mapped_column(Numeric(15, 5))
    owner: Mapped[User] = relationship(back_populates="accounts")
class User(Base):
    __tablename__ = 'user'
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    accounts: Mapped[List[SavingsAccount]] = relationship(
        back_populates="owner", lazy="selectin"
    )
    @hybrid_property
    def balance(self) -> Decimal:
        return sum((acc.balance for acc in self.accounts), start=Decimal("0"))
    @balance.inplace.expression
    @classmethod
    def _balance_expression(cls) -> SQLColumnExpression[Decimal]:
        return (
            select(func.sum(SavingsAccount.balance))
            .where(SavingsAccount.user_id == cls.id)
            .label("total_balance")
        )

上面的示例将给我们一个 balance 列,它呈现一个相关的 SELECT:

>>> from sqlalchemy import select
>>> print(select(User).filter(User.balance > 400))
SELECT  "user".id,  "user".name
FROM  "user"
WHERE  (
  SELECT  sum(account.balance)  AS  sum_1  FROM  account
  WHERE  account.user_id  =  "user".id
)  >  :param_1 

构建自定义比较器

混合属性还包括一个辅助程序,允许构建自定义比较器。比较器对象允许单独定制每个 SQLAlchemy 表达式操作符的行为。在创建在 SQL 方面具有某些高度特殊行为的自定义类型时很有用。

注意

本节中引入的hybrid_property.comparator()装饰器替换hybrid_property.expression()装饰器的使用。它们不能一起使用。

下面的示例类允许在名为word_insensitive的属性上进行不区分大小写的比较:

from __future__ import annotations
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy import func
from sqlalchemy.ext.hybrid import Comparator
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
class Base(DeclarativeBase):
    pass
class CaseInsensitiveComparator(Comparator[str]):
    def __eq__(self, other: Any) -> ColumnElement[bool]:  # type: ignore[override]  # noqa: E501
        return func.lower(self.__clause_element__()) == func.lower(other)
class SearchWord(Base):
    __tablename__ = 'searchword'
    id: Mapped[int] = mapped_column(primary_key=True)
    word: Mapped[str]
    @hybrid_property
    def word_insensitive(self) -> str:
        return self.word.lower()
    @word_insensitive.inplace.comparator
    @classmethod
    def _word_insensitive_comparator(cls) -> CaseInsensitiveComparator:
        return CaseInsensitiveComparator(cls.word)

在上述情况下,针对word_insensitive的 SQL 表达式将对两侧应用LOWER() SQL 函数:

>>> from sqlalchemy import select
>>> print(select(SearchWord).filter_by(word_insensitive="Trucks"))
SELECT  searchword.id,  searchword.word
FROM  searchword
WHERE  lower(searchword.word)  =  lower(:lower_1) 

上述的CaseInsensitiveComparator实现了ColumnOperators接口的部分内容。可以使用Operators.operate()对所有比较操作(即eqltgt等)应用“强制转换”操作,如转换为小写:

class CaseInsensitiveComparator(Comparator):
    def operate(self, op, other, **kwargs):
        return op(
            func.lower(self.__clause_element__()),
            func.lower(other),
            **kwargs,
        )
```## 在子类之间重用混合属性
可以从超类中引用混合体,以允许修改方法,如`hybrid_property.getter()`,`hybrid_property.setter()`,以便在子类中重新定义这些方法。这类似于标准 Python 的`@property`对象的工作原理:
```py
class FirstNameOnly(Base):
    # ...
    first_name: Mapped[str]
    @hybrid_property
    def name(self) -> str:
        return self.first_name
    @name.inplace.setter
    def _name_setter(self, value: str) -> None:
        self.first_name = value
class FirstNameLastName(FirstNameOnly):
    # ...
    last_name: Mapped[str]
    # 'inplace' is not used here; calling getter creates a copy
    # of FirstNameOnly.name that is local to FirstNameLastName
    @FirstNameOnly.name.getter
    def name(self) -> str:
        return self.first_name + ' ' + self.last_name
    @name.inplace.setter
    def _name_setter(self, value: str) -> None:
        self.first_name, self.last_name = value.split(' ', 1)

在上述情况下,FirstNameLastName类引用了从FirstNameOnly.name到子类的混合体,以重新利用其 getter 和 setter。

当仅在首次引用超类时覆盖hybrid_property.expression()hybrid_property.comparator()作为类级别的第一个引用时,这些名称会与返回类级别的QueryableAttribute对象上的同名访问器发生冲突。要在直接引用父类描述符时覆盖这些方法,请添加特殊限定词hybrid_property.overrides,该限定词将仪表化的属性引用回混合对象:

class FirstNameLastName(FirstNameOnly):
    # ...
    last_name: Mapped[str]
    @FirstNameOnly.name.overrides.expression
    @classmethod
    def name(cls):
        return func.concat(cls.first_name, ' ', cls.last_name)


SqlAlchemy 2.0 中文文档(三十二)(3)https://developer.aliyun.com/article/1562551

相关文章
|
5月前
|
SQL API 数据安全/隐私保护
SqlAlchemy 2.0 中文文档(三十二)(3)
SqlAlchemy 2.0 中文文档(三十二)
34 1
|
5月前
|
SQL 存储 关系型数据库
SqlAlchemy 2.0 中文文档(三十四)(4)
SqlAlchemy 2.0 中文文档(三十四)
45 1
|
5月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(三十四)(5)
SqlAlchemy 2.0 中文文档(三十四)
41 0
|
5月前
|
JSON 测试技术 数据格式
SqlAlchemy 2.0 中文文档(三十一)(4)
SqlAlchemy 2.0 中文文档(三十一)
38 1
|
5月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(三十一)(2)
SqlAlchemy 2.0 中文文档(三十一)
34 1
|
5月前
|
存储 SQL 测试技术
SqlAlchemy 2.0 中文文档(三十一)(1)
SqlAlchemy 2.0 中文文档(三十一)
54 1
|
5月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(三十一)(3)
SqlAlchemy 2.0 中文文档(三十一)
30 1
|
5月前
|
SQL 数据库 数据库管理
SqlAlchemy 2.0 中文文档(二十七)(3)
SqlAlchemy 2.0 中文文档(二十七)
31 1
|
5月前
|
SQL API 数据安全/隐私保护
SqlAlchemy 2.0 中文文档(三十二)(5)
SqlAlchemy 2.0 中文文档(三十二)
35 0
|
5月前
|
SQL Python
SqlAlchemy 2.0 中文文档(三十二)(4)
SqlAlchemy 2.0 中文文档(三十二)
25 0