SqlAlchemy 2.0 中文文档(十四)(4)

简介: SqlAlchemy 2.0 中文文档(十四)

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)

自定义集合实现

您也可以为集合使用自己的类型。在简单情况下,继承自 listset,添加自定义行为即可。在其他情况下,需要特殊的装饰器来告诉 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"

appendremoveextendlist 的已知成员,并将自动进行仪器化。__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__ 属性强制将其视为 setremove 已知是集合接口的一部分,并将被仪器化。

但是,此类暂时无法正常工作:需要一点粘合剂来使其适应 SQLAlchemy 的使用。ORM 需要知道用于附加、删除和迭代集合成员的方法。当使用类似于 listset 的类型时,当存在时,适当的方法是众所周知的并且会自动使用。然而,上面的类,虽然只大致类似于 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 使用这种方法进行内置,当直接使用listsetdict时,会悄悄地替换为一个微不足道的子类。

通过装饰器注释自定义集合

可以使用装饰器标记 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 使用此方法处理内置功能,当直接使用 listsetdict 时,会静默地替换为一个微不足道的子类。


SqlAlchemy 2.0 中文文档(十四)(5)https://developer.aliyun.com/article/1562973

相关文章
|
4月前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(十一)(4)
SqlAlchemy 2.0 中文文档(十一)
51 11
|
4月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(十一)(3)
SqlAlchemy 2.0 中文文档(十一)
43 11
|
4月前
|
存储 SQL Python
SqlAlchemy 2.0 中文文档(十一)(5)
SqlAlchemy 2.0 中文文档(十一)
43 10
|
4月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(十七)(2)
SqlAlchemy 2.0 中文文档(十七)
38 4
|
4月前
|
SQL API 数据库
SqlAlchemy 2.0 中文文档(十一)(1)
SqlAlchemy 2.0 中文文档(十一)
41 2
|
4月前
|
存储 SQL 数据库
SqlAlchemy 2.0 中文文档(十一)(2)
SqlAlchemy 2.0 中文文档(十一)
30 2
|
4月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(十二)(5)
SqlAlchemy 2.0 中文文档(十二)
26 2
|
4月前
|
API 数据库 Python
SqlAlchemy 2.0 中文文档(十四)(5)
SqlAlchemy 2.0 中文文档(十四)
40 1
|
4月前
|
API 数据库 C++
SqlAlchemy 2.0 中文文档(十四)(1)
SqlAlchemy 2.0 中文文档(十四)
31 1
|
4月前
|
数据库 Python 容器
SqlAlchemy 2.0 中文文档(十四)(3)
SqlAlchemy 2.0 中文文档(十四)
28 1