与 dataclasses 和 attrs 集成

从 SQLAlchemy 2.0 版本开始,支持 “原生 dataclass” 集成,其中,带注解的声明式表 映射可以通过在映射类中添加单个 mixin 或装饰器来转换为 Python dataclass

版本 2.0 中的新增内容: 将 dataclass 创建与 ORM Declarative 类集成

还提供了一些模式,可以将现有 dataclasses 映射,以及映射由 attrs 第三方集成库进行检测的类。

声明式 Dataclass 映射

SQLAlchemy 带注解的声明式表 映射可以通过额外的 mixin 类或装饰器指令进行增强,这将为映射过程添加额外的步骤,在映射完成后,将就地将映射类转换为 Python dataclass,然后完成映射过程,该过程将 ORM 特定的 检测 应用于该类。这带来的最显著的行为添加是生成一个 __init__() 方法,该方法对带或不带默认值的 positional 和 keyword 参数进行细粒度控制,以及生成 __repr__()__eq__() 等方法。

PEP 484 类型化的角度来看,该类被识别为具有 Dataclass 特定的行为,最值得注意的是利用了 PEP 681 “Dataclass 变换”,这允许类型化工具将该类视为使用 @dataclasses.dataclass 装饰器显式装饰的类。

注意

截至2023 年 4 月 4 日,类型化工具对 PEP 681 的支持有限,目前已知 PyrightMypy (截至1.2 版本) 支持。请注意,Mypy 1.1.1 引入了 PEP 681 支持,但并未正确处理 Python 描述符,这会导致在使用 SQLAlchemy 的 ORM 映射方案时出现错误。

另请参见

https://peps.pythonlang.cn/pep-0681/#the-dataclass-transform-decorator - 关于 SQLAlchemy 等库如何实现 PEP 681 支持的背景信息

可以通过向任何 Declarative 类添加 MappedAsDataclass mixin (用于 DeclarativeBase 类层次结构) 或使用 registry.mapped_as_dataclass() 类装饰器 (用于装饰器映射) 来添加 Dataclass 转换。

MappedAsDataclass mixin 可以应用于 Declarative Base 类或任何超类,如下面的示例所示

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(MappedAsDataclass, DeclarativeBase):
    """subclasses will be converted to dataclasses"""


class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

或者可以直接应用于扩展 Declarative 基类的类

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(DeclarativeBase):
    pass


class User(MappedAsDataclass, Base):
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

使用装饰器形式时,只支持 registry.mapped_as_dataclass() 装饰器

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry


reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

类级特性配置

对 dataclasses 特性的支持是部分的。目前支持 initrepreqorderunsafe_hash 特性,match_argskw_only 在 Python 3.10+ 上受支持。目前不支持 frozenslots 特性。

使用 MappedAsDataclass 的 mixin 类形式时,类配置参数作为类级参数传递

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass


class Base(DeclarativeBase):
    pass


class User(MappedAsDataclass, Base, repr=False, unsafe_hash=True):
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

使用 registry.mapped_as_dataclass() 的装饰器形式时,类配置参数直接传递给装饰器

from sqlalchemy.orm import registry
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


reg = registry()


@reg.mapped_as_dataclass(unsafe_hash=True)
class User:
    """User class will be converted to a dataclass"""

    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

有关 dataclass 类选项的背景信息,请参阅 dataclasses 文档,网址为 @dataclasses.dataclass

属性配置

SQLAlchemy 原生 dataclasses 与普通 dataclasses 的区别在于,要映射的属性在所有情况下都使用 Mapped 通用注解容器进行描述。映射遵循与 带 mapped_column() 的声明式表 中记录的相同形式,并支持 mapped_column()Mapped 的所有特性。

此外,ORM 属性配置构造 (包括 mapped_column()relationship()composite()) 支持每个属性的字段选项,包括 initdefaultdefault_factoryrepr。这些参数的名称是固定的,如 PEP 681 中所指定。功能等效于 dataclasses

