SqlAlchemy 2.0 中文文档(十四)(3)https://developer.aliyun.com/article/1562971
处理键变化和字典集合的反向填充
当使用attribute_keyed_dict()
时,字典的“键”来自目标对象上的属性。对此键的更改不会被跟踪。这意味着键必须在首次使用时被分配,并且如果键发生更改,则集合将不会发生变化。一个典型的例子是当依赖反向引用来填充属性映射集合时可能会出现问题。给定以下内容:
class A(Base): __tablename__ = "a" id: Mapped[int] = mapped_column(primary_key=True) bs: Mapped[Dict[str, "B"]] = relationship( collection_class=attribute_keyed_dict("data"), back_populates="a", ) class B(Base): __tablename__ = "b" id: Mapped[int] = mapped_column(primary_key=True) a_id: Mapped[int] = mapped_column(ForeignKey("a.id")) data: Mapped[str] a: Mapped["A"] = relationship(back_populates="bs")
如果我们创建引用特定A()
的B()
,那么反向填充将把B()
添加到A.bs
集合中,但是如果B.data
的值尚未设置,则键将为None
:
>>> a1 = A() >>> b1 = B(a=a1) >>> a1.bs {None: <test3.B object at 0x7f7b1023ef70>}
在事后设置b1.data
不会更新集合:
>>> b1.data = "the key" >>> a1.bs {None: <test3.B object at 0x7f7b1023ef70>}
如果尝试在构造函数中设置 B()
,也可以看到这一点。参数的顺序会改变结果:
>>> B(a=a1, data="the key") <test3.B object at 0x7f7b10114280> >>> a1.bs {None: <test3.B object at 0x7f7b10114280>}
对比:
>>> B(data="the key", a=a1) <test3.B object at 0x7f7b10114340> >>> a1.bs {'the key': <test3.B object at 0x7f7b10114340>}
如果以这种方式使用反向引用,请确保使用__init__
方法以正确的顺序填充属性。
还可以使用以下事件处理程序来跟踪集合中的更改:
from sqlalchemy import event from sqlalchemy.orm import attributes @event.listens_for(B.data, "set") def set_item(obj, value, previous, initiator): if obj.a is not None: previous = None if previous == attributes.NO_VALUE else previous obj.a.bs[value] = obj obj.a.bs.pop(previous) ```#### 处理键变化和字典集合的反向填充 当使用`attribute_keyed_dict()`时,字典的“键”来自目标对象上的属性。**对此键的更改不会被跟踪**。这意味着键必须在首次使用时被分配,并且如果键发生更改,则集合将不会发生变化。一个典型的例子是当依赖反向引用来填充属性映射集合时可能会出现问题。给定以下内容: ```py class A(Base): __tablename__ = "a" id: Mapped[int] = mapped_column(primary_key=True) bs: Mapped[Dict[str, "B"]] = relationship( collection_class=attribute_keyed_dict("data"), back_populates="a", ) class B(Base): __tablename__ = "b" id: Mapped[int] = mapped_column(primary_key=True) a_id: Mapped[int] = mapped_column(ForeignKey("a.id")) data: Mapped[str] a: Mapped["A"] = relationship(back_populates="bs")
如果我们创建引用特定A()
的B()
,那么反向填充将把B()
添加到A.bs
集合中,但是如果B.data
的值尚未设置,则键将为None
:
>>> a1 = A() >>> b1 = B(a=a1) >>> a1.bs {None: <test3.B object at 0x7f7b1023ef70>}
在事后设置b1.data
不会更新集合:
>>> b1.data = "the key" >>> a1.bs {None: <test3.B object at 0x7f7b1023ef70>}
如果尝试在构造函数中设置 B()
,也可以看到这一点。参数的顺序会改变结果:
>>> B(a=a1, data="the key") <test3.B object at 0x7f7b10114280> >>> a1.bs {None: <test3.B object at 0x7f7b10114280>}
对比:
>>> B(data="the key", a=a1) <test3.B object at 0x7f7b10114340> >>> a1.bs {'the key': <test3.B object at 0x7f7b10114340>}
如果以这种方式使用反向引用,请确保使用__init__
方法以正确的顺序填充属性。
还可以使用以下事件处理程序来跟踪集合中的更改:
from sqlalchemy import event from sqlalchemy.orm import attributes @event.listens_for(B.data, "set") def set_item(obj, value, previous, initiator): if obj.a is not None: previous = None if previous == attributes.NO_VALUE else previous obj.a.bs[value] = obj obj.a.bs.pop(previous)
自定义集合实现
您也可以为集合使用自己的类型。在简单情况下,继承自 list
或 set
,添加自定义行为即可。在其他情况下,需要特殊的装饰器来告诉 SQLAlchemy 关于集合操作的更多细节。
SQLAlchemy 中的集合是透明的 instrumented。仪器化意味着对集合的常规操作将被跟踪,并在刷新时写入数据库中。此外,集合操作可以触发 事件,指示必须进行某些次要操作。次要操作的示例包括在父项的Session
(即 save-update
级联)中保存子项,以及同步双向关系的状态(即 backref()
)。
集合包理解列表、集合和字典的基本接口,并将自动将仪器应用于这些内置类型及其子类。通过鸭子类型检测实现基本集合接口的对象派生类型,以进行仪器化:
class ListLike: def __init__(self): self.data = [] def append(self, item): self.data.append(item) def remove(self, item): self.data.remove(item) def extend(self, items): self.data.extend(items) def __iter__(self): return iter(self.data) def foo(self): return "foo"
append
、remove
和 extend
是 list
的已知成员,并将自动进行仪器化。__iter__
不是一个修改器方法,不会被仪器化,foo
也不会被仪器化。
当然,鸭子类型(即猜测)并不是百分之百可靠的,所以您可以通过提供一个 __emulates__
类属性来明确您正在实现的接口:
class SetLike: __emulates__ = set def __init__(self): self.data = set() def append(self, item): self.data.add(item) def remove(self, item): self.data.remove(item) def __iter__(self): return iter(self.data)
此类似于 Python list
(即 “类似列表”),因为它有一个 append
方法,但 __emulates__
属性强制将其视为 set
。 remove
已知是集合接口的一部分,并将被仪器化。
但是,此类暂时无法正常工作:需要一点粘合剂来使其适应 SQLAlchemy 的使用。ORM 需要知道用于附加、删除和迭代集合成员的方法。当使用类似于 list
或 set
的类型时,当存在时,适当的方法是众所周知的并且会自动使用。然而,上面的类,虽然只大致类似于 set
,但并未提供预期的 add
方法,因此我们必须告诉 ORM 替代 add
方法的方法,在这种情况下使用装饰器 @collection.appender
;这在下一节中进行了说明。
通过装饰器注释自定义集合
当您的类不完全符合其容器类型的常规接口时,或者您希望以其他方式使用不同的方法来完成工作时,可以使用装饰器标记 ORM 需要管理集合的各个方法。
from sqlalchemy.orm.collections import collection class SetLike: __emulates__ = set def __init__(self): self.data = set() @collection.appender def append(self, item): self.data.add(item) def remove(self, item): self.data.remove(item) def __iter__(self): return iter(self.data)
而这就是完成示例所需的全部。SQLAlchemy 将通过append
方法添加实例。remove
和__iter__
是集合的默认方法,将用于移除和迭代。默认方法也可以更改:
from sqlalchemy.orm.collections import collection class MyList(list): @collection.remover def zark(self, item): # do something special... ... @collection.iterator def hey_use_this_instead_for_iteration(self): ...
完全不需要“类似于列表”或“类似于集合”。集合类可以是任何形状,只要它们有被标记为 SQLAlchemy 使用的 append、remove 和 iterate 接口。append 和 remove 方法将以映射实体作为唯一参数调用,迭代器方法将以无参数调用,并且必须返回迭代器。
自定义基于字典的集合
KeyFuncDict
类可以用作自定义类型的基类,也可以作为混合类快速为其他类添加dict
集合支持。它使用一个键函数来委托__setitem__
和__delitem__
:
from sqlalchemy.orm.collections import KeyFuncDict class MyNodeMap(KeyFuncDict): """Holds 'Node' objects, keyed by the 'name' attribute.""" def __init__(self, *args, **kw): super().__init__(keyfunc=lambda node: node.name) dict.__init__(self, *args, **kw)
当子类化KeyFuncDict
时,如果用户定义了__setitem__()
或__delitem__()
的版本,并且它们调用了相同的方法KeyFuncDict
上的方法,则应使用collection.internally_instrumented()
进行装饰,如果它们调用了相同的方法KeyFuncDict
上的方法。这是因为KeyFuncDict
上的方法已经被仪器化了 - 从已经仪器化的调用中调用它们可能会导致事件被重复触发,或者不适当地触发,在极少数情况下会导致内部状态损坏:
from sqlalchemy.orm.collections import KeyFuncDict, collection class MyKeyFuncDict(KeyFuncDict): """Use @internally_instrumented when your methods call down to already-instrumented methods. """ @collection.internally_instrumented def __setitem__(self, key, value, _sa_initiator=None): # do something with key, value super(MyKeyFuncDict, self).__setitem__(key, value, _sa_initiator) @collection.internally_instrumented def __delitem__(self, key, _sa_initiator=None): # do something with key super(MyKeyFuncDict, self).__delitem__(key, _sa_initiator)
ORM 与dict
接口的理解方式与列表和集合一样,并且如果选择对dict
进行子类化或在鸭式类型的类中提供类似于字典的集合行为,则会自动对所有“类似于字典”的方法进行仪器化。但是,必须装饰追加和删除方法 - 基本字典接口中没有兼容的方法供 SQLAlchemy 默认使用。迭代将通过values()
进行,除非另有装饰。
仪器化和自定义类型
许多自定义类型和现有库类可以直接使用作为实体集合类型,无需额外操作。但是,重要的是要注意,仪器化过程将修改类型,自动在方法周围添加装饰器。
装饰很轻量级,在关系之外不起作用,但是当在其他地方触发时会增加不必要的开销。当将库类用作集合时,最好使用“微不足道的子类”技巧将装饰限制为关系中的使用。例如:
class MyAwesomeList(some.great.library.AwesomeList): pass # ... relationship(..., collection_class=MyAwesomeList)
ORM 使用这种方法进行内置,当直接使用list
、set
或dict
时,会悄悄地替换为一个微不足道的子类。
通过装饰器注释自定义集合
可以使用装饰器标记 ORM 需要管理集合的各个方法。当您的类不完全符合其容器类型的常规接口时,或者当您希望以不同的方法完成工作时,请使用它们。
from sqlalchemy.orm.collections import collection class SetLike: __emulates__ = set def __init__(self): self.data = set() @collection.appender def append(self, item): self.data.add(item) def remove(self, item): self.data.remove(item) def __iter__(self): return iter(self.data)
这就是完成示例所需的全部内容。SQLAlchemy 将通过append
方法添加实例。remove
和__iter__
是集合的默认方法,将用于删除和迭代。默认方法也可以更改:
from sqlalchemy.orm.collections import collection class MyList(list): @collection.remover def zark(self, item): # do something special... ... @collection.iterator def hey_use_this_instead_for_iteration(self): ...
完全不需要“类似于列表”或“类似于集合”。集合类可以是任何形状,只要它们具有标记为 SQLAlchemy 使用的追加、移除和迭代接口即可。追加和移除方法将以映射实体作为单个参数调用,并且迭代方法将不带参数调用,并且必须返回迭代器。
自定义基于字典的集合
KeyFuncDict
类可以作为自定义类型的基类,也可以作为混合类快速将dict
集合支持添加到其他类中。它使用键函数来委托__setitem__
和__delitem__
:
from sqlalchemy.orm.collections import KeyFuncDict class MyNodeMap(KeyFuncDict): """Holds 'Node' objects, keyed by the 'name' attribute.""" def __init__(self, *args, **kw): super().__init__(keyfunc=lambda node: node.name) dict.__init__(self, *args, **kw)
当子类化KeyFuncDict
时,用户定义的__setitem__()
或__delitem__()
版本应该用collection.internally_instrumented()
进行修饰,如果它们调用同样的方法。这是因为KeyFuncDict
上的方法已经被仪器化-在已经仪器化的调用中调用它们可能会导致事件重复触发,或者在罕见情况下导致内部状态损坏:
from sqlalchemy.orm.collections import KeyFuncDict, collection class MyKeyFuncDict(KeyFuncDict): """Use @internally_instrumented when your methods call down to already-instrumented methods. """ @collection.internally_instrumented def __setitem__(self, key, value, _sa_initiator=None): # do something with key, value super(MyKeyFuncDict, self).__setitem__(key, value, _sa_initiator) @collection.internally_instrumented def __delitem__(self, key, _sa_initiator=None): # do something with key super(MyKeyFuncDict, self).__delitem__(key, _sa_initiator)
ORM 理解dict
接口就像理解列表和集合一样,并且如果您选择子类化dict
或在鸭子类型的类中提供类似于 dict 的集合行为,则会自动仪器化所有“类似于 dict”的方法。但是,您必须修饰追加和移除方法-默认情况下,基本字典接口中没有兼容的方法供 SQLAlchemy 使用。迭代将通过values()
进行,除非另有修饰。
仪器化和自定义类型
许多自定义类型和现有的库类可以直接用作实体集合类型,无需进一步操作。但是,需要注意的是,仪器化过程将修改类型,自动在方法周围添加修饰符。
装饰是轻量级的,并且在关系之外不起作用,但是当在其他地方触发时它们会增加不必要的开销。当将库类用作集合时,使用“trivial subclass”技巧将装饰限制为仅在关系中使用的情况可能是一个好习惯。例如:
class MyAwesomeList(some.great.library.AwesomeList): pass # ... relationship(..., collection_class=MyAwesomeList)
ORM 使用此方法处理内置功能,当直接使用 list
、set
或 dict
时,会静默地替换为一个微不足道的子类。
SqlAlchemy 2.0 中文文档(十四)(5)https://developer.aliyun.com/article/1562973