集合自定义和 API 详情
relationship()
函数定义了两个类之间的链接。当链接定义了一对多或多对多的关系时,在加载和操作对象时,它被表示为 Python 集合。本节介绍了有关集合配置和技术的其他信息。
自定义集合访问
将一对多或多对多的关系映射为一组可通过父实例上的属性访问的值的集合。对于这些关系的两种常见集合类型是 list
和 set
,在使用 Mapped
的 声明式 映射中,通过在 Mapped
容器中使用集合类型来建立,如下面的 Parent.children
集合中所示,其中使用了 list
:
from sqlalchemy import ForeignKey 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 Parent(Base): __tablename__ = "parent" parent_id: Mapped[int] = mapped_column(primary_key=True) # use a list children: Mapped[List["Child"]] = relationship() class Child(Base): __tablename__ = "child" child_id: Mapped[int] = mapped_column(primary_key=True) parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
或者使用 set
,在相同的 Parent.children
集合中进行说明:
from typing import Set from sqlalchemy import ForeignKey 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 Parent(Base): __tablename__ = "parent" parent_id: Mapped[int] = mapped_column(primary_key=True) # use a set children: Mapped[Set["Child"]] = relationship() class Child(Base): __tablename__ = "child" child_id: Mapped[int] = mapped_column(primary_key=True) parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
注意
如果使用 Python 3.7 或 3.8,则集合的注释需要使用 typing.List
或 typing.Set
,例如 Mapped[List["Child"]]
或 Mapped[Set["Child"]]
;在这些 Python 版本中,list
和 set
Python 内置的类型尚不支持通用注释,例如:
from typing import List class Parent(Base): __tablename__ = "parent" parent_id: Mapped[int] = mapped_column(primary_key=True) # use a List, Python 3.8 and earlier children: Mapped[List["Child"]] = relationship()
当使用没有 Mapped
注释的映射时,比如使用 命令式映射 或者未经类型化的 Python 代码,以及在一些特殊情况下,relationship()
的集合类始终可以直接使用 relationship.collection_class
参数进行指定:
# non-annotated mapping class Parent(Base): __tablename__ = "parent" parent_id = mapped_column(Integer, primary_key=True) children = relationship("Child", collection_class=set) class Child(Base): __tablename__ = "child" child_id = mapped_column(Integer, primary_key=True) parent_id = mapped_column(ForeignKey("parent.id"))
在缺少 relationship.collection_class
或 Mapped
的情况下,默认的集合类型是 list
。
除了内置的 list
和 set
,还支持两种字典的变体,下文将进行描述 字典集合。还支持任何任意可变序列类型可以设置为目标集合,需要一些额外的配置步骤;这在 自定义集合实现 部分进行了描述。
字典集合
当使用字典作为集合时需要一些额外的细节。这是因为对象总是作为列表从数据库加载的,必须提供一种键生成策略才能正确地填充字典。attribute_keyed_dict()
函数是实现简单字典集合的最常见方式。它生成一个字典类,该类将映射类的特定属性作为键。下面我们映射了一个包含以Note.keyword
属性作为键的Note
项目字典的Item
类。当使用attribute_keyed_dict()
时,可以使用Mapped
注释,可以使用KeyFuncDict
或仅使用普通的dict
,如下面的示例所示。然而,在这种情况下,需要使用relationship.collection_class
参数,以便适当地参数化attribute_keyed_dict()
:
from typing import Dict from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy.orm import attribute_keyed_dict 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 Item(Base): __tablename__ = "item" id: Mapped[int] = mapped_column(primary_key=True) notes: Mapped[Dict[str, "Note"]] = relationship( collection_class=attribute_keyed_dict("keyword"), cascade="all, delete-orphan", ) class Note(Base): __tablename__ = "note" id: Mapped[int] = mapped_column(primary_key=True) item_id: Mapped[int] = mapped_column(ForeignKey("item.id")) keyword: Mapped[str] text: Mapped[Optional[str]] def __init__(self, keyword: str, text: str): self.keyword = keyword self.text = text
Item.notes
然后是一个字典:
>>> item = Item() >>> item.notes["a"] = Note("a", "atext") >>> item.notes.items() {'a': <__main__.Note object at 0x2eaaf0>}
attribute_keyed_dict()
将确保每个Note
的.keyword
属性与字典中的键相匹配。例如,当分配给Item.notes
时,我们提供的字典键必须与实际Note
对象的键相匹配:
item = Item() item.notes = { "a": Note("a", "atext"), "b": Note("b", "btext"), }
attribute_keyed_dict()
用作键的属性根本不需要被映射!使用常规的 Python @property
允许使用对象的几乎任何细节或组合细节作为键,就像下面我们将其建立为Note.keyword
元组和Note.text
字段的前十个字母一样:
class Item(Base): __tablename__ = "item" id: Mapped[int] = mapped_column(primary_key=True) notes: Mapped[Dict[str, "Note"]] = relationship( collection_class=attribute_keyed_dict("note_key"), back_populates="item", cascade="all, delete-orphan", ) class Note(Base): __tablename__ = "note" id: Mapped[int] = mapped_column(primary_key=True) item_id: Mapped[int] = mapped_column(ForeignKey("item.id")) keyword: Mapped[str] text: Mapped[str] item: Mapped["Item"] = relationship() @property def note_key(self): return (self.keyword, self.text[0:10]) def __init__(self, keyword: str, text: str): self.keyword = keyword self.text = text
在上面,我们添加了一个带有双向relationship.back_populates
配置的Note.item
关系。将Note
分配给这个反向关系时,Note
被添加到Item.notes
字典中,并且键会自动为我们生成:
>>> item = Item() >>> n1 = Note("a", "atext") >>> n1.item = item >>> item.notes {('a', 'atext'): <__main__.Note object at 0x2eaaf0>}
其他内置的字典类型包括column_keyed_dict()
,几乎与attribute_keyed_dict()
类似,只是直接给出Column
对象:
from sqlalchemy.orm import column_keyed_dict class Item(Base): __tablename__ = "item" id: Mapped[int] = mapped_column(primary_key=True) notes: Mapped[Dict[str, "Note"]] = relationship( collection_class=column_keyed_dict(Note.__table__.c.keyword), cascade="all, delete-orphan", )
以及mapped_collection()
,它接受任何可调用函数。请注意,通常更容易使用前面提到的@property
与attribute_keyed_dict()
一起使用:
from sqlalchemy.orm import mapped_collection class Item(Base): __tablename__ = "item" id: Mapped[int] = mapped_column(primary_key=True) notes: Mapped[Dict[str, "Note"]] = relationship( collection_class=mapped_collection(lambda note: note.text[0:10]), cascade="all, delete-orphan", )
字典映射经常与“Association Proxy”扩展组合以产生简化的字典视图。请参阅 Proxying to Dictionary Based Collections 和 Composite Association Proxies 以获取示例。
处理键突变和字典集合的反向填充
当使用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>}
vs:
>>> 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()`)。 集合包理解列表、集合和字典的基本接口,并将自动对这些内置类型及其子类应用仪表化。实现基本集合接口的对象衍生类型会通过鸭子类型检测到并进行仪表化: ```py 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 标记的附加、删除和迭代接口。附加和删除方法将以映射的实体作为单个参数调用,迭代器方法将不带参数调用,并且必须返回一个迭代器。
SqlAlchemy 2.0 中文文档(十四)(2)https://developer.aliyun.com/article/1562970