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

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

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


自定义集合访问

映射一对多或多对多的关系会导致通过父实例上的属性访问的值集合。这两种常见的集合类型是 listset,在使用 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.Listtyping.Set,例如 Mapped[List["Child"]]Mapped[Set["Child"]];在这些 Python 版本中,listset 内置的 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_classMapped 的情况下,默认的集合类型是 list

除了内置的 listset 外,还支持两种字典的变体,下面在 字典集合 中描述。还支持将任何任意可变序列类型设置为目标集合,只需进行一些额外的配置步骤;这在 自定义集合实现 部分有描述。

字典集合

使用字典作为集合时需要一些额外的细节。这是因为对象总是以列表形式从数据库加载的,必须提供一种键生成策略以正确地填充字典。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.keywordNote.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

相关文章
|
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 关系型数据库 API
SqlAlchemy 2.0 中文文档(十七)(4)
SqlAlchemy 2.0 中文文档(十七)
78 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 数据库 C++
SqlAlchemy 2.0 中文文档(十四)(1)
SqlAlchemy 2.0 中文文档(十四)
31 1
|
4月前
|
数据库 Python 容器
SqlAlchemy 2.0 中文文档(十四)(4)
SqlAlchemy 2.0 中文文档(十四)
32 1
|
4月前
|
API 数据库 Python
SqlAlchemy 2.0 中文文档(十四)(2)
SqlAlchemy 2.0 中文文档(十四)
45 1