与 dataclasses 的另一个主要区别是,属性的默认值 **必须** 使用 ORM 结构的 default 参数进行配置,例如 mapped_column(default=None)。不支持类似 dataclass 语法的语法,该语法接受简单的 Python 值作为默认值,而无需使用 @dataclases.field()

例如,使用 mapped_column(),下面的映射将生成一个 __init__() 方法,该方法只接受 namefullname 字段,其中 name 是必需的,可以按位置传递,而 fullname 是可选的。我们期望由数据库生成的 id 字段根本不属于构造函数。

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str] = mapped_column(default=None)


# 'fullname' is optional keyword argument
u1 = User("name")

列默认值

为了适应 default 参数与现有 Column.default 参数(Column 结构)的名称重叠,mapped_column() 结构通过添加一个新的参数 mapped_column.insert_default 来区分这两个名称,该参数将直接填充到 Column.default 参数(Column)中,独立于 mapped_column.default 上可能设置的内容,mapped_column.default 始终用于 dataclasses 配置。例如,要配置一个 datetime 列,其 Column.default 设置为 func.utc_timestamp() SQL 函数,但参数在构造函数中是可选的。

from datetime import datetime

from sqlalchemy import func
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    created_at: Mapped[datetime] = mapped_column(
        insert_default=func.utc_timestamp(), default=None
    )

使用上面的映射,对一个新的 User 对象进行 INSERT 操作,其中没有为 created_at 传递参数,操作过程如下。

>>> with Session(e) as session:
...     session.add(User())
...     session.commit()
BEGIN (implicit) INSERT INTO user_account (created_at) VALUES (utc_timestamp()) [generated in 0.00010s] () COMMIT

与 Annotated 集成

将整个列声明映射到 Python 类型 中介绍的方法说明了如何使用 PEP 593 Annotated 对象来打包整个 mapped_column() 结构以供重复使用。此功能受 dataclasses 功能支持。但是,此功能的一个方面在使用类型工具时需要变通方法,即 PEP 681 特定参数 initdefaultreprdefault_factory **必须** 位于右侧,打包到显式的 mapped_column() 结构中,以便类型工具能够正确解释该属性。例如,下面的方法在运行时可以正常工作,但是类型工具将认为 User() 构造无效,因为它们看不到 init=False 参数存在。

from typing import Annotated

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

# typing tools will ignore init=False here
intpk = Annotated[int, mapped_column(init=False, primary_key=True)]

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"
    id: Mapped[intpk]


# typing error: Argument missing for parameter "id"
u1 = User()

相反,mapped_column() 也必须出现在右侧,并为 mapped_column.init 设置显式值;其他参数可以保留在 Annotated 结构中。

from typing import Annotated

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

intpk = Annotated[int, mapped_column(primary_key=True)]

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    # init=False and other pep-681 arguments must be inline
    id: Mapped[intpk] = mapped_column(init=False)


u1 = User()

使用 mixin 和抽象超类

MappedAsDataclass 映射类中使用的任何 mixin 或基类,这些类包含 Mapped 属性,本身也必须是 MappedAsDataclass 层次结构的一部分,例如下面使用 mixin 的示例。

class Mixin(MappedAsDataclass):
    create_user: Mapped[int] = mapped_column()
    update_user: Mapped[Optional[int]] = mapped_column(default=None, init=False)


class Base(DeclarativeBase, MappedAsDataclass):
    pass


class User(Base, Mixin):
    __tablename__ = "sys_user"

    uid: Mapped[str] = mapped_column(
        String(50), init=False, default_factory=uuid4, primary_key=True
    )
    username: Mapped[str] = mapped_column()
    email: Mapped[str] = mapped_column()

支持 PEP 681 的 Python 类型检查器不会将来自非 dataclass mixin 的属性视为 dataclass 的一部分。

自版本 2.0.8 起已弃用: MappedAsDataclassregistry.mapped_as_dataclass() 层次结构中使用非 dataclass 的 mixin 和抽象基类已弃用,因为这些字段不受 PEP 681 支持,它们不属于 dataclass。对于这种情况,将发出一个警告,稍后该警告将成为错误。

关系配置

