与dataclass和attrs的集成

Integration with dataclasses and attrs

SQLAlchemy 自 2.0 版本起,支持“原生数据类”集成,通过在映射类中添加一个混入或装饰器,可以将 Annotated Declarative Table 映射转换为 Python 数据类_

在 2.0 版本加入: 与 ORM 声明类集成的数据类创建

还有一些模式允许映射现有的数据类,以及映射由 attrs 第三方集成库注入的类。

SQLAlchemy as of version 2.0 features “native dataclass” integration where an Annotated Declarative Table mapping may be turned into a Python dataclass by adding a single mixin or decorator to mapped classes.

在 2.0 版本加入: Integrated dataclass creation with ORM Declarative classes

There are also patterns available that allow existing dataclasses to be mapped, as well as to map classes instrumented by the attrs third party integration library.

声明式Dataclass映射

Declarative Dataclass Mapping

SQLAlchemy Annotated Declarative Table 映射可以通过额外的混入类或装饰器指令进行增强,这将在映射完成后为声明式过程添加一个额外步骤,该步骤将在完成映射过程之前将映射类 就地(in-place) 转换为 Python 数据类_,然后应用 ORM 特定的 instrumentation 到类中。这种增强提供的最显著的行为是生成一个具有位置参数和关键字参数的细粒度控制的 __init__() 方法,无论是否有默认值,以及生成像 __repr__()__eq__() 这样的方法。

PEP 484 类型注释的角度来看,该类被认为具有数据类特定的行为,最显著的是利用 PEP 681 “数据类转换”,这允许类型工具将该类视为使用 @dataclasses.dataclass 装饰器显式装饰的类。

备注

截至 2023年4月4日,类型工具对 PEP 681 的支持有限,目前已知 Pyright 以及 Mypy1.2 版本 开始支持。请注意,Mypy 1.1.1 引入了 PEP 681 支持,但没有正确适应 Python 描述符,这会在使用 SQLAlchemy 的 ORM 映射方案时导致错误。

参见

https://peps.python.org/pep-0681/#the-dataclass-transform-decorator - 有关像 SQLAlchemy 这样的库如何实现 PEP 681 支持的背景信息

数据类转换可以通过添加 MappedAsDataclass 混入到 DeclarativeBase 类层次结构中,或通过使用 registry.mapped_as_dataclass() 类装饰器进行装饰映射。

MappedAsDataclass 混入可以应用于声明式 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):
    """子类将转换为数据类"""


class User(Base):
    __tablename__ = "user_account"

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

