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

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


原文:docs.sqlalchemy.org/en/20/contents.html

集合自定义和 API 详情

原文:docs.sqlalchemy.org/en/20/orm/collection_api.html

relationship() 函数定义了两个类之间的链接。当链接定义了一对多或多对多的关系时,在加载和操作对象时,它被表示为 Python 集合。本节介绍了有关集合配置和技术的其他信息。

自定义集合访问

一对多或多对多的关系映射为一组可通过父实例上的属性访问的值的集合。对于这些关系的两种常见集合类型是 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.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(),它接受任何可调用函数。请注意,通常更容易使用前面提到的@propertyattribute_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"

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 标记的附加、删除和迭代接口。附加和删除方法将以映射的实体作为单个参数调用,迭代器方法将不带参数调用,并且必须返回一个迭代器。


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

相关文章
|
3月前
|
SQL 关系型数据库 API
SqlAlchemy 2.0 中文文档(十七)(4)
SqlAlchemy 2.0 中文文档(十七)
53 4
|
3月前
|
SQL 关系型数据库 数据库
SqlAlchemy 2.0 中文文档(十七)(3)
SqlAlchemy 2.0 中文文档(十七)
34 4
|
3月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(十七)(2)
SqlAlchemy 2.0 中文文档(十七)
33 4
|
3月前
|
SQL API 数据库
SqlAlchemy 2.0 中文文档(十一)(1)
SqlAlchemy 2.0 中文文档(十一)
37 2
|
3月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(十二)(5)
SqlAlchemy 2.0 中文文档(十二)
21 2
|
3月前
|
数据库 Python 容器
SqlAlchemy 2.0 中文文档(十四)(3)
SqlAlchemy 2.0 中文文档(十四)
25 1
|
3月前
|
API 数据库 Python
SqlAlchemy 2.0 中文文档(十四)(2)
SqlAlchemy 2.0 中文文档(十四)
28 1
|
3月前
|
API 数据库 Python
SqlAlchemy 2.0 中文文档(十四)(5)
SqlAlchemy 2.0 中文文档(十四)
35 1
|
3月前
|
数据库 Python 容器
SqlAlchemy 2.0 中文文档(十四)(4)
SqlAlchemy 2.0 中文文档(十四)
26 1
|
3月前
|
测试技术 Python 容器
SqlAlchemy 2.0 中文文档(十二)(2)
SqlAlchemy 2.0 中文文档(十二)
31 1
下一篇
无影云桌面