Mapped 注释与 relationship() 结合使用,与 基本关系模式 中描述的方式相同。当将基于集合的 relationship() 指定为可选的关键字参数时,必须传递 relationship.default_factory 参数,并且它必须引用要使用的集合类。多对一和标量对象引用可以使用 relationship.default,如果默认值应为 None

from typing import List

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

reg = registry()


@reg.mapped_as_dataclass
class Parent:
    __tablename__ = "parent"
    id: Mapped[int] = mapped_column(primary_key=True)
    children: Mapped[List["Child"]] = relationship(
        default_factory=list, back_populates="parent"
    )


@reg.mapped_as_dataclass
class Child:
    __tablename__ = "child"
    id: Mapped[int] = mapped_column(primary_key=True)
    parent_id: Mapped[int] = mapped_column(ForeignKey("parent.id"))
    parent: Mapped["Parent"] = relationship(default=None)

上面的映射将在构造一个新的 Parent() 对象时为 Parent.children 生成一个空列表,而无需传递 children,类似地,在构造一个新的 Child() 对象时为 Child.parent 生成一个 None 值,而无需传递 parent

虽然 relationship.default_factory 可以从 relationship() 给定的集合类自动推断,但这会破坏与 dataclasses 的兼容性,因为 relationship.default_factoryrelationship.default 的存在决定了参数在渲染到 __init__() 方法时是必需还是可选。

使用未映射的 Dataclass 字段

当使用声明式 dataclasses 时,类上也可以使用未映射的字段,这些字段将成为 dataclass 构造过程的一部分,但不会被映射。任何不使用 Mapped 的字段都会被映射过程忽略。在下面的示例中,字段 ctrl_onectrl_two 将成为对象实例级状态的一部分,但不会被 ORM 持久化。

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class Data:
    __tablename__ = "data"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    status: Mapped[str]

    ctrl_one: Optional[str] = None
    ctrl_two: Optional[str] = None

上面 Data 的实例可以创建为

d1 = Data(status="s1", ctrl_one="ctrl1", ctrl_two="ctrl2")

一个更真实的例子可能是结合使用 Dataclasses 的 InitVar 特性以及 __post_init__() 特性来接收仅用于初始化的字段,这些字段可用于组合持久化数据。在下面的示例中,User 类使用 idnamepassword_hash 作为映射特征进行声明,但使用仅用于初始化的 passwordrepeat_password 字段来表示用户创建过程(注意:要运行此示例,请将函数 your_crypt_function_here() 替换为第三方加密函数,例如 bcryptargon2-cffi

from dataclasses import InitVar
from typing import Optional

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import registry

reg = registry()


@reg.mapped_as_dataclass
class User:
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(init=False, primary_key=True)
    name: Mapped[str]

    password: InitVar[str]
    repeat_password: InitVar[str]

    password_hash: Mapped[str] = mapped_column(init=False, nullable=False)

    def __post_init__(self, password: str, repeat_password: str):
        if password != repeat_password:
            raise ValueError("passwords do not match")

        self.password_hash = your_crypt_function_here(password)

上面的对象使用参数 passwordrepeat_password 创建,这些参数在前端被消耗,以便生成 password_hash 变量。

>>> u1 = User(name="some_user", password="xyz", repeat_password="xyz")
>>> u1.password_hash
'$6$9ppc... (example crypted string....)'

在版本 2.0.0rc1 中更改: 当使用 registry.mapped_as_dataclass()MappedAsDataclass 时,不包含 Mapped 注释的字段可以被包含,这些字段将被视为结果 dataclass 的一部分,但不会被映射,而无需再指定 __allow_unmapped__ 类属性。之前的 2.0 beta 版本需要显式地存在此属性,即使此属性的目的是为了允许传统的 ORM 类型映射继续工作。

与 Pydantic 等其他 Dataclass 提供商集成

警告

Pydantic 的 dataclass 层与 SQLAlchemy 的类检测不完全兼容,需要额外的内部更改,许多功能,例如相关集合,可能无法正常工作。

为了与 Pydantic 兼容,请考虑使用 SQLModel ORM,它是在 SQLAlchemy ORM 之上使用 Pydantic 构建的,其中包含明确解决这些不兼容性的特殊实现细节。

SQLAlchemy 的 MappedAsDataclass 类和 registry.mapped_as_dataclass() 方法调用直接进入 Python 标准库的 dataclasses.dataclass 类装饰器,在声明式映射过程应用于类之后。此函数调用可以被替换为其他 Dataclasses 提供商,例如 Pydantic 的提供商,使用 dataclass_callable 参数,该参数被 MappedAsDataclass 作为类关键字参数以及 registry.mapped_as_dataclass() 接受。

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import MappedAsDataclass
from sqlalchemy.orm import registry


class Base(
    MappedAsDataclass,
    DeclarativeBase,
    dataclass_callable=pydantic.dataclasses.dataclass,
):
    pass


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]

