SqlAlchemy 2.0 中文文档(三十二)(2)https://developer.aliyun.com/article/1562543
混合值对象
请注意,在我们之前的例子中,如果我们将SearchWord
实例的word_insensitive
属性与普通的 Python 字符串进行比较,普通的 Python 字符串不会被强制转换为小写 - 我们构建的CaseInsensitiveComparator
,由@word_insensitive.comparator
返回,仅适用于 SQL 端。
自定义比较器的更全面形式是构建一个混合值对象。这种技术将目标值或表达式应用于一个值对象,然后由访问器在所有情况下返回。值对象允许控制对值的所有操作,以及如何处理比较的值,无论是在 SQL 表达式方面还是在 Python 值方面。用新的CaseInsensitiveWord
类替换以前的CaseInsensitiveComparator
类:
class CaseInsensitiveWord(Comparator): "Hybrid value representing a lower case representation of a word." def __init__(self, word): if isinstance(word, basestring): self.word = word.lower() elif isinstance(word, CaseInsensitiveWord): self.word = word.word else: self.word = func.lower(word) def operate(self, op, other, **kwargs): if not isinstance(other, CaseInsensitiveWord): other = CaseInsensitiveWord(other) return op(self.word, other.word, **kwargs) def __clause_element__(self): return self.word def __str__(self): return self.word key = 'word' "Label to apply to Query tuple results"
在上面的例子中,CaseInsensitiveWord
对象表示self.word
,它可能是一个 SQL 函数,也可能是一个 Python 本机对象。通过重写operate()
和__clause_element__()
以使用self.word
,所有比较操作将针对“转换”形式的word
进行,无论是在 SQL 端还是在 Python 端。我们的SearchWord
类现在可以无条件地从单个混合调用中交付CaseInsensitiveWord
对象:
class SearchWord(Base): __tablename__ = 'searchword' id: Mapped[int] = mapped_column(primary_key=True) word: Mapped[str] @hybrid_property def word_insensitive(self) -> CaseInsensitiveWord: return CaseInsensitiveWord(self.word)
word_insensitive
属性现在在所有情况下都具有不区分大小写的比较行为,包括 SQL 表达式与 Python 表达式(请注意,此处 Python 值在 Python 端转换为小写):
>>> print(select(SearchWord).filter_by(word_insensitive="Trucks")) SELECT searchword.id AS searchword_id, searchword.word AS searchword_word FROM searchword WHERE lower(searchword.word) = :lower_1
SQL 表达式与 SQL 表达式:
>>> from sqlalchemy.orm import aliased >>> sw1 = aliased(SearchWord) >>> sw2 = aliased(SearchWord) >>> print( ... select(sw1.word_insensitive, sw2.word_insensitive).filter( ... sw1.word_insensitive > sw2.word_insensitive ... ) ... ) SELECT lower(searchword_1.word) AS lower_1, lower(searchword_2.word) AS lower_2 FROM searchword AS searchword_1, searchword AS searchword_2 WHERE lower(searchword_1.word) > lower(searchword_2.word)
仅 Python 表达式:
>>> ws1 = SearchWord(word="SomeWord") >>> ws1.word_insensitive == "sOmEwOrD" True >>> ws1.word_insensitive == "XOmEwOrX" False >>> print(ws1.word_insensitive) someword
混合值模式非常适用于可能具有多种表示形式的任何类型的值,例如时间戳、时间差、测量单位、货币和加密密码。
另请参阅
混合和值不可知类型 - 在 techspot.zzzeek.org 博客上
值不可知类型,第二部分 - 在 techspot.zzzeek.org 博客上
API 参考
对象名称 | 描述 |
比较器 | 一个帮助类,允许轻松构建用于混合使用的自定义PropComparator 类。 |
hybrid_method | 一个装饰器,允许定义具有实例级和类级行为的 Python 对象方法。 |
hybrid_property | 一个装饰器,允许定义具有实例级和类级行为的 Python 描述符。 |
混合扩展类型 | 一个枚举类型。 |
class sqlalchemy.ext.hybrid.hybrid_method
一个装饰器,允许定义具有实例级和类级行为的 Python 对象方法。
成员
init(), expression(), extension_type, inplace, is_attribute
类签名
类sqlalchemy.ext.hybrid.hybrid_method
(sqlalchemy.orm.base.InspectionAttrInfo
, typing.Generic
)
method __init__(func: Callable[[Concatenate[Any, _P]], _R], expr: Callable[[Concatenate[Any, _P]], SQLCoreOperations[_R]] | None = None)
创建一个新的hybrid_method
。
通常使用装饰器:
from sqlalchemy.ext.hybrid import hybrid_method class SomeClass: @hybrid_method def value(self, x, y): return self._value + x + y @value.expression @classmethod def value(cls, x, y): return func.some_function(cls._value, x, y)
method expression(expr: Callable[[Concatenate[Any, _P]], SQLCoreOperations[_R]]) → hybrid_method[_P, _R]
提供一个修改装饰器,定义一个生成 SQL 表达式的方法。
attribute extension_type: InspectionAttrExtensionType = 'HYBRID_METHOD'
扩展类型,如果有的话。默认为NotExtension.NOT_EXTENSION
另请参阅
HybridExtensionType
AssociationProxyExtensionType
attribute inplace
返回此hybrid_method
的原地变异器。
当调用hybrid_method.expression()
装饰器时,hybrid_method
类已经执行“原地”变异,因此此属性返回 Self。
版本 2.0.4 中的新功能。
另请参阅
使用 inplace 创建符合 pep-484 的混合属性
attribute is_attribute = True
如果这个对象是一个 Python 描述符,则为 True。
这可以指代许多类型之一。通常是一个处理属性事件的QueryableAttribute
,代表一个MapperProperty
。但也可以是一个扩展类型,如AssociationProxy
或hybrid_property
。InspectionAttr.extension_type
将指代一个标识特定子类型的常量。
另请参阅
Mapper.all_orm_descriptors
class sqlalchemy.ext.hybrid.hybrid_property
一个允许定义具有实例级和类级行为的 Python 描述符的装饰器。
成员
init(), comparator(), deleter(), expression(), extension_type, getter(), inplace, is_attribute, overrides, setter(), update_expression()
类签名
类sqlalchemy.ext.hybrid.hybrid_property
(sqlalchemy.orm.base.InspectionAttrInfo
, sqlalchemy.orm.base.ORMDescriptor
)
method __init__(fget: _HybridGetterType[_T], fset: _HybridSetterType[_T] | None = None, fdel: _HybridDeleterType[_T] | None = None, expr: _HybridExprCallableType[_T] | None = None, custom_comparator: Comparator[_T] | None = None, update_expr: _HybridUpdaterType[_T] | None = None)
创建一个新的hybrid_property
。
通常使用修饰器:
from sqlalchemy.ext.hybrid import hybrid_property class SomeClass: @hybrid_property def value(self): return self._value @value.setter def value(self, value): self._value = value
method comparator(comparator: _HybridComparatorCallableType[_T]) → hybrid_property[_T]
提供一个修饰器,定义一个自定义比较器生成方法。
被修饰方法的返回值应该是Comparator
的一个实例。
注意
hybrid_property.comparator()
修饰器替代了hybrid_property.expression()
修饰器的使用。它们不能同时使用。
当在类级别调用混合属性时,这里给出的Comparator
对象被包装在一个专门的QueryableAttribute
中,这是 ORM 用来表示其他映射属性的对象。这样做的原因是为了在返回的结构中保留其他类级别属性,如文档字符串和对混合属性本身的引用,而不对传入的原始比较器对象进行任何修改。
注意
当引用拥有类(例如 SomeClass.some_hybrid
)的混合属性时,会返回一个QueryableAttribute
的实例,表示此混合对象的表达式或比较器对象。但是,该对象本身具有名为 expression
和 comparator
的访问器;因此,在尝试在子类上覆盖这些装饰器时,可能需要首先使用 hybrid_property.overrides
修饰符进行限定。有关详细信息,请参阅该修饰符。
method deleter(fdel: _HybridDeleterType[_T]) → hybrid_property[_T]
提供一个修改装饰器,定义一个删除方法。
method expression(expr: _HybridExprCallableType[_T]) → hybrid_property[_T]
提供一个修改装饰器,定义一个生成 SQL 表达式的方法。
当在类级别调用混合时,此处给出的 SQL 表达式将包装在一个专门的 QueryableAttribute
内,这是 ORM 用于表示其他映射属性的相同类型的对象。这样做的原因是为了在返回的结构中维护其他类级别属性,例如文档字符串和混合本身的引用,而不对传入的原始 SQL 表达式进行任何修改。
注意
当引用拥有类(例如 SomeClass.some_hybrid
)的混合属性时,会返回一个QueryableAttribute
的实例,表示表达式或比较器对象以及此混合对象。但是,该对象本身具有名为 expression
和 comparator
的访问器;因此,在尝试在子类上覆盖这些装饰器时,可能需要首先使用 hybrid_property.overrides
修饰符进行限定。有关详细信息,请参阅该修饰符。
请参见
定义与属性行为不同的表达行为
attribute extension_type: InspectionAttrExtensionType = 'HYBRID_PROPERTY'
扩展类型,如果有的话。默认为 NotExtension.NOT_EXTENSION
请参见
HybridExtensionType
AssociationProxyExtensionType
method getter(fget: _HybridGetterType[_T]) → hybrid_property[_T]
提供一个修改装饰器,定义一个获取器方法。
自 1.2 版开始新添加。
attribute inplace
返回此 hybrid_property
的原地变异器。
这是为了允许对混合进行就地变异,允许重用特定名称的第一个混合方法以添加更多方法,而不必将这些方法命名为相同的名称,例如:
class Interval(Base): # ... @hybrid_property def radius(self) -> float: return abs(self.length) / 2 @radius.inplace.setter def _radius_setter(self, value: float) -> None: self.length = value * 2 @radius.inplace.expression def _radius_expression(cls) -> ColumnElement[float]: return type_coerce(func.abs(cls.length) / 2, Float)
自 2.0.4 版开始新添加。
请参见
使用 inplace 创建符合 pep-484 的混合属性
attribute is_attribute = True
如果此对象是 Python 的 描述符,则为 True。
这可能是多种类型之一。通常是一个QueryableAttribute
,它代表一个MapperProperty
上的属性事件。但也可以是诸如AssociationProxy
或hybrid_property
之类的扩展类型。InspectionAttr.extension_type
将引用一个常量,以识别特定的子类型。
另请参阅
Mapper.all_orm_descriptors
attribute overrides
重写现有属性的方法的前缀。
hybrid_property.overrides
访问器只是返回这个混合对象,当在父类的类级别从父类调用时,它将取消引用通常在此级别返回的“instrumented attribute”,并允许修改装饰器,例如 hybrid_property.expression()
和 hybrid_property.comparator()
被使用而不与通常存在于QueryableAttribute
上的同名属性冲突:
class SuperClass: # ... @hybrid_property def foobar(self): return self._foobar class SubClass(SuperClass): # ... @SuperClass.foobar.overrides.expression def foobar(cls): return func.subfoobar(self._foobar)
1.2 版的新功能。
另请参阅
在子类中重用混合属性
method setter(fset: _HybridSetterType[_T]) → hybrid_property[_T]
提供一个定义 setter 方法的修改装饰器。
method update_expression(meth: _HybridUpdaterType[_T]) → hybrid_property[_T]
提供一个定义产生 UPDATE 元组的修改装饰器方法。
该方法接受一个值,该值将被渲染到 UPDATE 语句的 SET 子句中。然后,该方法应将此值处理为适合最终 SET 子句的单个列表达式,并将它们作为 2 元组的序列返回。每个元组包含一个列表达式作为键和要渲染的值。
例如:
class Person(Base): # ... first_name = Column(String) last_name = Column(String) @hybrid_property def fullname(self): return first_name + " " + last_name @fullname.update_expression def fullname(cls, value): fname, lname = value.split(" ", 1) return [ (cls.first_name, fname), (cls.last_name, lname) ]
1.2 版的新功能。
class sqlalchemy.ext.hybrid.Comparator
一个辅助类,允许轻松构建自定义PropComparator
类以与混合一起使用。
类签名
类sqlalchemy.ext.hybrid.Comparator
(sqlalchemy.orm.PropComparator
)
class sqlalchemy.ext.hybrid.HybridExtensionType
一个枚举。
成员
HYBRID_METHOD, HYBRID_PROPERTY
类签名
类sqlalchemy.ext.hybrid.HybridExtensionType
(sqlalchemy.orm.base.InspectionAttrExtensionType
)
attribute HYBRID_METHOD = 'HYBRID_METHOD'
表示一个InspectionAttr
的符号,其类型为hybrid_method
。
被赋予InspectionAttr.extension_type
属性。
另请参阅
Mapper.all_orm_attributes
attribute HYBRID_PROPERTY = 'HYBRID_PROPERTY'
表示一个InspectionAttr
的符号
类型为hybrid_method
。
被赋予InspectionAttr.extension_type
属性。
另请参阅
Mapper.all_orm_attributes
定义与属性行为不同的表达行为
在前一节中,我们在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)
在上面的示例中,首先将 hybrid_property
分配给名称 Interval.radius
,然后通过后续调用名为 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
描述符的用法:
# 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 的行为,这意味着这种语法在 SQLAlchemy 下不符合 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
识别为类而不是方法签名中的实例。
定义 Setter
hybrid_property.setter()
修饰符允许构建自定义 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
类具有链接到Interval.start
的混合类型start_point
,则可以直接替换:
from sqlalchemy import update stmt = update(Interval).values({Interval.start_point: 10})
然而,当使用像Interval.length
这样的复合混合类型时,这个混合类型代表多于一个列。我们可以设置一个处理程序,该处理程序将适应传递到 VALUES 表达式中的值,这可能会影响此处理程序,使用hybrid_property.update_expression()
装饰器。一个与我们的 setter 类似的处理程序将是:
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 语句的背景信息
SqlAlchemy 2.0 中文文档(三十二)(4)https://developer.aliyun.com/article/1562554