也可以直接应用于从声明式基类扩展的类:

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 类将转换为数据类"""

    __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]

SQLAlchemy Annotated Declarative Table mappings may be augmented with an additional mixin class or decorator directive, which will add an additional step to the Declarative process after the mapping is complete that will convert the mapped class in-place into a Python dataclass, before completing the mapping process which applies ORM-specific instrumentation to the class. The most prominent behavioral addition this provides is generation of an __init__() method with fine-grained control over positional and keyword arguments with or without defaults, as well as generation of methods like __repr__() and __eq__().

From a PEP 484 typing perspective, the class is recognized as having Dataclass-specific behaviors, most notably by taking advantage of PEP 681 “Dataclass Transforms”, which allows typing tools to consider the class as though it were explicitly decorated using the @dataclasses.dataclass decorator.

备注

Support for PEP 681 in typing tools as of April 4, 2023 is

limited and is currently known to be supported by Pyright as well as Mypy as of version 1.2. Note that Mypy 1.1.1 introduced PEP 681 support but did not correctly accommodate Python descriptors which will lead to errors when using SQLAlchemy’s ORM mapping scheme.

参见

https://peps.python.org/pep-0681/#the-dataclass-transform-decorator - background on how libraries like SQLAlchemy enable PEP 681 support

Dataclass conversion may be added to any Declarative class either by adding the MappedAsDataclass mixin to a DeclarativeBase class hierarchy, or for decorator mapping by using the registry.mapped_as_dataclass() class decorator.

The MappedAsDataclass mixin may be applied either to the Declarative Base class or any superclass, as in the example below:

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]

Or may be applied directly to classes that extend from the 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(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]

When using the decorator form, only the registry.mapped_as_dataclass() decorator is supported:

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]

类级功能配置

Class level feature configuration

对数据类功能的支持是部分的。目前 支持 的功能包括 initrepreqorderunsafe_hash,在 Python 3.10+ 上还支持 match_argskw_only。当前 不支持 的功能包括 frozenslots

当使用 MappedAsDataclass 混入类形式时,类配置参数作为类级参数传递:

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 类将转换为数据类"""

    __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 类将转换为数据类"""

    __tablename__ = "user_account"

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

有关数据类类选项的背景信息,请参阅 dataclasses 文档 在 @dataclasses.dataclass

Support for dataclasses features is partial. Currently supported are the init, repr, eq, order and unsafe_hash features, match_args and kw_only are supported on Python 3.10+. Currently not supported are the frozen and slots features.

When using the mixin class form with MappedAsDataclass, class configuration arguments are passed as class-level parameters:

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]

When using the decorator form with registry.mapped_as_dataclass(), class configuration arguments are passed to the decorator directly:

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]

For background on dataclass class options, see the dataclasses documentation at @dataclasses.dataclass.

属性配置

Attribute Configuration

SQLAlchemy 原生数据类与普通数据类不同,映射的属性在所有情况下都使用 Mapped 泛型注解容器描述。映射遵循 带有 mapped_column() 的声明表 中记录的相同形式,并且支持 mapped_column()Mapped 的所有功能。

此外,ORM 属性配置结构包括 mapped_column()relationship()composite() 支持 每个属性字段选项 ,包括 initdefaultdefault_factoryrepr。这些参数的名称如 PEP 681 中规定的那样是固定的。功能等同于数据类:

另一个与数据类的关键区别是属性的默认值 必须 使用 ORM 结构的 default 参数配置,例如 mapped_column(default=None)。不支持类似数据类语法的接受简单 Python 值作为默认值的语法,而无需使用 @dataclasses.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' 是可选的关键字参数
u1 = User("name")

SQLAlchemy native dataclasses differ from normal dataclasses in that attributes to be mapped are described using the Mapped generic annotation container in all cases. Mappings follow the same forms as those documented at 带有 mapped_column() 的声明表, and all features of mapped_column() and Mapped are supported.

Additionally, ORM attribute configuration constructs including mapped_column(), relationship() and composite() support per-attribute field options, including init, default, default_factory and repr. The names of these arguments is fixed as specified in PEP 681. Functionality is equivalent to dataclasses:

relationship.init, if False indicates the field should not be part of the __init__() method * default, as in mapped_column.default, relationship.default indicates a default value for the field as given as a keyword argument in the __init__() method. * default_factory, as in mapped_column.default_factory, relationship.default_factory, indicates a callable function that will be invoked to generate a new default value for a parameter if not passed explicitly to the __init__() method. * repr True by default, indicates the field should be part of the generated __repr__() method

Another key difference from dataclasses is that default values for attributes must be configured using the default parameter of the ORM construct, such as mapped_column(default=None). A syntax that resembles dataclass syntax which accepts simple Python values as defaults without using @dataclases.field() is not supported.

As an example using mapped_column(), the mapping below will produce an __init__() method that accepts only the fields name and fullname, where name is required and may be passed positionally, and fullname is optional. The id field, which we expect to be database-generated, is not part of the constructor at all:

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")

列默认值

Column Defaults

为了适应 default 参数与 Column 结构的现有 Column.default 参数的名称重叠,mapped_column() 结构通过添加一个新参数 mapped_column.insert_default 来消除两者的歧义,该参数将直接填充到 ColumnColumn.default 参数中,而不管在 mapped_column.default 上设置了什么值,mapped_column.default 始终用于数据类配置。例如,配置一个 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 对象时没有传递 created_at 参数,INSERT 将按以下方式进行:

>>> 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

In order to accommodate the name overlap of the default argument with the existing Column.default parameter of the Column construct, the mapped_column() construct disambiguates the two names by adding a new parameter mapped_column.insert_default, which will be populated directly into the Column.default parameter of Column, independently of what may be set on mapped_column.default, which is always used for the dataclasses configuration. For example, to configure a datetime column with a Column.default set to the func.utc_timestamp() SQL function, but where the parameter is optional in the constructor:

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
    )

With the above mapping, an INSERT for a new User object where no parameter for created_at were passed proceeds as:

>>> 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

与注解集成

Integration with Annotated

将整个列声明映射到 Python 类型 中介绍的方法展示了如何使用 PEP 593 Annotated 对象来打包整个 mapped_column() 结构以便重用。虽然 Annotated 对象可以与数据类一起使用,但 不幸的是,数据类特定的关键字参数不能在 Annotated 结构中使用 。这些包括 PEP 681 特定的参数 initdefaultreprdefault_factory,它们 必须 出现在与类属性内联的 mapped_column() 或类似结构中。

在 2.0.14/2.0.22 版本发生变更: Annotated 结构与像 mapped_column() 这样的 ORM 结构一起使用时,不能容纳数据类字段参数如 initrepr - 这种用法违反了 Python 数据类的设计,并且不受 PEP 681 支持,因此在运行时也被 SQLAlchemy ORM 拒绝。现在发出弃用警告,并且该属性将被忽略。

例如,下面的 init=False 参数将被忽略并另外发出弃用警告:

from typing import Annotated

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

# 类型工具以及 SQLAlchemy 将忽略此处的 init=False
intpk = Annotated[int, mapped_column(init=False, primary_key=True)]

reg = registry()


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


# 类型错误以及运行时错误:缺少参数 "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 和其他 pep-681 参数必须内联
    id: Mapped[intpk] = mapped_column(init=False)


u1 = User()

The approach introduced at 将整个列声明映射到 Python 类型 illustrates how to use PEP 593 Annotated objects to package whole mapped_column() constructs for re-use. While Annotated objects can be combined with the use of dataclasses, dataclass-specific keyword arguments unfortunately cannot be used within the Annotated construct. This includes PEP 681-specific arguments init, default, repr, and default_factory, which must be present in a mapped_column() or similar construct inline with the class attribute.

在 2.0.14/2.0.22 版本发生变更: the Annotated construct when used with an ORM construct like mapped_column() cannot accommodate dataclass field parameters such as init and repr - this use goes against the design of Python dataclasses and is not supported by PEP 681, and therefore is also rejected by the SQLAlchemy ORM at runtime. A deprecation warning is now emitted and the attribute will be ignored.

As an example, the init=False parameter below will be ignored and additionally emit a deprecation warning:

from typing import Annotated

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

# typing tools as well as SQLAlchemy 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 as well as runtime error: Argument missing for parameter "id"
u1 = User()

Instead, mapped_column() must be present on the right side as well with an explicit setting for mapped_column.init; the other arguments can remain within the Annotated construct:

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()

使用混合和抽象超类

Using mixins and abstract superclasses

任何在 MappedAsDataclass 映射类中使用的混入或基类,如果包含 Mapped 属性,则它们本身必须是 MappedAsDataclass 层次结构的一部分,例如在下面的示例中使用混入:

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 类型检查器否则不会将非数据类混入的属性视为数据类的一部分。

自 2.0.8 版本弃用: MappedAsDataclassregistry.mapped_as_dataclass() 层次结构中使用混入和抽象基类,如果它们本身不是数据类,则已弃用,因为这些字段不被 PEP 681 视为属于数据类。在这种情况下会发出警告,后来将成为错误。

Any mixins or base classes that are used in a MappedAsDataclass mapped class which include Mapped attributes must themselves be part of a MappedAsDataclass hierarchy, such as in the example below using a 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()

Python type checkers which support PEP 681 will otherwise not consider attributes from non-dataclass mixins to be part of the dataclass.

自 2.0.8 版本弃用: Using mixins and abstract bases within MappedAsDataclass or registry.mapped_as_dataclass() hierarchies which are not themselves dataclasses is deprecated, as these fields are not supported by PEP 681 as belonging to the dataclass. A warning is emitted for this case which will later be an error.

关系配置

Relationship Configuration

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

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)

上述映射将在没有传递 children 的情况下构造新的 Parent() 对象时为 Parent.children 生成一个空列表,并且在没有传递 parent 的情况下构造新的 Child() 对象时为 Child.parent 生成一个 None 值。

虽然 relationship() 本身的给定集合类可以自动派生 relationship.default_factory,但这会破坏与数据类的兼容性,因为 relationship.default_factoryrelationship.default 的存在决定了参数在渲染到 __init__() 方法时是否必须或可选。

The Mapped annotation in combination with relationship() is used in the same way as described at 基本关系模式. When specifying a collection-based relationship() as an optional keyword argument, the relationship.default_factory parameter must be passed and it must refer to the collection class that’s to be used. Many-to-one and scalar object references may make use of relationship.default if the default value is to be 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)

The above mapping will generate an empty list for Parent.children when a new Parent() object is constructed without passing children, and similarly a None value for Child.parent when a new Child() object is constructed without passing parent.

While the relationship.default_factory can be automatically derived from the given collection class of the relationship() itself, this would break compatibility with dataclasses, as the presence of relationship.default_factory or relationship.default is what determines if the parameter is to be required or optional when rendered into the __init__() method.

使用非映射数据类字段

Using Non-Mapped Dataclass Fields

在使用声明式数据类时,非映射字段也可以在类中使用,它们将成为数据类构造过程的一部分,但不会被映射。任何不使用 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")

一个更现实的例子可能是结合 __post_init__() 功能使用 Dataclasses 的 InitVar 功能来接收仅初始化字段,这些字段可用于组合持久化数据。如下例所示, 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 注解的字段,这些字段将被视为生成的数据类的一部分,但不会被映射,而无需同时指示 __allow_unmapped__ 类属性。以前的 2.0 测试版需要显式存在此属性,尽管此属性的目的是仅允许旧版 ORM 类型映射继续运行。

When using Declarative dataclasses, non-mapped fields may be used on the class as well, which will be part of the dataclass construction process but will not be mapped. Any field that does not use Mapped will be ignored by the mapping process. In the example below, the fields ctrl_one and ctrl_two will be part of the instance-level state of the object, but will not be persisted by the 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

Instance of Data above can be created as:

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

A more real world example might be to make use of the Dataclasses InitVar feature in conjunction with the __post_init__() feature to receive init-only fields that can be used to compose persisted data. In the example below, the User class is declared using id, name and password_hash as mapped features, but makes use of init-only password and repeat_password fields to represent the user creation process (note: to run this example, replace the function your_crypt_function_here() with a third party crypt function, such as bcrypt or argon2-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)

The above object is created with parameters password and repeat_password, which are consumed up front so that the password_hash variable may be generated:

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

在 2.0.0rc1 版本发生变更: When using registry.mapped_as_dataclass() or MappedAsDataclass, fields that do not include the Mapped annotation may be included, which will be treated as part of the resulting dataclass but not be mapped, without the need to also indicate the __allow_unmapped__ class attribute. Previous 2.0 beta releases would require this attribute to be explicitly present, even though the purpose of this attribute was only to allow legacy ORM typed mappings to continue to function.

与 Pydantic 等备用数据类提供程序集成

Integrating with Alternate Dataclass Providers such as Pydantic

警告

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

对于 Pydantic 兼容性,请考虑 SQLModel ORM,它是基于 SQLAlchemy ORM 构建的 Pydantic,包含专门的实现细节, 明确解决 了这些不兼容性。

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

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 类将被应用为数据类,使用 Pydantic 的 pydantic.dataclasses.dataclasses 可调用对象。该过程适用于映射类以及从 MappedAsDataclass 扩展的混入类或直接应用了 registry.mapped_as_dataclass() 的类。

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

警告

The dataclass layer of Pydantic is not fully compatible with SQLAlchemy’s class instrumentation without additional internal changes, and many features such as related collections may not work correctly.

For Pydantic compatibility, please consider the SQLModel ORM which is built with Pydantic on top of SQLAlchemy ORM, which includes special implementation details which explicitly resolve these incompatibilities.

SQLAlchemy’s MappedAsDataclass class and registry.mapped_as_dataclass() method call directly into the Python standard library dataclasses.dataclass class decorator, after the declarative mapping process has been applied to the class. This function call may be swapped out for alternateive dataclasses providers, such as that of Pydantic, using the dataclass_callable parameter accepted by MappedAsDataclass as a class keyword argument as well as by 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]

The above User class will be applied as a dataclass, using Pydantic’s pydantic.dataclasses.dataclasses callable. The process is available both for mapped classes as well as mixins that extend from MappedAsDataclass or which have registry.mapped_as_dataclass() applied directly.

在 2.0.4 版本加入: Added the dataclass_callable class and method parameters for MappedAsDataclass and registry.mapped_as_dataclass(), and adjusted some of the dataclass internals to accommodate more strict dataclass functions such as that of Pydantic.

将 ORM 映射应用于现有数据类(旧式数据类使用)

Applying ORM Mappings to an existing dataclass (legacy dataclass use)

此处描述的方法已被 2.0 系列 SQLAlchemy 中的新特性 声明式Dataclass映射 取代。此新版本功能建立在 1.4 版本中首次添加的数据类支持之上,本节描述了该支持。

要映射现有的数据类,SQLAlchemy 的“内联”声明性指令不能直接使用;ORM 指令使用以下三种技术之一进行分配:

SQLAlchemy 将映射应用于数据类的总体过程与普通类相同,但还包括 SQLAlchemy 将检测在数据类声明过程中作为类级属性的属性,并在运行时将其替换为通常的 SQLAlchemy ORM 映射属性。dataclasses 生成的 __init__ 方法保持不变,dataclasses 生成的所有其他方法如 __eq__()__repr__() 等也是如此。

Legacy Feature

The approaches described here are superseded by the 声明式Dataclass映射 feature new in the 2.0 series of SQLAlchemy. This newer version of the feature builds upon the dataclass support first added in version 1.4, which is described in this section.

To map an existing dataclass, SQLAlchemy’s “inline” declarative directives cannot be used directly; ORM directives are assigned using one of three techniques:

The general process by which SQLAlchemy applies mappings to a dataclass is the same as that of an ordinary class, but also includes that SQLAlchemy will detect class-level attributes that were part of the dataclasses declaration process and replace them at runtime with the usual SQLAlchemy ORM mapped attributes. The __init__ method that would have been generated by dataclasses is left intact, as is the same for all the other methods that dataclasses generates such as __eq__(), __repr__(), etc.

使用声明式和命令式表映射预先存在的数据类

Mapping pre-existing dataclasses using Declarative With Imperative Table

下面是一个使用 @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() 设置 defaultinit=False 一起使用时,将不会如纯数据类那样工作,因为 SQLAlchemy 类检测将替换数据类创建过程在类上设置的默认值。请改用 default_factory。在使用 声明式Dataclass映射 时会自动进行此调整。

An example of a mapping using @dataclass using 使用命令式表的声明式(又名混合声明式) is below. A complete Table object is constructed explicitly and assigned to the __table__ attribute. Instance fields are defined using normal dataclass syntaxes. Additional MapperProperty definitions such as relationship(), are placed in the __mapper_args__ class-level dictionary underneath the properties key, corresponding to the Mapper.properties parameter:

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

In the above example, the User.id, Address.id, and Address.user_id attributes are defined as field(init=False). This means that parameters for these won’t be added to __init__() methods, but Session will still be able to set them after getting their values during flush from autoincrement or other default value generator. To allow them to be specified in the constructor explicitly, they would instead be given a default value of None.

For a relationship() to be declared separately, it needs to be specified directly within the Mapper.properties dictionary which itself is specified within the __mapper_args__ dictionary, so that it is passed to the constructor for Mapper. An alternative to this approach is in the next example.

警告

Declaring a dataclass field() setting a default together with init=False will not work as would be expected with a totally plain dataclass, since the SQLAlchemy class instrumentation will replace the default value set on the class by the dataclass creation process. Use default_factory instead. This adaptation is done automatically when making use of 声明式Dataclass映射.

使用声明式样式字段映射预先存在的数据类

Mapping pre-existing dataclasses using Declarative-style fields

Legacy Feature

此声明数据类映射方法应被视为遗留方法。它将继续受支持,但与 声明式Dataclass映射 中详细介绍的新方法相比,不太可能提供任何优势。

请注意, 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))})

Legacy Feature

This approach to Declarative mapping with dataclasses should be considered as legacy. It will remain supported however is unlikely to offer any advantages against the new approach detailed at 声明式Dataclass映射.

Note that mapped_column() is not supported with this use; the Column construct should continue to be used to declare table metadata within the metadata field of dataclasses.field().

The fully declarative approach requires that Column objects are declared as class attributes, which when using dataclasses would conflict with the dataclass-level attributes. An approach to combine these together is to make use of the metadata attribute on the dataclass.field object, where SQLAlchemy-specific mapping information may be supplied. Declarative supports extraction of these parameters when the class specifies the attribute __sa_dataclass_metadata_key__. This also provides a more succinct method of indicating the relationship() association:

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))})

将声明式混合与预先存在的数据类结合使用

Using Declarative Mixins with pre-existing dataclasses

使用 Mixins 组合映射层次结构 部分中,引入了声明性 Mixin 类。声明性 mixin 的一个要求是,某些无法轻松复制的结构必须使用 declared_attr 装饰器以可调用方式提供,例如在 混合关系 示例中:

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")

在 Dataclasses field() 对象中,通过使用 lambda 表示 field() 内的 SQLAlchemy 结构来支持此形式。使用 declared_attr() 包围 lambda 是可选的。如果我们想生成上述 ORM 字段来自 mixin 且 mixin 本身为数据类的 User 类,形式如下:

@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 版本加入: 添加对“声明属性(declared attr)”风格的 mixin 属性的支持,即 relationship() 结构以及带有外键声明的 Column 对象,以用于“带声明表的数据类(Dataclasses with Declarative Table)”样式映射中。

In the section 使用 Mixins 组合映射层次结构, Declarative Mixin classes are introduced. One requirement of declarative mixins is that certain constructs that can’t be easily duplicated must be given as callables, using the declared_attr decorator, such as in the example at 混合关系:

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")

This form is supported within the Dataclasses field() object by using a lambda to indicate the SQLAlchemy construct inside the field(). Using declared_attr() to surround the lambda is optional. If we wanted to produce our User class above where the ORM fields came from a mixin that is itself a dataclass, the form would be:

@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 版本加入: Added support for “declared attr” style mixin attributes, namely relationship() constructs as well as Column objects with foreign key declarations, to be used within “Dataclasses with Declarative Table” style mappings.

使用命令式映射映射预先存在的数据类

Mapping pre-existing dataclasses using Imperative Mapping

如前所述,使用 @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)

与使用此映射样式时,使用声明式和命令式表映射预先存在的数据类 中提到的相同警告适用。

As described previously, a class which is set up as a dataclass using the @dataclass decorator can then be further decorated using the registry.mapped() decorator in order to apply declarative-style mapping to the class. As an alternative to using the registry.mapped() decorator, we may also pass the class through the registry.map_imperatively() method instead, so that we may pass all Table and Mapper configuration imperatively to the function rather than having them defined on the class itself as class variables:

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)

The same warning mentioned in 使用声明式和命令式表映射预先存在的数据类 applies when using this mapping style.

将 ORM 映射应用于现有属性类

Applying ORM mappings to an existing attrs class

警告

attrs 库不是 SQLAlchemy 的持续集成测试的一部分,由于任何一方引入的不兼容性,可能会在没有通知的情况下更改与该库的兼容性。

attrs 库是一个流行的第三方库,提供与数据类相似的功能,并提供许多普通数据类中没有的额外功能。

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

SQLAlchemy ORM 支持使用 命令式 映射映射 attrs 类。这种风格的通用形式等同于使用数据类的 使用命令式映射映射预先存在的数据类 映射形式,其中类构造仅使用 attrs,ORM 映射在类构造之后应用,而不进行任何类属性扫描。

attrs@define 装饰器默认情况下会用一个新的基于 __slots__ 的类替换注解类,这是不支持的。使用旧样式注解 @attr.s 或使用 define(slots=False) 时,类不会被替换。此外, attrs 在装饰器运行后会删除其自己的类绑定属性,以便 SQLAlchemy 的映射过程可以接管这些属性而不会出现任何问题。这两个装饰器 @attr.s@define(slots=False) 均适用于 SQLAlchemy。

在 2.0 版本发生变更: SQLAlchemy 与 attrs 的集成仅适用于命令式映射风格,即不使用声明性。引入的 ORM 注解声明风格与 attrs 不兼容。

首先构建 attrs 类。SQLAlchemy ORM 映射可以在之后应用,使用 registry.map_imperatively():

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)

警告

The attrs library is not part of SQLAlchemy’s continuous integration testing, and compatibility with this library may change without notice due to incompatibilities introduced by either side.

The attrs library is a popular third party library that provides similar features as dataclasses, with many additional features provided not found in ordinary dataclasses.

A class augmented with attrs uses the @define decorator. This decorator initiates a process to scan the class for attributes that define the class’ behavior, which are then used to generate methods, documentation, and annotations.

The SQLAlchemy ORM supports mapping an attrs class using Imperative mapping. The general form of this style is equivalent to the 使用命令式映射映射预先存在的数据类 mapping form used with dataclasses, where the class construction uses attrs alone, with ORM mappings applied after the fact without any class attribute scanning.

The @define decorator of attrs by default replaces the annotated class with a new __slots__ based class, which is not supported. When using the old style annotation @attr.s or using define(slots=False), the class does not get replaced. Furthermore attrs removes its own class-bound attributes after the decorator runs, so that SQLAlchemy’s mapping process takes over these attributes without any issue. Both decorators, @attr.s and @define(slots=False) work with SQLAlchemy.

在 2.0 版本发生变更: SQLAlchemy integration with attrs works only with imperative mapping style, that is, not using Declarative. The introduction of ORM Annotated Declarative style is not cross-compatible with attrs.

The attrs class is built first. The SQLAlchemy ORM mapping can be applied after the fact using registry.map_imperatively():

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)