上面的 User 类将被应用为 dataclass,使用 Pydantic 的 pydantic.dataclasses.dataclasses 可调用对象。此过程适用于映射类以及扩展自 MappedAsDataclass 或直接应用了 registry.mapped_as_dataclass() 的 mixin。

在版本 2.0.4 中添加: MappedAsDataclassregistry.mapped_as_dataclass() 添加了 dataclass_callable 类和方法参数,并调整了一些 dataclass 内部内容以适应更严格的 dataclass 函数,例如 Pydantic 的函数。

将 ORM 映射应用于现有的 dataclass(传统 dataclass 使用)

传统功能

此处描述的方法已被 SQLAlchemy 2.0 系列中新引入的 声明式 Dataclass 映射 功能所取代。此功能的较新版本构建在版本 1.4 中首次添加的 dataclass 支持的基础上,本节将对此进行描述。

要映射现有的 dataclass,SQLAlchemy 的“内联”声明式指令不能直接使用;ORM 指令使用三种技术之一分配。

SQLAlchemy 将映射应用于数据类的通用过程与普通类相同,但它还会检测数据类声明过程中包含的类级属性,并在运行时将它们替换为常用的 SQLAlchemy ORM 映射属性。由数据类生成的 __init__ 方法保持不变,数据类生成的其它方法,如 __eq__()__repr__() 等,也保持不变。

使用声明式与命令式表映射预先存在的 dataclasses

以下示例使用 @dataclass 进行映射,并使用 声明式与命令式表(又称混合声明式)。完整 Table 对象被显式地构造并赋值给 __table__ 属性。实例字段使用正常的数据类语法定义。额外的 MapperProperty 定义,如 relationship(),放置在 __mapper_args__ 类级字典中,位于 properties 键下,对应于 Mapper.properties 参数。

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List, Optional

from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import registry, relationship

mapper_registry = registry()


@mapper_registry.mapped
@dataclass
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("fullname", String(50)),
        Column("nickname", String(12)),
    )
    id: int = field(init=False)
    name: Optional[str] = None
    fullname: Optional[str] = None
    nickname: Optional[str] = None
    addresses: List[Address] = field(default_factory=list)

    __mapper_args__ = {  # type: ignore
        "properties": {
            "addresses": relationship("Address"),
        }
    }


@mapper_registry.mapped
@dataclass
class Address:
    __table__ = Table(
        "address",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", Integer, ForeignKey("user.id")),
        Column("email_address", String(50)),
    )
    id: int = field(init=False)
    user_id: int = field(init=False)
    email_address: Optional[str] = None

在上面的示例中,User.idAddress.idAddress.user_id 属性被定义为 field(init=False)。这意味着这些属性的参数不会添加到 __init__() 方法中,但 Session 仍然能够在从自动递增或其他默认值生成器获取值后设置它们。为了允许在构造函数中显式指定它们,它们将被赋予 None 的默认值。

为了单独声明一个 relationship(),需要直接在 Mapper.properties 字典中指定它,该字典本身在 __mapper_args__ 字典中指定,以便它传递给 Mapper 的构造函数。该方法的另一种方式将在下一个示例中介绍。

警告

