SqlAlchemy 2.0 中文文档(三十二)(1)https://developer.aliyun.com/article/1562539
混合属性
定义在 ORM 映射类上具有“混合”行为的属性。
“混合”意味着属性在类级别和实例级别具有不同的行为。
hybrid
扩展提供了一种特殊形式的方法装饰器,并且对 SQLAlchemy 的其余部分具有最小的依赖性。 它的基本操作理论可以与任何基于描述符的表达式系统一起使用。
考虑一个映射 Interval
,表示整数 start
和 end
值。 我们可以在映射类上定义更高级别的函数,这些函数在类级别生成 SQL 表达式,并在实例级别进行 Python 表达式评估。 下面,每个使用 hybrid_method
或 hybrid_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
属性返回 end
和 start
属性之间的差异。 对于 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.contains
和 Interval.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.radius
的 hybrid_property
在后续使用 Interval._radius_expression
方法进行修改,使用装饰器 @radius.inplace.expression
,将两个修饰符 hybrid_property.inplace
和 hybrid_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_setter
和Interval._radius_expression
命名不同。
2.0.4 版中的新功能:添加了hybrid_property.inplace
,以允许更少冗长的构造复合hybrid_property
对象,同时无需使用重复的方法名称。此外,允许在hybrid_property.expression
、hybrid_property.update_expression
和hybrid_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 语句的背景信息
处理关系
创建与基于列的数据不同的混合对象时,本质上没有区别。对于不同的表达式的需求往往更大。我们将展示的两种变体是“连接依赖”混合和“相关子查询”混合。
连接依赖关系混合
考虑以下将 User
与 SavingsAccount
关联的声明性映射:
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.balance
的 relationship()
上的 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()
对所有比较操作(即eq
、lt
、gt
等)应用“强制转换”操作,如转换为小写:
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