SqlAlchemy 2.0 中文文档(十四)(2)https://developer.aliyun.com/article/1562970
自定义集合访问
映射一对多或多对多的关系会导致通过父实例上的属性访问的值集合。这两种常见的集合类型是 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
在上面的示例中,我们添加了一个 Note.item
关系,具有双向的 relationship.back_populates
配置。将 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()
。请注意,通常更容易使用 attribute_keyed_dict()
以及前面提到的 @property
:
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”扩展结合使用以生成简化的字典视图。有关示例,请参见代理到基于字典的集合 和 复合关联代理。
处理键的突变和字典集合的反向填充
当使用 attribute_keyed_dict()
时,字典的“键”来自目标对象上的属性。不会跟踪此键的更改。这意味着必须在首次使用时分配键,如果键更改,则集合将不会发生变化。可能出现问题的典型示例是依赖 backrefs 填充属性映射集合。给定以下情况:
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()
,那么 back populates 将把 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>}
如果 backrefs 被这样使用,请确保使用 __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()` 函数是实现简单字典集合的最常见方式。它生成一个字典类,该类将应用映射类的特定属性作为键。下面我们映射了一个包含以`Note.keyword`属性为键的`Note`项目字典的`Item`类。在使用`attribute_keyed_dict()`时,可以使用`Mapped`注释使用`KeyFuncDict`或普通的`dict`,如下例所示。然而,在这种情况下,必须使用`relationship.collection_class`参数,以便适当地对`attribute_keyed_dict()`进行参数化: ```py 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
被添加到 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", )
字典映射经常与“关联代理”扩展组合以产生流畅的字典视图。参见基于字典的集合的代理和复合关联代理以获取示例。
SqlAlchemy 2.0 中文文档(十四)(4)https://developer.aliyun.com/article/1562972