在数据类 field() 中声明设置 default 以及 init=False 的方式不会像完全普通的数据类那样生效,因为 SQLAlchemy 类检测会用数据类创建过程设置的默认值替换类上的默认值。请改为使用 default_factory。在使用 声明式数据类映射 时,此调整会自动完成。

使用声明式样式字段映射预先存在的 dataclasses

传统功能

这种使用数据类的声明式映射方法应被视为遗留方法。它将继续得到支持,但不太可能比 声明式数据类映射 中介绍的新方法更具优势。

请注意,**mapped_column() 不支持这种方式**;Column 结构应继续用于在 dataclasses.field()metadata 字段中声明表元数据。

完全声明式的方法要求将 Column 对象声明为类属性,这在使用数据类时会与数据类级属性冲突。将它们组合在一起的一种方法是使用 dataclass.field 对象上的 metadata 属性,可以在其中提供特定于 SQLAlchemy 的映射信息。声明式支持在类指定 __sa_dataclass_metadata_key__ 属性时提取这些参数。这也提供了一种更简洁的方法来指示 relationship() 关联。

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import registry, relationship

mapper_registry = registry()


@mapper_registry.mapped
@dataclass
class User:
    __tablename__ = "user"

    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    name: str = field(default=None, metadata={"sa": Column(String(50))})
    fullname: str = field(default=None, metadata={"sa": Column(String(50))})
    nickname: str = field(default=None, metadata={"sa": Column(String(12))})
    addresses: List[Address] = field(
        default_factory=list, metadata={"sa": relationship("Address")}
    )


@mapper_registry.mapped
@dataclass
class Address:
    __tablename__ = "address"
    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    user_id: int = field(init=False, metadata={"sa": Column(ForeignKey("user.id"))})
    email_address: str = field(default=None, metadata={"sa": Column(String(50))})

在预先存在的 dataclasses 中使用声明式 Mixin

使用 Mixin 组合映射层次结构 部分中,介绍了声明式 Mixin 类。声明式 Mixin 的一项要求是,必须使用 declared_attr 装饰器以可调用对象的形式提供某些无法轻松复制的结构,例如 Mixin 中的关系 中的示例。

class RefTargetMixin:
    @declared_attr
    def target_id(cls) -> Mapped[int]:
        return mapped_column("target_id", ForeignKey("target.id"))

    @declared_attr
    def target(cls):
        return relationship("Target")

这种形式在数据类 field() 对象中得到支持,通过使用 lambda 在 field() 中指示 SQLAlchemy 结构。使用 declared_attr() 包裹 lambda 是可选的。如果我们想生成上面的 User 类,其中 ORM 字段来自一个本身是数据类的 Mixin,其形式将是

@dataclass
class UserMixin:
    __tablename__ = "user"

    __sa_dataclass_metadata_key__ = "sa"

    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})

    addresses: List[Address] = field(
        default_factory=list, metadata={"sa": lambda: relationship("Address")}
    )


@dataclass
class AddressMixin:
    __tablename__ = "address"
    __sa_dataclass_metadata_key__ = "sa"
    id: int = field(init=False, metadata={"sa": Column(Integer, primary_key=True)})
    user_id: int = field(
        init=False, metadata={"sa": lambda: Column(ForeignKey("user.id"))}
    )
    email_address: str = field(default=None, metadata={"sa": Column(String(50))})


@mapper_registry.mapped
class User(UserMixin):
    pass


@mapper_registry.mapped
class Address(AddressMixin):
    pass

新版 1.4.2 中新增:添加了对“声明式属性”风格的 Mixin 属性的支持,即 relationship() 结构以及带有外键声明的 Column 对象,用于在“使用声明式表的 dataclasses”风格的映射中使用。

使用命令式映射映射预先存在的 dataclasses

如前所述,使用 @dataclass 装饰器设置为数据类的类可以使用 registry.mapped() 装饰器进一步装饰,以便将声明式风格的映射应用于该类。作为使用 registry.mapped() 装饰器的替代方案,我们也可以将该类传递给 registry.map_imperatively() 方法,以便我们可以将所有 TableMapper 配置以命令式方式传递给函数,而不是将它们作为类变量定义在类本身中。

from __future__ import annotations

from dataclasses import dataclass
from dataclasses import field
from typing import List

from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@dataclass
class User:
    id: int = field(init=False)
    name: str = None
    fullname: str = None
    nickname: str = None
    addresses: List[Address] = field(default_factory=list)


@dataclass
class Address:
    id: int = field(init=False)
    user_id: int = field(init=False)
    email_address: str = None


metadata_obj = MetaData()

user = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

address = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String(50)),
)

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id),
    },
)

mapper_registry.map_imperatively(Address, address)

使用这种映射风格时,适用于 使用声明式与命令式表映射预先存在的 dataclasses 中提到的相同警告。

将 ORM 映射应用于现有的 attrs 类

attrs 库是一个流行的第三方库,它提供了与数据类类似的功能,并且提供了普通数据类中未提供的许多附加功能。

使用 attrs 增强过的类使用 @define 装饰器。该装饰器启动一个过程来扫描类以查找定义类行为的属性,然后使用这些属性生成方法、文档和注释。

SQLAlchemy ORM 支持使用 **声明式与命令式表** 或 **命令式** 映射来映射 attrs 类。这两种风格的通用形式与使用数据类的 使用声明式样式字段映射预先存在的 dataclasses使用声明式与命令式表映射预先存在的 dataclasses 映射形式完全相同,其中数据类或 attrs 使用的内联属性指令保持不变,并且 SQLAlchemy 的面向表的检测在运行时应用。

attrs@define 装饰器默认情况下会用新的基于 __slots__ 的类替换注释过的类,这不受支持。使用旧式注释 @attr.s 或使用 define(slots=False) 时,类不会被替换。此外,attrs 在装饰器运行后会移除自身绑定的类属性,以便 SQLAlchemy 的映射过程在没有任何问题的情况下接管这些属性。这两个装饰器 @attr.s@define(slots=False) 都适用于 SQLAlchemy。

使用声明式“命令式表”映射 attrs

在“声明式与命令式表格”风格中,Table 对象是在声明式类内联声明的。@define 装饰器首先应用于类,然后是 registry.mapped() 装饰器。

from __future__ import annotations

from typing import List
from typing import Optional

from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@mapper_registry.mapped
@define(slots=False)
class User:
    __table__ = Table(
        "user",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(50)),
        Column("FullName", String(50), key="fullname"),
        Column("nickname", String(12)),
    )
    id: Mapped[int]
    name: Mapped[str]
    fullname: Mapped[str]
    nickname: Mapped[str]
    addresses: Mapped[List[Address]]

    __mapper_args__ = {  # type: ignore
        "properties": {
            "addresses": relationship("Address"),
        }
    }


@mapper_registry.mapped
@define(slots=False)
class Address:
    __table__ = Table(
        "address",
        mapper_registry.metadata,
        Column("id", Integer, primary_key=True),
        Column("user_id", Integer, ForeignKey("user.id")),
        Column("email_address", String(50)),
    )
    id: Mapped[int]
    user_id: Mapped[int]
    email_address: Mapped[Optional[str]]

注意

attrs slots=True 选项,它在映射类上启用 __slots__,不能与 SQLAlchemy 映射一起使用,除非完全实现替代的 属性检测,因为映射类通常依赖于直接访问 __dict__ 用于状态存储。当此选项存在时,行为未定义。

使用命令式映射映射属性

就像使用数据类一样,我们可以使用 registry.map_imperatively() 来映射现有的 attrs 类。

from __future__ import annotations

from typing import List

from attrs import define
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import MetaData
from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship

mapper_registry = registry()


@define(slots=False)
class User:
    id: int
    name: str
    fullname: str
    nickname: str
    addresses: List[Address]


@define(slots=False)
class Address:
    id: int
    user_id: int
    email_address: Optional[str]


metadata_obj = MetaData()

user = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)

address = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String(50)),
)

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id),
    },
)

mapper_registry.map_imperatively(Address, address)

以上形式等同于之前使用声明式与命令式表格的示例。