SqlAlchemy-2-0-中文文档-四-

SqlAlchemy 2.0 中文文档(四)

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

使用混合类组合映射层次结构

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

在使用 Declarative 风格映射类时,常见的需求是共享常见功能,例如特定列、表或映射器选项、命名方案或其他映射属性,跨多个类。在使用声明性映射时,可以通过使用 mixin 类,以及通过扩展声明性基类本身来支持此习惯用法。

提示

除了 mixin 类之外,还可以使用PEP 593 Annotated 类型共享许多类的常见列选项;请参阅将多种类型配置映射到 Python 类型和将整个列声明映射到 Python 类型以获取有关这些 SQLAlchemy 2.0 功能的背景信息。

以下是一些常见的混合用法示例:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
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 CommonMixin:
  """define a series of common elements that may be applied to mapped
 classes using this class as a mixin class."""

    @declared_attr.directive
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

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

class HasLogRecord:
  """mark classes that have a many-to-one relationship to the
 ``LogRecord`` class."""

    log_record_id: Mapped[int] = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self) -> Mapped["LogRecord"]:
        return relationship("LogRecord")

class LogRecord(CommonMixin, Base):
    log_info: Mapped[str]

class MyModel(CommonMixin, HasLogRecord, Base):
    name: Mapped[str]

上述示例说明了一个类MyModel,它包括两个混合类CommonMixinHasLogRecord,以及一个补充类LogRecord,该类也包括CommonMixin,演示了在混合类和基类上支持的各种构造,包括:

  • 使用mapped_column()MappedColumn声明的列将从混合类或基类复制到要映射的目标类;上面通过列属性CommonMixin.idHasLogRecord.log_record_id说明了这一点。

  • 可以将声明性指令(如__table_args____mapper_args__)分配给混合类或基类,在继承混合类或基类的任何类中,这些指令将自动生效。上述示例使用__table_args____mapper_args__属性说明了这一点。

  • 所有的 Declarative 指令,包括 __tablename____table____table_args____mapper_args__,都可以使用用户定义的类方法来实现,这些方法使用了 declared_attr 装饰器进行修饰(具体地说是 declared_attr.directive 子成员,稍后会详细介绍)。上面的示例使用了一个 def __tablename__(cls) 类方法动态生成一个 Table 名称;当应用到 MyModel 类时,表名将生成为 "mymodel",而当应用到 LogRecord 类时,表名将生成为 "logrecord"

  • 其他 ORM 属性,如 relationship(),也可以通过在目标类上生成的用户定义的类方法来生成,并且这些类方法也使用了 declared_attr 装饰器进行修饰。上面的例子演示了通过生成一个多对一的 relationship() 到一个名为 LogRecord 的映射对象来实现此功能。

上述功能都可以使用 select() 示例进行演示:

>>> from sqlalchemy import select
>>> print(select(MyModel).join(MyModel.log_record))
SELECT  mymodel.name,  mymodel.id,  mymodel.log_record_id
FROM  mymodel  JOIN  logrecord  ON  logrecord.id  =  mymodel.log_record_id 

提示

declared_attr 的示例将尝试说明每个方法示例的正确的 PEP 484 注解。使用 declared_attr 函数的注解是完全可选的,并且不会被 Declarative 消耗;然而,为了通过 Mypy 的 --strict 类型检查,这些注解是必需的。

另外,上面所示的 declared_attr.directive 子成员也是可选的,它只对 PEP 484 类型工具有意义,因为它调整了创建用于重写 Declarative 指令的方法时的期望返回类型,例如 __tablename____mapper_args____table_args__

新版本 2.0 中新增:作为 SQLAlchemy ORM 的 PEP 484 类型支持的一部分,添加了 declared_attr.directive 来将 declared_attr 区分为 Mapped 属性和声明性配置属性之间的区别。

混合类和基类的顺序没有固定的约定。普通的 Python 方法解析规则适用,上述示例也同样适用:

class MyModel(Base, HasLogRecord, CommonMixin):
    name: Mapped[str] = mapped_column()

这是因为这里的 Base 没有定义 CommonMixinHasLogRecord 定义的任何变量,即 __tablename____table_args__id 等。如果 Base 定义了同名属性,则位于继承列表中的第一个类将决定在新定义的类上使用哪个属性。

提示

虽然上述示例使用了基于 Mapped 注解类的注释声明表形式,但混合类与非注释和遗留声明形式也完全兼容,比如直接使用 Column 而不是 mapped_column() 时。

从版本 2.0 开始更改:对于从 SQLAlchemy 1.4 系列迁移到的用户可能一直在使用 mypy 插件,不再需要使用 declarative_mixin() 类装饰器来标记声明性混合类,假设不再使用 mypy 插件。

扩充基类

除了使用纯混合类之外,本节中的大多数技术也可以直接应用于基类,用于适用于从特定基类派生的所有类的模式。下面的示例演示了上一节中的一些示例在 Base 类方面的情况:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
  """define a series of common elements that may be applied to mapped
 classes using this class as a base class."""

    @declared_attr.directive
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

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

class HasLogRecord:
  """mark classes that have a many-to-one relationship to the
 ``LogRecord`` class."""

    log_record_id: Mapped[int] = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self) -> Mapped["LogRecord"]:
        return relationship("LogRecord")

class LogRecord(Base):
    log_info: Mapped[str]

class MyModel(HasLogRecord, Base):
    name: Mapped[str]

上述,MyModel 以及 LogRecord,在从 Base 派生时,它们的表名都将根据其类名派生,一个名为 id 的主键列,以及由 Base.__table_args__Base.__mapper_args__ 定义的上述表和映射器参数。

当使用遗留 declarative_base()registry.generate_base() 时,可以像下面的非注释示例中所示使用 declarative_base.cls 参数来生成等效效果:

# legacy declarative_base() use

from sqlalchemy import Integer, String
from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base:
  """define a series of common elements that may be applied to mapped
 classes using this class as a base class."""

    @declared_attr.directive
    def __tablename__(cls):
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

    id = mapped_column(Integer, primary_key=True)

Base = declarative_base(cls=Base)

class HasLogRecord:
  """mark classes that have a many-to-one relationship to the
 ``LogRecord`` class."""

    log_record_id = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self):
        return relationship("LogRecord")

class LogRecord(Base):
    log_info = mapped_column(String)

class MyModel(HasLogRecord, Base):
    name = mapped_column(String)

混合列

如果使用声明式表 风格的配置(而不是命令式表 配置),则可以在混合中指定列,以便混合中声明的列随后将被复制为声明式进程生成的Table 的一部分。在声明式混合中可以内联声明三种构造:mapped_column()MappedColumn

class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(default=func.now())
    updated_at: Mapped[datetime]

class MyModel(TimestampMixin, Base):
    __tablename__ = "test"

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

在上述位置,所有包含TimestampMixin在其类基础中的声明性类将自动包含一个应用于所有行插入的时间戳的列created_at,以及一个updated_at列,该列不包含示例目的的默认值(如果有的话,我们将使用Column.onupdate 参数,该参数由mapped_column() 接受)。这些列结构始终是从原始混合或基类复制的,因此相同的混合/基类可以应用于任意数量的目标类,每个目标类都将具有自己的列结构。

所有声明式列形式都受混合支持,包括:

  • 带有注解的属性 - 无论是否存在mapped_column()

    class TimestampMixin:
        created_at: Mapped[datetime] = mapped_column(default=func.now())
        updated_at: Mapped[datetime]
    
  • mapped_column - 无论是否存在 Mapped

    class TimestampMixin:
        created_at = mapped_column(default=func.now())
        updated_at: Mapped[datetime] = mapped_column()
    
  • - 传统的声明式形式:

    class TimestampMixin:
        created_at = Column(DateTime, default=func.now())
        updated_at = Column(DateTime)
    

在上述每种形式中,声明式通过创建构造的副本来处理混合类上的基于列的属性,然后将其应用于目标类。

自版本 2.0 起更改:声明式 API 现在可以容纳Column 对象以及使用混合时的任何形式的mapped_column() 构造,而无需使用declared_attr()。已经删除了以前的限制,该限制阻止具有ForeignKey 元素的列直接在混合中使用。

混入关系

通过relationship()创建的关系,仅使用declared_attr方法提供声明性混合类,从而消除了复制关系及其可能绑定到列的内容时可能出现的任何歧义。下面是一个示例,其中结合了外键列和关系,以便两个类FooBar都可以通过多对一引用到一个公共目标类:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class RefTargetMixin:
    target_id: Mapped[int] = mapped_column(ForeignKey("target.id"))

    @declared_attr
    def target(cls) -> Mapped["Target"]:
        return relationship("Target")

class Foo(RefTargetMixin, Base):
    __tablename__ = "foo"
    id: Mapped[int] = mapped_column(primary_key=True)

class Bar(RefTargetMixin, Base):
    __tablename__ = "bar"
    id: Mapped[int] = mapped_column(primary_key=True)

class Target(Base):
    __tablename__ = "target"
    id: Mapped[int] = mapped_column(primary_key=True)

使用上面的映射,FooBar中的每一个都包含一个访问.target属性的到Target的关系:

>>> from sqlalchemy import select
>>> print(select(Foo).join(Foo.target))
SELECT  foo.id,  foo.target_id
FROM  foo  JOIN  target  ON  target.id  =  foo.target_id
>>> print(select(Bar).join(Bar.target))
SELECT  bar.id,  bar.target_id
FROM  bar  JOIN  target  ON  target.id  =  bar.target_id 

特殊参数,比如relationship.primaryjoin,也可以在混合类方法中使用,这些方法通常需要引用正在映射的类。对于需要引用本地映射列的方案,在普通情况下,这些列将作为 Declarative 的属性在映射类上提供,并作为传递给装饰类方法的cls参数。利用这个特性,我们可以例如重新编写RefTargetMixin.target方法,使用明确的 primaryjoin,它引用了Targetcls上的待定映射列:

class Target(Base):
    __tablename__ = "target"
    id: Mapped[int] = mapped_column(primary_key=True)

class RefTargetMixin:
    target_id: Mapped[int] = mapped_column(ForeignKey("target.id"))

    @declared_attr
    def target(cls) -> Mapped["Target"]:
        # illustrates explicit 'primaryjoin' argument
        return relationship("Target", primaryjoin=Target.id == cls.target_id)
```  ## 混合使用`column_property()` 和其他 `MapperProperty` 类

像`relationship()`一样,其他的`MapperProperty`子类,比如`column_property()`,在混合使用时也需要生成类本地副本,因此也在被`declared_attr`装饰的函数中声明。在函数内部,使用`mapped_column()`、`Mapped`或`Column`声明的其他普通映射列将从`cls`参数中提供,以便可以用于组合新的属性,如下面的示例,将两列相加:

```py
from sqlalchemy.orm import column_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class SomethingMixin:
    x: Mapped[int]
    y: Mapped[int]

    @declared_attr
    def x_plus_y(cls) -> Mapped[int]:
        return column_property(cls.x + cls.y)

class Something(SomethingMixin, Base):
    __tablename__ = "something"

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

在上面的示例中,我们可以在语句中使用Something.x_plus_y,产生完整的表达式:

>>> from sqlalchemy import select
>>> print(select(Something.x_plus_y))
SELECT  something.x  +  something.y  AS  anon_1
FROM  something 

提示

declared_attr 装饰器使装饰的可调用对象的行为与类方法完全相同。然而,像Pylance这样的类型工具可能无法识别这一点,有时会因为无法访问函数体内的 cls 变量而抱怨。要解决此问题,当发生时,可以直接将 @classmethod 装饰器与declared_attr 结合使用,如下所示:

class SomethingMixin:
    x: Mapped[int]
    y: Mapped[int]

    @declared_attr
    @classmethod
    def x_plus_y(cls) -> Mapped[int]:
        return column_property(cls.x + cls.y)

新版本 2.0 中:- declared_attr 可以适应使用 @classmethod 装饰的函数来协助PEP 484集成的需要。## 使用混合和基类进行映射继承模式

在处理如映射类继承层次结构中记录的映射器继承模式时,当使用 declared_attr 时,可以使用一些附加功能,无论是与混合类一起使用,还是在类层次结构中增加映射和未映射的超类时。

当在映射继承层次结构中由子类解释的函数由declared_attr装饰在混合或基类上定义时,必须区分两种情况,即生成 Declarative 使用的特殊名称如 __tablename____mapper_args__ 与生成普通映射属性如mapped_column()relationship()。定义 Declarative 指令 的函数会 在层次结构中的每个子类中调用,而生成 映射属性 的函数只会 在层次结构中的第一个映射的超类中调用

此行为差异的基本原理是,映射属性已经可以被类继承,例如,超类映射表上的特定列不应该在子类中重复出现,而特定于特定类或其映射表的元素不可继承,例如,局部映射的表的名称。

这两种用例之间行为上的差异在以下两个部分中得到了展示。

使用declared_attr()结合继承的TableMapper参数

使用 mixin 的一个常见方法是创建一个 def __tablename__(cls) 函数,动态生成映射的 Table 名称。

这个方法可以用于生成继承映射层次结构中的表名称,就像下面的示例一样,该示例创建一个 mixin,根据类名给每个类生成一个简单的表名称。下面的示例说明了如何为 Person 映射类和 PersonEngineer 子类生成表名称,但不为 PersonManager 子类生成表名称:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
  """override __tablename__ so that Manager is single-inheritance to Person"""

        return None

    __mapper_args__ = {"polymorphic_identity": "manager"}

在上述示例中,Person 基类和 Engineer 类都是 Tablename mixin 类的子类,该类生成新的表名称,因此它们都将具有生成的 __tablename__ 属性。对于 Declarative,这意味着每个类都应该有自己的 Table 生成,并将其映射到其中。对于 Engineer 子类,所应用的继承风格是联合表继承,因为它将映射到一个与基本 person 表连接的 engineer 表。从 Person 继承的任何其他子类也将默认应用此继承风格(在此特定示例中,每个子类都需要指定一个主键列;关于这一点,后面会详细介绍)。

相比之下,PersonManager 子类覆盖__tablename__ 类方法,将其返回值设为 None。这表明对于 Declarative 来说,该类不应生成一个 Table,而应仅使用 Person 映射到的基本 Table。对于 Manager 子类,所应用的继承风格是单表继承。

上面的示例说明了 Declarative 指令(如 __tablename__)必须分别应用于每个子类,因为每个映射类都需要说明将映射到哪个 Table,或者是否将自身映射到继承的超类的 Table

如果我们希望反转上面说明的默认表方案,使得单表继承成为默认,只有在提供了 __tablename__ 指令以覆盖它时才能定义连接表继承,我们可以在顶级 __tablename__() 方法中使用 Declarative 助手,本例中称为 has_inherited_table()。此函数将返回 True 如果超类已经映射到一个 Table。我们可以在基类中的最低级 __tablename__() 类方法中使用此辅助函数,以便我们有条件地如果表已经存在,则返回 None 作为表名,从而默认为继承子类的单表继承:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import has_inherited_table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls):
        if has_inherited_table(cls):
            return None
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    @declared_attr.directive
    def __tablename__(cls):
  """override __tablename__ so that Engineer is joined-inheritance to Person"""

        return cls.__name__.lower()

    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    __mapper_args__ = {"polymorphic_identity": "manager"}

使用 declared_attr() 生成特定于表的继承列

与在与 declared_attr 一起使用时如何处理 __tablename__ 和其他特殊名称不同,当我们混入列和属性(例如关系、列属性等)时,该函数仅在层次结构中的基类调用,除非结合使用 declared_attr 指令和 declared_attr.cascading 子指令。在下面的示例中,只有 Person 类将收到名为 id 的列;对于未给出主键的 Engineer,映射将失败:

class HasId:
    id: Mapped[int] = mapped_column(primary_key=True)

class Person(HasId, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

# this mapping will fail, as there's no primary key
class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

在连接表继承中,通常情况下我们希望每个子类具有不同命名的列。然而在这种情况下,我们可能希望每个表上都有一个 id 列,并且通过外键相互引用。我们可以通过使用 declared_attr.cascading 修饰符作为 mixin 来实现这一点,该修饰符表示应该对层次结构中的每个类调用该函数,几乎(见下面的警告)与对 __tablename__ 调用方式相同:

class HasIdMixin:
    @declared_attr.cascading
    def id(cls) -> Mapped[int]:
        if has_inherited_table(cls):
            return mapped_column(ForeignKey("person.id"), primary_key=True)
        else:
            return mapped_column(Integer, primary_key=True)

class Person(HasIdMixin, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

警告

目前,declared_attr.cascading 功能不允许子类使用不同的函数或值覆盖属性。这是在如何解析 @declared_attr 的机制中的当前限制,并且如果检测到此条件,则会发出警告。此限制仅适用于 ORM 映射的列、关系和其他属性的MapperProperty风格。它适用于诸如 __tablename____mapper_args__ 等的声明性指令,这些指令在内部以与declared_attr.cascading不同的方式解析。

从多个混合类组合表/映射器参数

当使用声明性混合类指定 __table_args____mapper_args__ 时,您可能希望将一些参数从多个混合类中与您希望在类本身上定义的参数结合起来。可以在这里使用 declared_attr 装饰器来创建用户定义的排序例程,该例程从多个集合中提取:

from sqlalchemy.orm import declarative_mixin, declared_attr

class MySQLSettings:
    __table_args__ = {"mysql_engine": "InnoDB"}

class MyOtherMixin:
    __table_args__ = {"info": "foo"}

class MyModel(MySQLSettings, MyOtherMixin, Base):
    __tablename__ = "my_model"

    @declared_attr.directive
    def __table_args__(cls):
        args = dict()
        args.update(MySQLSettings.__table_args__)
        args.update(MyOtherMixin.__table_args__)
        return args

    id = mapped_column(Integer, primary_key=True)

在混合类上使用命名约定创建索引和约束

对于具有命名约束的使用,如 IndexUniqueConstraintCheckConstraint,其中每个对象应该是唯一的,针对从混合类派生的特定表,需要为每个实际映射的类创建每个对象的单独实例。

作为一个简单的示例,要定义一个命名的、可能是多列的 Index,该索引适用于从混合类派生的所有表,可以使用 Index 的“内联”形式,并将其建立为 __table_args__ 的一部分,使用 declared_attr 来建立 __table_args__() 作为一个类方法,该方法将被每个子类调用:

class MyMixin:
    a = mapped_column(Integer)
    b = mapped_column(Integer)

    @declared_attr.directive
    def __table_args__(cls):
        return (Index(f"test_idx_{cls.__tablename__}", "a", "b"),)

class MyModelA(MyMixin, Base):
    __tablename__ = "table_a"
    id = mapped_column(Integer, primary_key=True)

class MyModelB(MyMixin, Base):
    __tablename__ = "table_b"
    id = mapped_column(Integer, primary_key=True)

上面的示例将生成两个表 "table_a""table_b",其中包含索引 "test_idx_table_a""test_idx_table_b"

通常,在现代 SQLAlchemy 中,我们会使用一种命名约定,如在配置约束命名约定中记录的那样。虽然命名约定会在创建新的Constraint对象时自动进行,因为此约定是在基于特定Constraint的父Table的对象构造时间应用的,因此需要为每个继承子类创建一个不同的Constraint对象,并再次使用declared_attr__table_args__(),下面通过使用抽象映射基类进行说明:

from uuid import UUID

from sqlalchemy import CheckConstraint
from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

constraint_naming_conventions = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

class Base(DeclarativeBase):
    metadata = MetaData(naming_convention=constraint_naming_conventions)

class MyAbstractBase(Base):
    __abstract__ = True

    @declared_attr.directive
    def __table_args__(cls):
        return (
            UniqueConstraint("uuid"),
            CheckConstraint("x > 0 OR y < 100", name="xy_chk"),
        )

    id: Mapped[int] = mapped_column(primary_key=True)
    uuid: Mapped[UUID]
    x: Mapped[int]
    y: Mapped[int]

class ModelAlpha(MyAbstractBase):
    __tablename__ = "alpha"

class ModelBeta(MyAbstractBase):
    __tablename__ = "beta"

上述映射将生成包含所有约束的表特定名称的 DDL,包括主键、CHECK 约束、唯一约束:

CREATE  TABLE  alpha  (
  id  INTEGER  NOT  NULL,
  uuid  CHAR(32)  NOT  NULL,
  x  INTEGER  NOT  NULL,
  y  INTEGER  NOT  NULL,
  CONSTRAINT  pk_alpha  PRIMARY  KEY  (id),
  CONSTRAINT  uq_alpha_uuid  UNIQUE  (uuid),
  CONSTRAINT  ck_alpha_xy_chk  CHECK  (x  >  0  OR  y  <  100)
)

CREATE  TABLE  beta  (
  id  INTEGER  NOT  NULL,
  uuid  CHAR(32)  NOT  NULL,
  x  INTEGER  NOT  NULL,
  y  INTEGER  NOT  NULL,
  CONSTRAINT  pk_beta  PRIMARY  KEY  (id),
  CONSTRAINT  uq_beta_uuid  UNIQUE  (uuid),
  CONSTRAINT  ck_beta_xy_chk  CHECK  (x  >  0  OR  y  <  100)
)

增强基类

除了使用纯混合外,本节中的大多数技术也可以直接应用于基类,用于适用于从特定基类派生的所有类的模式。下面的示例说明了如何在Base类方面应用上一节的一些示例:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
  """define a series of common elements that may be applied to mapped
 classes using this class as a base class."""

    @declared_attr.directive
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

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

class HasLogRecord:
  """mark classes that have a many-to-one relationship to the
 ``LogRecord`` class."""

    log_record_id: Mapped[int] = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self) -> Mapped["LogRecord"]:
        return relationship("LogRecord")

class LogRecord(Base):
    log_info: Mapped[str]

class MyModel(HasLogRecord, Base):
    name: Mapped[str]

在上述示例中,MyModelLogRecord,在派生自Base时,它们的表名都将根据其类名派生,一个名为id的主键列,以及由Base.__table_args__Base.__mapper_args__定义的上述表和映射器参数。

在使用旧版declarative_base()registry.generate_base()时,可以使用declarative_base.cls参数来生成等效效果,如下所示的未注释示例:

# legacy declarative_base() use

from sqlalchemy import Integer, String
from sqlalchemy import ForeignKey
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base:
  """define a series of common elements that may be applied to mapped
 classes using this class as a base class."""

    @declared_attr.directive
    def __tablename__(cls):
        return cls.__name__.lower()

    __table_args__ = {"mysql_engine": "InnoDB"}
    __mapper_args__ = {"eager_defaults": True}

    id = mapped_column(Integer, primary_key=True)

Base = declarative_base(cls=Base)

class HasLogRecord:
  """mark classes that have a many-to-one relationship to the
 ``LogRecord`` class."""

    log_record_id = mapped_column(ForeignKey("logrecord.id"))

    @declared_attr
    def log_record(self):
        return relationship("LogRecord")

class LogRecord(Base):
    log_info = mapped_column(String)

class MyModel(HasLogRecord, Base):
    name = mapped_column(String)

混合列

如果使用声明式表的配置风格(而不是命令式表配置),则可以在混合类中指示列,以便在声明式过程生成的 Table 的一部分。可以在声明式混合类中内联声明三种构造:mapped_column()MappedColumn

class TimestampMixin:
    created_at: Mapped[datetime] = mapped_column(default=func.now())
    updated_at: Mapped[datetime]

class MyModel(TimestampMixin, Base):
    __tablename__ = "test"

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

在上述情况下,所有包括 TimestampMixin 在其类基类中的声明式类将自动包含一个名为 created_at 的列,该列对所有行插入应用时间戳,以及一个名为 updated_at 的列,该列不包含默认值以示例为目的(如果有的话,我们将使用 Column.onupdate 参数,该参数被 mapped_column() 接受)。这些列构造始终从源混合类或基类复制,因此可以将相同的混合类/基类应用于任意数量的目标类,每个目标类都将有自己的列构造。

所有声明式列形式都受到混合类的支持,包括:

  • 带注释的属性 - 无论是否存在 mapped_column()

    class TimestampMixin:
        created_at: Mapped[datetime] = mapped_column(default=func.now())
        updated_at: Mapped[datetime]
    
  • mapped_column - 无论是否存在 Mapped

    class TimestampMixin:
        created_at = mapped_column(default=func.now())
        updated_at: Mapped[datetime] = mapped_column()
    
  • Column - 传统的声明式形式:

    class TimestampMixin:
        created_at = Column(DateTime, default=func.now())
        updated_at = Column(DateTime)
    

在上述每种形式中,声明式通过创建构造的副本来处理混合类上的基于列的属性,然后将其应用于目标类。

版本 2.0 中的变化:声明式 API 现在可以接受 Column 对象以及任何形式的 mapped_column() 构造,当使用混合类时无需使用 declared_attr()。已经移除了以前的限制,这些限制阻止直接在混合类中使用具有 ForeignKey 元素的列。

混入关系

通过relationship()创建的关系通过declared_attr方法提供的声明式混合类,排除了在复制关系及其可能与列绑定的内容时可能出现的任何歧义。下面是一个示例,将外键列和关系组合在一起,以便两个类FooBar都可以配置为通过多对一引用一个共同的目标类:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship

class Base(DeclarativeBase):
    pass

class RefTargetMixin:
    target_id: Mapped[int] = mapped_column(ForeignKey("target.id"))

    @declared_attr
    def target(cls) -> Mapped["Target"]:
        return relationship("Target")

class Foo(RefTargetMixin, Base):
    __tablename__ = "foo"
    id: Mapped[int] = mapped_column(primary_key=True)

class Bar(RefTargetMixin, Base):
    __tablename__ = "bar"
    id: Mapped[int] = mapped_column(primary_key=True)

class Target(Base):
    __tablename__ = "target"
    id: Mapped[int] = mapped_column(primary_key=True)

使用上述映射,每个FooBar都包含一个到Target的关系,通过.target属性访问:

>>> from sqlalchemy import select
>>> print(select(Foo).join(Foo.target))
SELECT  foo.id,  foo.target_id
FROM  foo  JOIN  target  ON  target.id  =  foo.target_id
>>> print(select(Bar).join(Bar.target))
SELECT  bar.id,  bar.target_id
FROM  bar  JOIN  target  ON  target.id  =  bar.target_id 

类似relationship.primaryjoin的特殊参数也可以在混入的 classmethod 中使用,这些参数通常需要引用正在映射的类。对于需要引用本地映射列的方案,在普通情况下,这些列通过 Declarative 作为映射类的属性提供,该类作为参数cls传递给修饰的 classmethod。使用此功能,我们可以例如使用显式的 primaryjoin 重写RefTargetMixin.target方法,该方法引用了Targetcls上的待定映射列:

class Target(Base):
    __tablename__ = "target"
    id: Mapped[int] = mapped_column(primary_key=True)

class RefTargetMixin:
    target_id: Mapped[int] = mapped_column(ForeignKey("target.id"))

    @declared_attr
    def target(cls) -> Mapped["Target"]:
        # illustrates explicit 'primaryjoin' argument
        return relationship("Target", primaryjoin=Target.id == cls.target_id)

混合使用column_property()和其他MapperProperty

relationship()类似,其他MapperProperty子类,如column_property()在被混合使用时也需要生成类局部副本,因此在被declared_attr修饰的函数内声明。在该函数内,使用mapped_column()MappedColumn声明的其他普通映射列将从cls参数中提取,以便它们可以被用来组合新的属性,如下例所示,将两个列相加:

from sqlalchemy.orm import column_property
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class SomethingMixin:
    x: Mapped[int]
    y: Mapped[int]

    @declared_attr
    def x_plus_y(cls) -> Mapped[int]:
        return column_property(cls.x + cls.y)

class Something(SomethingMixin, Base):
    __tablename__ = "something"

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

在上面的例子中,我们可以在生成完整表达式的语句中使用Something.x_plus_y

>>> from sqlalchemy import select
>>> print(select(Something.x_plus_y))
SELECT  something.x  +  something.y  AS  anon_1
FROM  something 

提示

declared_attr装饰器使装饰的可调用对象表现得完全像一个类方法。然而,像Pylance这样的类型工具可能无法识别这一点,这有时会导致它在函数体内部抱怨无法访问cls变量。当出现此问题时,可以直接将@classmethod装饰器与declared_attr结合使用来解决:

class SomethingMixin:
    x: Mapped[int]
    y: Mapped[int]

    @declared_attr
    @classmethod
    def x_plus_y(cls) -> Mapped[int]:
        return column_property(cls.x + cls.y)

新版本 2.0 中:- declared_attr可以容纳使用@classmethod装饰的函数,以帮助需要的PEP 484集成。

使用混合类和基类与映射继承模式

在处理映射类继承层次结构中所记录的映射继承模式时,使用declared_attr时,无论是使用混合类还是在类层次结构中增加映射和非映射的超类时,都会存在一些额外的功能。

当在混合类或基类上定义由declared_attr装饰的函数,以便由映射继承层次结构中的子类解释时,有一个重要的区别是函数生成特殊名称(例如__tablename____mapper_args__)与生成普通映射属性(例如mapped_column()relationship())之间的区别。定义声明性指令的函数在层次结构中的每个子类中都会被调用,而生成映射属性的函数仅在层次结构中的第一个映射的超类中被调用

此行为差异的原理是映射属性已经可以被类继承,例如,超类映射表上的特定列不应该重复到子类中,而特定于特定类或其映射表的元素不可继承,例如本地映射的表名。

这两种用例之间行为上的差异在以下两个部分中得到展示。

使用带有继承TableMapper参数的declared_attr()

使用混合类的常见方法是创建一个def __tablename__(cls)函数,动态生成映射的Table的名称。

这个配方可用于为继承映射器层次结构生成表名,如下例所示,该示例创建了一个混合类,根据类名为每个类提供一个简单的表名。下面的示例说明了这个配方,在这个示例中为Person映射类和Person的子类Engineer生成了一个表名,但没有为Person的子类Manager生成表名:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
  """override __tablename__ so that Manager is single-inheritance to Person"""

        return None

    __mapper_args__ = {"polymorphic_identity": "manager"}

在上面的示例中,Person基类以及Engineer类,作为生成新表名的Tablename混合类的子类,都将有一个生成的__tablename__属性,这对于 Declarative 表示每个类应该有自己的Table生成并映射到它。对于Engineer子类,应用的继承风格是联接表继承,因为它将映射到一个与基本person表连接的engineer表。从Person继承的任何其他子类也将默认应用这种继承风格(在这个特定示例中,需要为每个子类指定一个主键列;在下一节中会详细介绍)。

相比之下,Person的子类Manager 覆盖__tablename__类方法以返回None。这告诉 Declarative 这个类 不应 生成一个Table,而应该完全使用Person映射到的基本Table。对于Manager子类,应用的继承风格是单表继承。

上面的示例说明了 Declarative 指令如__tablename__必须分别应用于每个子类,因为每个映射类都需要声明将映射到哪个Table,或者是否将自身映射到继承的超类的Table

如果我们希望反转上述默认表方案,使单表继承成为默认情况,并且只有在提供__tablename__指令以覆盖它时才能定义联接表继承,则可以在顶层__tablename__()方法中使用 Declarative 辅助函数,在本例中是一个称为has_inherited_table()的辅助函数。如果超类已经映射到Table,此函数将返回True。我们可以在最基本的__tablename__()类方法中使用此辅助函数,以便在表已存在时有条件地返回None作为表名,从而默认情况下通过继承子类进行单表继承:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import has_inherited_table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls):
        if has_inherited_table(cls):
            return None
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    @declared_attr.directive
    def __tablename__(cls):
  """override __tablename__ so that Engineer is joined-inheritance to Person"""

        return cls.__name__.lower()

    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    __mapper_args__ = {"polymorphic_identity": "manager"}

使用declared_attr()生成特定表继承列

与在使用declared_attr时处理__tablename__和其他特殊名称的方式相反,当我们混合列和属性(例如关系、列属性等)时,该函数仅在层次结构中的基类上调用,除非在与declared_attr.cascading子指令结合使用时使用declared_attr指令。在下面的示例中,只有Person类将接收一个名为id的列;对于未给出主键的Engineer,映射将失败:

class HasId:
    id: Mapped[int] = mapped_column(primary_key=True)

class Person(HasId, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

# this mapping will fail, as there's no primary key
class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

在联接表继承中,通常情况下我们希望每个子类上都有不同命名的列。然而,在这种情况下,我们可能希望在每个表上都有一个id列,并通过外键引用它们。我们可以通过使用declared_attr.cascading修饰符作为混合来实现这一点,该修饰符指示该函数应该以几乎(请参阅下面的警告)与__tablename__相同的方式为层次结构中的每个类调用:

class HasIdMixin:
    @declared_attr.cascading
    def id(cls) -> Mapped[int]:
        if has_inherited_table(cls):
            return mapped_column(ForeignKey("person.id"), primary_key=True)
        else:
            return mapped_column(Integer, primary_key=True)

class Person(HasIdMixin, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

警告

declared_attr.cascading 特性目前不允许子类使用不同的函数或值覆盖属性。这是在解析@declared_attr的机制中的当前限制,并且如果检测到此条件,则会发出警告。这个限制仅适用于 ORM 映射的列、关系和其他MapperProperty风格的属性。它不适用于声明性指令,如__tablename____mapper_args__等,在内部的解析方式与declared_attr.cascading不同。

使用 declared_attr() 与继承的 TableMapper 参数

使用 mixin 的一个常见方法是创建一个def __tablename__(cls)函数,该函数动态生成映射的 Table 名称。

此示例可用于为继承的映射器层次结构生成表名,如下例所示,它创建了一个基于类名的简单表名的 mixin。下面的示例说明了该示例,其中为Person映射类和PersonEngineer子类生成了一个表名,但未为PersonManager子类生成表名:

from typing import Optional

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    @declared_attr.directive
    def __tablename__(cls) -> Optional[str]:
  """override __tablename__ so that Manager is single-inheritance to Person"""

        return None

    __mapper_args__ = {"polymorphic_identity": "manager"}

在上面的示例中,Person基类和Engineer类都是Tablename mixin 类的子类,该类生成新的表名,因此它们都会有一个生成的__tablename__属性,对于声明性来说,这表示每个类都应该有自己的 Table 生成,并且将映射到该表。对于Engineer子类,应用的继承风格是联接表继承,因为它将映射到一个连接到基本person表的表engineer。从Person继承的任何其他子类也将默认应用此继承风格(并且在这个特定示例中,每个子类都需要指定一个主键列;更多关于这一点的内容将在下一节中介绍)。

相比之下,PersonManager子类覆盖__tablename__类方法以返回None。这表明对于 Declarative 来说,这个类应该生成一个Table,而是完全使用Person被映射到的基础Table。对于Manager子类,应用的继承样式是单表继承。

上面的示例说明 Declarative 指令如__tablename__必须分别应用于每个子类,因为每个映射类都需要说明它将映射到哪个Table,或者它将自行映射到继承超类的Table

如果我们希望反转上面示例中的默认表方案,使得单表继承成为默认,并且只有在提供了__tablename__指令来覆盖它时才能定义连接表继承,我们可以在最顶层的__tablename__()方法中使用 Declarative 助手,本例中称为has_inherited_table()。此函数将在超类已经映射到Table时返回True。我们可以在最基本的__tablename__()类方法中使用此助手,以便我们可以在表已经存在时有条件地返回None作为表名,从而默认指示继承子类的单表继承:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import has_inherited_table
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

class Base(DeclarativeBase):
    pass

class Tablename:
    @declared_attr.directive
    def __tablename__(cls):
        if has_inherited_table(cls):
            return None
        return cls.__name__.lower()

class Person(Tablename, Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    @declared_attr.directive
    def __tablename__(cls):
  """override __tablename__ so that Engineer is joined-inheritance to Person"""

        return cls.__name__.lower()

    id: Mapped[int] = mapped_column(ForeignKey("person.id"), primary_key=True)

    primary_language: Mapped[str]

    __mapper_args__ = {"polymorphic_identity": "engineer"}

class Manager(Person):
    __mapper_args__ = {"polymorphic_identity": "manager"}

使用declared_attr()生成特定于表的继承列

与在使用declared_attr时处理__tablename__和其他特殊名称的方式相反,当我们混合列和属性(例如关系、列属性等)时,该函数仅在层次结构中的基类中调用,除非与declared_attr.cascading子指令结合使用declared_attr指令。下面,只有Person类将收到名为id的列;对于未给出主键的Engineer,映射将失败:

class HasId:
    id: Mapped[int] = mapped_column(primary_key=True)

class Person(HasId, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

# this mapping will fail, as there's no primary key
class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

在连接表继承中通常情况下,我们希望每个子类都有具有不同名称的列。但在这种情况下,我们可能希望在每个表上都有一个id列,并且通过外键相互引用。我们可以通过使用declared_attr.cascading修饰符作为混入来实现此目的,该修饰符指示该函数应在层次结构中的每个类中调用,与__tablename__几乎(参见下面的警告)相同的方式:

class HasIdMixin:
    @declared_attr.cascading
    def id(cls) -> Mapped[int]:
        if has_inherited_table(cls):
            return mapped_column(ForeignKey("person.id"), primary_key=True)
        else:
            return mapped_column(Integer, primary_key=True)

class Person(HasIdMixin, Base):
    __tablename__ = "person"

    discriminator: Mapped[str]
    __mapper_args__ = {"polymorphic_on": "discriminator"}

class Engineer(Person):
    __tablename__ = "engineer"

    primary_language: Mapped[str]
    __mapper_args__ = {"polymorphic_identity": "engineer"}

警告

declared_attr.cascading 特性目前允许子类使用不同的函数或值来覆盖属性。这是在解析@declared_attr时当前机制的限制,并且如果检测到此条件,则会发出警告。此限制仅适用于 ORM 映射的列、关联和其他MapperProperty风格的属性。它适用于诸如__tablename____mapper_args__等的声明性指令,后者在内部解析方式与declared_attr.cascading不同。

将来自多个混入的表/映射器参数组合起来

在使用声明性混入指定的__table_args____mapper_args__的情况下,您可能希望将几个混入的一些参数与您希望在类本身上定义的参数合并。在这里可以使用declared_attr装饰器来创建用户定义的排序例程,这些例程来自多个集合:

from sqlalchemy.orm import declarative_mixin, declared_attr

class MySQLSettings:
    __table_args__ = {"mysql_engine": "InnoDB"}

class MyOtherMixin:
    __table_args__ = {"info": "foo"}

class MyModel(MySQLSettings, MyOtherMixin, Base):
    __tablename__ = "my_model"

    @declared_attr.directive
    def __table_args__(cls):
        args = dict()
        args.update(MySQLSettings.__table_args__)
        args.update(MyOtherMixin.__table_args__)
        return args

    id = mapped_column(Integer, primary_key=True)

使用混入创建具有命名约定的索引和约束

使用命名约束,如IndexUniqueConstraintCheckConstraint,其中每个对象应该是从混入派生的特定表上唯一的,需要为每个实际映射类创建每个对象的单个实例。

作为一个简单的例子,要定义一个命名的、可能是多列的Index,该索引适用于从混合类派生的所有表,可以使用Index的“内联”形式,并将其建立为__table_args__的一部分,使用declared_attr来建立__table_args__()作为一个类方法,该方法将被调用用于每个子类:

class MyMixin:
    a = mapped_column(Integer)
    b = mapped_column(Integer)

    @declared_attr.directive
    def __table_args__(cls):
        return (Index(f"test_idx_{cls.__tablename__}", "a", "b"),)

class MyModelA(MyMixin, Base):
    __tablename__ = "table_a"
    id = mapped_column(Integer, primary_key=True)

class MyModelB(MyMixin, Base):
    __tablename__ = "table_b"
    id = mapped_column(Integer, primary_key=True)

上面的例子将生成两个表"table_a""table_b",带有索引"test_idx_table_a""test_idx_table_b"

通常,在现代 SQLAlchemy 中,我们会使用命名约定,如配置约束命名约定中所述。虽然命名约定在创建新的Constraint对象时会自动进行,因为该约定是根据特定的父Table在对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时应用于对象构建时时的,需要为每个继承子类创建一个独立的Constraint对象,并再次使用declared_attr__table_args__(),下面以抽象映射基类进行说明:

from uuid import UUID

from sqlalchemy import CheckConstraint
from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import UniqueConstraint
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import declared_attr
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column

constraint_naming_conventions = {
    "ix": "ix_%(column_0_label)s",
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s",
}

class Base(DeclarativeBase):
    metadata = MetaData(naming_convention=constraint_naming_conventions)

class MyAbstractBase(Base):
    __abstract__ = True

    @declared_attr.directive
    def __table_args__(cls):
        return (
            UniqueConstraint("uuid"),
            CheckConstraint("x > 0 OR y < 100", name="xy_chk"),
        )

    id: Mapped[int] = mapped_column(primary_key=True)
    uuid: Mapped[UUID]
    x: Mapped[int]
    y: Mapped[int]

class ModelAlpha(MyAbstractBase):
    __tablename__ = "alpha"

class ModelBeta(MyAbstractBase):
    __tablename__ = "beta"

上述映射将生成包括所有约束的特定于表的名称的 DDL,包括主键、CHECK 约束、唯一约束:

CREATE  TABLE  alpha  (
  id  INTEGER  NOT  NULL,
  uuid  CHAR(32)  NOT  NULL,
  x  INTEGER  NOT  NULL,
  y  INTEGER  NOT  NULL,
  CONSTRAINT  pk_alpha  PRIMARY  KEY  (id),
  CONSTRAINT  uq_alpha_uuid  UNIQUE  (uuid),
  CONSTRAINT  ck_alpha_xy_chk  CHECK  (x  >  0  OR  y  <  100)
)

CREATE  TABLE  beta  (
  id  INTEGER  NOT  NULL,
  uuid  CHAR(32)  NOT  NULL,
  x  INTEGER  NOT  NULL,
  y  INTEGER  NOT  NULL,
  CONSTRAINT  pk_beta  PRIMARY  KEY  (id),
  CONSTRAINT  uq_beta_uuid  UNIQUE  (uuid),
  CONSTRAINT  ck_beta_xy_chk  CHECK  (x  >  0  OR  y  <  100)
)

与 dataclasses 和 attrs 集成

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

SQLAlchemy 从 2.0 版本开始具有“本地数据类”集成,在带注释的声明表映射中,可以通过向映射类添加单个 mixin 或装饰器将其转换为 Python dataclass

2.0 版本中的新功能:将数据类创建与 ORM 声明类集成

还有一些可用的模式,允许将现有的数据类映射,以及映射由第三方集成库attrs仪表化的类。

声明式数据类映射

SQLAlchemy 带注释的声明表映射可以通过附加的 mixin 类或装饰器指令进行扩展,在映射完成后将映射类原地转换为 Python dataclass,然后完成应用 ORM 特定的仪表化到类的映射过程。这提供的最突出的行为增加是生成具有对位置和关键字参数具有或不具有默认值的精细控制的__init__()方法,以及生成诸如__repr__()__eq__()等方法。

PEP 484的类型化角度来看,该类被认为具有特定于 Dataclass 的行为,最重要的是通过利用PEP 681“数据类转换”,使类型工具可以将该类视为明确使用了@dataclasses.dataclass装饰器装饰的类。

注意

2023 年 4 月 4 日之后,对于PEP 681在类型工具中的支持是有限的,目前已知由Pyright以及1.2 版Mypy支持。请注意,Mypy 1.1.1 引入了PEP 681支持,但未正确适配 Python 描述符,这将导致在使用 SQLAlchemy 的 ORM 映射方案时出现错误。

另请参阅

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

或者直接应用于从声明性基类扩展的类:

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]

类级特性配置

对于数据类功能的支持是部分的。目前支持的功能有initrepreqorderunsafe_hashmatch_argskw_only在 Python 3.10+上支持。目前不支持的功能有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 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]

关于数据类类选项的背景,请参阅@dataclasses.dataclass中的dataclasses文档。

属性配置

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

另外,ORM 属性配置结构,包括mapped_column()relationship()composite()支持每个属性字段选项,包括initdefaultdefault_factoryrepr。这些参数的名称是按照PEP 681中指定的固定的。功能与 dataclasses 相同:

  • init,如mapped_column.initrelationship.init,如果为 False,则表示该字段不应该是__init__()方法的一部分

  • default,比如mapped_column.defaultrelationship.default,表示字段的默认值,可以在__init__()方法中作为关键字参数给出。

  • default_factory,如mapped_column.default_factoryrelationship.default_factory,表示一个可调用的函数,如果未显式传递给__init__()方法,则将调用该函数生成参数的新默认值。

  • repr默认为 True,表示该字段应该是生成的__repr__()方法的一部分

与 dataclasses 的另一个关键区别是,属性的默认值必须使用 ORM 结构的default参数进行配置,例如mapped_column(default=None)。不支持类似于 dataclass 语法的语法,该语法接受简单的 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' is optional keyword argument
u1 = User("name")

列默认值

为了适应 default 参数与 Column.default 构造函数中现有的参数重叠,mapped_column() 构造函数通过添加一个新的参数 mapped_column.insert_default 来消除这两个名称的歧义,该参数将直接填充到 Column.default 参数中,独立于 mapped_column.default 的设置,后者始终用于数据类配置。例如,要配置一个日期时间列,其 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
    )

使用上述映射,对于未传递 created_at 参数的新 User 对象的 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 

与 Annotated 的集成

将整个列声明映射到 Python 类型 中介绍的方法演示了如何使用 PEP 593Annotated 对象将整个 mapped_column() 构造函数打包以供重用。此功能受数据类功能支持。但是,该功能的一个方面在使用类型工具时需要一个解决方法,即 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()

使用混合和抽象超类

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支持。对于此情况,将发出警告,以后将是一个错误。

另请参阅

转换为数据类时,属性来自不是数据类的超类。 - 关于原因的背景

关系配置

当与relationship()结合使用时,Mapped注释的使用方式与基本关系模式中描述的方式相同。在指定基于集合的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)

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

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

使用非映射数据类字段

当使用声明式数据类时,类上也可以使用非映射字段,这些字段将成为数据类构造过程的一部分,但不会被映射。任何不使用 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 注解的字段可能会被包括在内,这些字段将被视为结果数据类的一部分,但不会被映射,无需指定 __allow_unmapped__ 类属性也可以。以前的 2.0 beta 版本需要显式地包含此属性,即使此属性的目的仅是为了使传统的 ORM 类型映射继续正常工作。 ### 与 Pydantic 等备选数据类提供程序集成

警告

Pydantic 的数据类层与 SQLAlchemy 的类仪器不完全兼容,除非进行额外的内部更改,否则许多功能,如相关集合,可能无法正常工作。

为了与 Pydantic 兼容,请考虑使用基于 SQLAlchemy ORM 构建的SQLModel ORM,该 ORM 包含专门解决这些不兼容性的实现细节。

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 的功能。## 将 ORM 映射应用于现有数据类(旧版数据类用法)

遗留特性

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

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

  • 使用“具有命令式表”的方法,要映射的表/列是使用分配给类的__table__属性的Table对象来定义的;关系在__mapper_args__字典中定义。使用registry.mapped()装饰器对类进行映射。以下是一个示例,位于使用具有命令式表的预先存在的数据类进行映射。

  • 使用完全“声明式”方法,Declarative 解释的指令,如Columnrelationship()被添加到dataclasses.field()构造函数的.metadata字典中,它们被声明性过程使用。再次使用registry.mapped()装饰器对类进行映射。请参见下面的示例,在使用声明式样式字段映射预先存在的数据类。

  • 可以使用registry.map_imperatively()方法将“命令式”映射应用到现有的数据类上,以完全相同的方式生成映射,就像在命令式映射中描述的那样。下面在使用命令式映射映射预先存在的数据类中进行了说明。

SQLAlchemy 将映射应用到数据类的一般过程与普通类的过程相同,但还包括 SQLAlchemy 将检测到的类级属性,这些属性是数据类声明过程的一部分,并在运行时用通常的 SQLAlchemy ORM 映射属性替换它们。由数据类生成的__init__方法保持不变,以及数据类生成的所有其他方法,如__eq__()__repr__()等。

使用具有命令式表的预先存在的数据类进行映射

下面是使用带命令式表的声明式(即混合声明)的@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的构造函数。这种方法的另一种选择在下一个示例中。

警告

使用default设置一个defaultinit=False的数据类字段()将不像预期的那样与完全普通的数据类一起工作,因为 SQLAlchemy 类工具将替换数据类创建过程中在类上设置的默认值。而是使用default_factory。当使用声明性数据类映射时,此适应过程会自动完成。### 使用声明式字段映射现有数据类

遗留功能

使用数据类进行声明性映射的这种方法应被视为遗留。它将继续受支持,但不太可能提供任何优势,与声明性数据类映射中详细描述的新方法相比。

请注意,不支持使用 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))})

使用声明性混合类型与预先存在的数据类

在使用混合类型构成映射层次结构部分中,引入了声明性混合类型类。声明性混合类型的一个要求是,某些不能轻易复制的构造必须作为可调用对象给出,使用declared_attr装饰器,例如在混合关系示例中:

class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column("target_id", ForeignKey("target.id"))

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

在数据类field()对象中,通过使用 lambda 表示 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对象,用于在“带有声明性表格的数据类”样式映射中使用。### 使用命令式映射映射预先存在的数据类

如前所述,使用 @dataclass 装饰器设置为 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)

使用此映射样式时,请注意 Mapping pre-existing dataclasses using Declarative With Imperative Table 中提到的相同警告。## 对现有 attrs 类应用 ORM 映射

attrs 库是一个流行的第三方库,提供了类似 dataclasses 的功能,并提供了许多普通 dataclasses 中没有的附加功能。

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

SQLAlchemy ORM 支持使用 Declarative with Imperative TableImperative 映射来映射 attrs 类。这两种样式的一般形式与用于 dataclasses 的 Mapping pre-existing dataclasses using Declarative-style fields 和 Mapping pre-existing dataclasses using Declarative With Imperative Table 映射形式完全等效,其中 dataclasses 或 attrs 使用的内联属性指令保持不变,并且 SQLAlchemy 的面向表的仪器化在运行时应用。

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

使用 Declarative “Imperative Table” 映射 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]]

注意

attrsslots=True选项,该选项在映射类上启用__slots__,不能与 SQLAlchemy 映射一起使用,除非完全实现了替代的属性仪器化,因为映射类通常依赖于对__dict__的直接访问来存储状态。当此选项存在时,行为是未定义的。

使用命令式映射映射 attrs

就像对待 dataclasses 一样,我们可以利用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)

上述形式等同于之前使用声明式与命令式表的示例。## 声明式数据类映射

SQLAlchemy 带注释的声明表映射可以通过附加的 mixin 类或装饰器指令来增强,这将在映射完成后的声明过程中添加一个额外的步骤,将映射的类原地转换为 Python dataclass,然后完成应用于类的 ORM 特定仪器化过程。这提供的最突出的行为增强是生成具有对位置和关键字参数的细粒度控制的__init__()方法,有或没有默认值,以及生成诸如__repr__()__eq__()之类的方法。

PEP 484的类型学角度来看,该类被认为具有 Dataclass 特定的行为,最值得注意的是通过利用PEP 681“Dataclass Transforms”,这允许类型工具将该类视为明确使用@dataclasses.dataclass装饰器装饰的类。

注意

截至2023 年 4 月 4 日,在类型工具中支持PEP 681的情况是有限的,并且目前已知被Pyright以及1.2 版Mypy支持。请注意,Mypy 1.1.1 引入了PEP 681支持,但未正确适应 Python 描述符,这将导致在使用 SQLAlchemy 的 ORM 映射方案时出现错误。

另请参阅

peps.python.org/pep-0681/#the-dataclass-transform-decorator - 背景介绍了像 SQLAlchemy 这样的库如何启用 PEP 681 支持

数据类转换可以通过将 MappedAsDataclass mixin 添加到 DeclarativeBase 类层次结构中的任何声明性类来实现,或者通过使用 registry.mapped_as_dataclass() 类装饰器来进行装饰映射。

MappedAsDataclass mixin 可以应用于声明性的 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]

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

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]

类级别的功能配置

数据类功能的支持是部分的。目前支持的是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]

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

属性配置

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

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

  • init,如mapped_column.initrelationship.init,如果为 False,则表示该字段不应该是__init__()方法的一部分

  • default,如mapped_column.defaultrelationship.default,表示字段的默认值,可以作为关键字参数在__init__()方法中传递。

  • default_factory,如mapped_column.default_factoryrelationship.default_factory,表示一个可调用函数,如果未明确传递给__init__()方法,则会调用它生成新的默认值。

  • repr 默认为 True,表示该字段应该是生成的__repr__()方法的一部分

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

作为使用mapped_column()的示例,下面的映射将生成一个仅接受字段namefullname__init__()方法,其中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 参数之间的名称重叠,mapped_column() 构造通过添加一个新参数 mapped_column.insert_default 来消除这两个名称的歧义,该参数将直接填充到 Column.default 参数中,并且独立于在 mapped_column.default 上设置的内容,后者始终用于数据类配置。例如,配置一个带有 func.utc_timestamp() SQL 函数作为 Column.default 的日期时间列,但在构造函数中该参数是可选的:

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
    )

使用上述映射,对于未传递任何 created_at 参数的新 User 对象的 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 

与 Annotated 的集成

将整个列声明映射到 Python 类型 中介绍的方法演示了如何使用 PEP 593Annotated 对象来打包整个 mapped_column() 构造以便重用。该功能支持使用数据类特性。然而,该特性的一个方面在与类型工具一起使用时需要一种解决方法,即必须将 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()

使用混入和抽象超类

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 视为数据类的一部分。对于这种情况会发出警告,稍后将成为错误。

另请参见

转换为数据类时,属性来自非数据类的超类。 - 关于原因的背景

关系配置

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)

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

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

使用非映射数据类字段

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

一个更实际的例子可能是结合数据类的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注释的字段,这些字段将被视为生成的数据类的一部分,但不会被映射,无需指定__allow_unmapped__类属性。以前的 2.0 beta 版本将要求明确存在此属性,即使此属性的目的仅是允许旧版 ORM 类型映射继续运行。### 与诸如 Pydantic 等替代数据类提供者集成

警告

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

为了与 Pydantic 兼容,请考虑使用SQLModel ORM,该 ORM 基于 SQLAlchemy ORM 构建,使用 Pydantic,其中包括明确解决这些不兼容性的特殊实现细节。

SQLAlchemy 的MappedAsDataclass类和registry.mapped_as_dataclass()方法调用直接进入 Python 标准库 dataclasses.dataclass 类装饰器,经过类的声明性映射处理后。此函数调用可以通过MappedAsDataclass作为类关键字参数以及registry.mapped_as_dataclass()接受的dataclass_callable参数交换为备用 dataclasses 提供程序,例如 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 的函数。

类级特性配置

对 dataclasses 特性的支持是部分的。当前支持的特性包括 initrepreqorderunsafe_hash 特性,match_argskw_only 在 Python 3.10+ 上受支持。当前不支持的特性包括 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 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 等效:

  • init,比如mapped_column.initrelationship.init,如果为 False,表示该字段不应该是__init__()方法的一部分。

  • default,比如mapped_column.defaultrelationship.default,表示字段的默认值,作为__init__()方法的关键字参数传递。

  • default_factory,比如mapped_column.default_factoryrelationship.default_factory,表示一个可调用函数,将被调用以生成一个新的默认值,如果参数未明确传递给__init__()方法。

  • repr 默认为 True,表示该字段应该是生成的__repr__()方法的一部分。

与 dataclasses 的另一个关键区别是,属性的默认值必须使用 ORM 构造函数的 default 参数进行配置,例如 mapped_column(default=None)。不支持接受简单 Python 值作为默认值的类似 dataclass 语法,而不使用 @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 构造函数现有参数的名称重叠,mapped_column() 构造函数通过添加一个新参数 mapped_column.insert_default 来消除两个名称之间的歧义,该参数将直接填充到 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 对象的 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()

列默认值

为了适应 default 参数与 Column.default 构造函数的现有参数的名称重叠,mapped_column() 构造函数通过添加一个新参数 mapped_column.insert_default 来消除这两个名称之间的歧义,该参数将直接填充到 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
    )

在上述映射中,当未传递 created_at 参数的情况下,对新的 User 对象进行 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 

Annotated 的集成

在将整个列声明映射到 Python 类型中介绍的方法说明了如何使用PEP 593 Annotated对象来打包整个mapped_column()结构以供重用。该功能与数据类功能一起使用。然而,该功能的一个方面在使用类型工具时需要一个解决方法,即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()也必须出现在右侧,并在Annotated结构中包含对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()

使用混入和抽象超类

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

Python 类型检查器,支持PEP 681,否则将不考虑非数据类混入的属性作为数据类的一部分。

从版本 2.0.8 开始已弃用:在MappedAsDataclassregistry.mapped_as_dataclass()层次结构中使用混入和抽象基类,这些类本身不是数据类,这是不推荐的,因为这些字段不被PEP 681支持为数据类的一部分。对于这种情况会发出警告,以后会成为错误。

另请参阅

当将转换为数据类时,属性源自不是数据类的超类。 - 关于原因的背景

关系配置

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)

上述映射将在构造新的Parent()对象时为Parent.children生成一个空列表,当构造新的Child()对象时,如果不传递parent,则Child.parent将生成一个None值。

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

使用非映射数据类字段

当使用声明性数据类时,也可以在类上使用非映射字段,这些字段将成为数据类构造过程的一部分,但不会被映射。任何不使用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类装饰器中,声明性映射过程应用到类之后。此函数调用可以用 Pydantic 等替代数据类提供程序替换,使用MappedAsDataclass作为类关键字参数接受的dataclass_callable参数,以及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类将被应用为一个数据类,使用 Pydantic 的pydantic.dataclasses.dataclasses可调用。此过程对于映射类和扩展自MappedAsDataclass或直接应用了registry.mapped_as_dataclass()的混入类都可用。

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

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

遗留特性

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

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

  • 使用“带命令式表”的方法,要映射的表/列是使用分配给类的__table__属性的Table对象定义的;关系在__mapper_args__字典中定义。使用registry.mapped()装饰器映射类。下面是一个示例,在使用带命令式表的方式映射预先存在的数据类中。

  • 使用完整的“声明式”,将 Declarative 解释的指令(例如Columnrelationship())添加到dataclasses.field()构造函数的.metadata字典中,它们将被声明性过程使用。再次使用registry.mapped()装饰器映射类。请参见下面的示例,在使用声明式字段映射预先存在的数据类中。

  • 可以使用 registry.map_imperatively() 方法将“命令式”映射应用于现有数据类,以完全相同的方式生成映射,如命令式映射中所述。下面在 使用命令式映射预存在的数据类中说明。

通过 SQLAlchemy 将映射应用于数据类的一般过程与普通类的过程相同,但还包括 SQLAlchemy 将检测到的类级别属性,这些属性是数据类声明过程的一部分,并在运行时用通常的 SQLAlchemy ORM 映射属性替换它们。数据类生成的 __init__ 方法保持不变,所有其他由数据类生成的方法,例如 __eq__()__repr__() 等也是如此。

使用声明式与命令式表(即混合式声明式)映射预存在的数据类

以下是使用 @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 的构造函数。另一种方法在下一个示例中。

警告

声明一个 dataclass field() 设置一个 defaultinit=False 一起使用不会像完全普通的 dataclass 那样起作用,因为 SQLAlchemy 类的内部机制会用数据类创建过程中设置的默认值替换类上的默认值。使用 default_factory 代替。当使用 声明式 Dataclass 映射 时,此适应将自动完成。### 使用声明式字段映射现有数据类

旧版特性

应将此数据类与声明式映射一起使用的方法视为旧版。它将继续受到支持,但是不太可能提供与 声明式 Dataclass 映射 中详细说明的新方法相比的任何优势。

注意mapped_column()在这种用法下不受支持;应继续使用Column 构造函数来声明 metadata 字段中的表元数据。

完全声明式方法要求将 Column 对象声明为类属性,在使用 dataclasses 时会与 dataclass 级别的属性冲突。结合这些的一种方法是利用 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))})

使用声明式 Mixin 与现有数据类

在 使用 Mixins 构建映射层次结构 部分介绍了声明式 Mixin 类。声明式 Mixin 的一个要求是,某些无法轻松复制的构造必须作为可调用对象给出,使用 declared_attr 装饰器,例如在 混合关系 中的示例:

class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column("target_id", ForeignKey("target.id"))

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

通过在数据类field()对象中使用 lambda 来表示field()内的 SQLAlchemy 构造支持此形式。使用declared_attr()将 lambda 包围起来是可选的。如果我们想要生成上述的User类,其中 ORM 字段来自于一个自身是数据类的混合类,形式将是:

@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 中新增:对“已声明属性”样式的混合属性提供支持,即relationship()构造以及带有外键声明的Column对象,可用于“声明式表的数据类”样式的映射中。### 使用命令式映射映射预先存在的数据类

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

使用此映射样式时,请注意使用声明式带命令式表映射预先存在的数据类中提到的相同警告。### 使用声明式带命令式表映射预先存在的数据类

下面是使用 声明式和命令式表格(也称为混合声明式) 的 @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 仍然可以在 flush 期间从自增或其他默认值生成器获取它们的值后设置它们。为了允许在构造函数中显式指定它们,它们将被给定一个默认值None

要单独声明一个 relationship(),需要直接在 Mapper.properties 字典内指定它,该字典本身在 __mapper_args__ 字典内指定,以便将其传递给 Mapper 的构造函数。这种方法的替代方案在下一个示例中。

警告

使用 field() 声明一个数据类并设置 default 以及 init=False 不会像在完全普通的数据类中预期的那样起作用,因为 SQLAlchemy 类的装饰会替换数据类创建过程中在类上设置的默认值。使用 default_factory 代替。当使用声明式数据类映射时,这个适配会自动完成。

使用声明式字段映射现有数据类

遗留特性

这种使用数据类进行声明式映射的方法应被视为遗留。它仍然会得到支持,但不太可能在声明式数据类映射的新方法面前提供任何优势。

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

使用具有预先存在的数据类的声明式混合

在 使用混合组合映射层次结构 部分介绍了声明式 Mixin 类。声明式 mixins 的一个要求是,某些无法轻松复制的构造必须以可调用的形式给出,使用 declared_attr 装饰器,例如在 混合关系 中的示例:

class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column("target_id", ForeignKey("target.id"))

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

此形式在 Dataclasses 的 field() 对象中得到支持,通过使用 lambda 来指示 field() 内部的 SQLAlchemy 构造。在 lambda 周围使用 declared_attr() 是可选的。如果我们想要生成我们上面的 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 对象。 #### 使用具有预先存在的数据类的声明式混合

在 使用混合组合映射层次结构 部分介绍了声明式 Mixin 类。声明式 mixins 的一个要求是,某些无法轻松复制的构造必须以可调用的形式给出,使用 declared_attr 装饰器,例如在 混合关系 中的示例:

class RefTargetMixin:
    @declared_attr
    def target_id(cls):
        return Column("target_id", ForeignKey("target.id"))

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

在 Dataclasses field() 对象中支持此形式,通过使用 lambda 表示 SQLAlchemy 构造在 field() 内部。使用 declared_attr() 将 lambda 包围起来是可选的。如果我们想要生成上述的 User 类,其中 ORM 字段来自于一个自身是 dataclass 的 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

如前所述,通过使用 @dataclass 装饰器设置为 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)

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

将 ORM 映射应用到现有 attrs 类

attrs 库是一个流行的第三方库,提供了类似于 dataclasses 的功能,同时提供了许多在普通 dataclasses 中找不到的附加功能。

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

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

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

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

在“声明式与命令式表”风格中,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]]

注意

attrsslots=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)

上述形式等同于先前使用声明式与命令式表的示例。

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

在“声明式与命令式表”风格中,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]]

注意

attrsslots=True 选项可以在映射类上启用 __slots__,但在不完全实现替代的属性调试时,无法与 SQLAlchemy 映射一起使用,因为映射类通常依赖于直接访问 __dict__ 来存储状态。当存在此选项时,行为是未定义的。

使用命令式映射(Imperative Mapping)进行属性映射

就像在数据类中一样,我们可以利用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)

上述形式等同于使用命令式表格进行声明的上一个示例。

SQL 表达式作为映射属性

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

映射类上的属性可以链接到 SQL 表达式,这些表达式可以在查询中使用。

使用混合

将相对简单的 SQL 表达式链接到类的最简单和最灵活的方法是使用所谓的“混合属性”,在 混合属性 部分中描述。混合提供了一个同时在 Python 级别和 SQL 表达式级别工作的表达式。例如,我们将一个类 User,其中包含属性 firstnamelastname,映射到下面一个混合,该混合将为我们提供 fullname,即这两者的字符串连接:

from sqlalchemy.ext.hybrid import hybrid_property

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @hybrid_property
    def fullname(self):
        return self.firstname + " " + self.lastname

在上面,fullname 属性在实例和类级别都被解释,因此可以从一个实例中使用:

some_user = session.scalars(select(User).limit(1)).first()
print(some_user.fullname)

以及可在查询中使用:

some_user = session.scalars(
    select(User).where(User.fullname == "John Smith").limit(1)
).first()

字符串连接示例是一个简单的示例,其中 Python 表达式可以在实例和类级别上兼用。通常,必须区分 SQL 表达式和 Python 表达式,可以使用hybrid_property.expression()来实现。下面我们展示了在混合内部需要存在条件的情况,使用 Python 中的if语句和 SQL 表达式的case()构造:

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql import case

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @hybrid_property
    def fullname(self):
        if self.firstname is not None:
            return self.firstname + " " + self.lastname
        else:
            return self.lastname

    @fullname.expression
    def fullname(cls):
        return case(
            (cls.firstname != None, cls.firstname + " " + cls.lastname),
            else_=cls.lastname,
        )

使用 column_property

column_property() 函数可用于将 SQL 表达式映射到与常规映射的 Column 类似的方式。使用这种技术,属性在加载时与所有其他列映射的属性一起加载。这在某些情况下优于使用混合的用法,因为该值可以在对象的父行加载时一次性加载,特别是如果表达式是链接到其他表(通常作为相关子查询)以访问通常不会在已加载对象上可用的数据的情况。

使用column_property()来表示 SQL 表达式的缺点包括表达式必须与整个类所发出的 SELECT 语句兼容,以及在使用来自声明性混合的column_property()时可能会出现一些配置怪癖。

我们的“fullname”示例可以使用column_property()表示如下:

from sqlalchemy.orm import column_property

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))
    fullname = column_property(firstname + " " + lastname)

也可以使用相关子查询。 下面我们使用select()构造创建一个ScalarSelect,表示一个面向列的 SELECT 语句,将特定User的可用Address对象的计数链接在一起:

from sqlalchemy.orm import column_property
from sqlalchemy import select, func
from sqlalchemy import Column, Integer, String, ForeignKey

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    address_count = column_property(
        select(func.count(Address.id))
        .where(Address.user_id == id)
        .correlate_except(Address)
        .scalar_subquery()
    )

在上述示例中,我们定义了一个类似以下的ScalarSelect()构造:

stmt = (
    select(func.count(Address.id))
    .where(Address.user_id == id)
    .correlate_except(Address)
    .scalar_subquery()
)

首先,我们使用select()创建一个Select构造,然后使用Select.scalar_subquery()方法将其转换为标量子查询,表示我们打算在列表达式上下文中使用此Select语句。

Select本身中,我们选择计数Address.id行,其中Address.user_id列等于id,在User类的上下文中,id是名为idColumn(请注意,id也是 Python 内置函数的名称,这不是我们想在这里使用的 - 如果我们在User类定义之外,我们将使用User.id)。

Select.correlate_except()方法指示此select()中 FROM 子句的每个元素都可以从 FROM 列表中省略(即与针对User的封闭 SELECT 语句相关联),除了与Address对应的元素。 这并不是绝对必要的,但是在UserAddress表之间进行一长串连接的情况下,防止Address意外地从 FROM 列表中省略。

对于引用从多对多关系链接的列的column_property(),使用and_()将关联表的字段与关系中的两个表连接起来:

from sqlalchemy import and_

class Author(Base):
    # ...

    book_count = column_property(
        select(func.count(books.c.id))
        .where(
            and_(
                book_authors.c.author_id == authors.c.id,
                book_authors.c.book_id == books.c.id,
            )
        )
        .scalar_subquery()
    )

将 column_property()添加到现有的声明映射类

如果导入问题阻止在类中定义column_property(),则可以在两者配置后将其分配给类。当使用使用声明性基类(即由DeclarativeBase超类或遗留函数(例如declarative_base())生成的映射时,此属性分配的效果是在事后调用Mapper.add_property()以添加额外的属性:

# only works if a declarative base class is in use
User.address_count = column_property(
    select(func.count(Address.id)).where(Address.user_id == User.id).scalar_subquery()
)

当使用不使用声明性基类的映射样式,如registry.mapped()装饰器时,可以在底层Mapper对象上显式调用Mapper.add_property()方法,该对象可以使用inspect()获取:

from sqlalchemy.orm import registry

reg = registry()

@reg.mapped
class User:
    __tablename__ = "user"

    # ... additional mapping directives

# later ...

# works for any kind of mapping
from sqlalchemy import inspect

inspect(User).add_property(
    column_property(
        select(func.count(Address.id))
        .where(Address.user_id == User.id)
        .scalar_subquery()
    )
)

另请参阅

将附加列添加到现有的声明式映射类

在映射时从列属性组合

可以创建将多个ColumnProperty对象组合在一起的映射。当在核心表达式上下文中使用ColumnProperty时,它将被解释为 SQL 表达式,前提是它被现有的表达式对象所指向;这是通过核心检测到对象具有__clause_element__()方法并返回 SQL 表达式来实现的。然而,如果ColumnProperty作为表达式中的主对象使用,而没有其他核心 SQL 表达式对象来指向它,那么ColumnProperty.expression属性将返回底层 SQL 表达式,以便可以一致地用于构建 SQL 表达式。下面,File类包含一个属性File.path,它将一个字符串令牌连接到File.filename属性上,该属性本身是一个ColumnProperty

class File(Base):
    __tablename__ = "file"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(64))
    extension = mapped_column(String(8))
    filename = column_property(name + "." + extension)
    path = column_property("C:/" + filename.expression)

File类通常在表达式中使用时,分配给filenamepath的属性可以直接使用。仅当直接在映射定义中使用ColumnProperty时,才需要使用ColumnProperty.expression属性:

stmt = select(File.path).where(File.filename == "foo.txt")

使用 Column Deferral 与column_property()

在 ORM 查询指南中引入的列延迟特性可在映射时应用于由column_property()映射的 SQL 表达式,方法是在column_property()的位置使用deferred()函数而不是column_property()

from sqlalchemy.orm import deferred

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    firstname: Mapped[str] = mapped_column()
    lastname: Mapped[str] = mapped_column()
    fullname: Mapped[str] = deferred(firstname + " " + lastname)

参见

使用 deferred()用于命令式映射器,映射的 SQL 表达式

使用简单描述符

在需要发出比column_property()hybrid_property提供的 SQL 查询更复杂的情况下,可以使用作为属性访问的常规 Python 函数,假设表达式仅需要在已加载的实例上可用。该函数使用 Python 自己的@property装饰器装饰,将其标记为只读属性。在函数内部,使用object_session()定位到与当前对象对应的Session,然后用于发出查询:

from sqlalchemy.orm import object_session
from sqlalchemy import select, func

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @property
    def address_count(self):
        return object_session(self).scalar(
            select(func.count(Address.id)).where(Address.user_id == self.id)
        )

简单描述符方法在紧急情况下很有用,但在通常情况下比混合和列属性方法性能更低,因为它需要在每次访问时发出 SQL 查询。

映射属性中的查询时 SQL 表达式

除了能够在映射类上配置固定的 SQL 表达式之外,SQLAlchemy ORM 还包括一个功能,可以在查询时将对象加载为任意 SQL 表达式的结果,并将其设置为其状态的一部分。通过使用 query_expression() 配置 ORM 映射属性,然后在查询时使用 with_expression() 加载器选项来实现此行为。查看 将任意 SQL 表达式加载到对象上 中的示例映射和用法。

使用混合

将相对简单的 SQL 表达式链接到类的最简单和最灵活的方法是使用所谓的“混合属性”,在 混合属性 部分中描述。混合提供了一个在 Python 级别和 SQL 表达式级别都起作用的表达式。例如,下面我们映射一个类 User,包含属性 firstnamelastname,并包含一个混合,将为我们提供 fullname,即两者的字符串连接:

from sqlalchemy.ext.hybrid import hybrid_property

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @hybrid_property
    def fullname(self):
        return self.firstname + " " + self.lastname

在上面的示例中,fullname 属性在实例和类级别都被解释,因此可以从实例中使用:

some_user = session.scalars(select(User).limit(1)).first()
print(some_user.fullname)

以及可在查询中使用:

some_user = session.scalars(
    select(User).where(User.fullname == "John Smith").limit(1)
).first()

字符串拼接示例是一个简单的示例,其中 Python 表达式可以在实例和类级别上都起到双重作用。通常,必须区分 SQL 表达式和 Python 表达式,可以使用 hybrid_property.expression() 来实现。下面我们举例说明一个需要在混合中存在条件的情况,使用 Python 中的 if 语句和 SQL 表达式的 case() 结构:

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.sql import case

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @hybrid_property
    def fullname(self):
        if self.firstname is not None:
            return self.firstname + " " + self.lastname
        else:
            return self.lastname

    @fullname.expression
    def fullname(cls):
        return case(
            (cls.firstname != None, cls.firstname + " " + cls.lastname),
            else_=cls.lastname,
        )

使用 column_property

column_property() 函数可用于以与常规映射的 Column 类似的方式映射 SQL 表达式。通过此技术,属性在加载时与所有其他列映射的属性一起加载。在某些情况下,这比使用混合的优势更大,因为值可以在与对象的父行同时加载的同时前置加载,特别是如果表达式是链接到其他表的(通常作为关联子查询)以访问在已加载对象上通常不可用的数据。

使用column_property()进行 SQL 表达式的缺点包括表达式必须与整个类的 SELECT 语句兼容,并且在使用column_property()时可能会出现一些配置怪癖。

我们的“fullname”示例可以使用column_property()表示如下:

from sqlalchemy.orm import column_property

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))
    fullname = column_property(firstname + " " + lastname)

也可以使用相关子查询。下面我们使用select()构造创建一个ScalarSelect,表示一个面向列的 SELECT 语句,它链接了特定User的可用Address对象的计数:

from sqlalchemy.orm import column_property
from sqlalchemy import select, func
from sqlalchemy import Column, Integer, String, ForeignKey

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class Address(Base):
    __tablename__ = "address"
    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user.id"))

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    address_count = column_property(
        select(func.count(Address.id))
        .where(Address.user_id == id)
        .correlate_except(Address)
        .scalar_subquery()
    )

在上面的例子中,我们定义一个如下所示的ScalarSelect()构造:

stmt = (
    select(func.count(Address.id))
    .where(Address.user_id == id)
    .correlate_except(Address)
    .scalar_subquery()
)

首先,我们使用select()创建一个Select构造,然后使用Select.scalar_subquery()方法将其转换为标量子查询,表明我们打算在列表达式上下文中使用这个Select语句。

Select本身中,我们选择Address.id行的计数,其中Address.user_id列等于id,在User类的上下文中,id是名为idColumn(请注意,id也是 Python 内置函数的名称,这不是我们想要在此处使用的 - 如果我们在User类定义之外,我们将使用User.id)。

Select.correlate_except() 方法指示此 select() 的 FROM 子句中的每个元素都可以从 FROM 列表中省略(即与针对 User 的封闭 SELECT 语句相关联),除了与 Address 对应的元素。这并非绝对必要,但在 UserAddress 表之间的一长串联接中,防止了 Address 在 SELECT 语句嵌套中无意中被省略出 FROM 列表。

对于引用来自多对多关系的列的 column_property(),使用 and_() 来将关联表的字段连接到关系中的两个表:

from sqlalchemy import and_

class Author(Base):
    # ...

    book_count = column_property(
        select(func.count(books.c.id))
        .where(
            and_(
                book_authors.c.author_id == authors.c.id,
                book_authors.c.book_id == books.c.id,
            )
        )
        .scalar_subquery()
    )

向现有的声明式映射类添加 column_property()

如果导入问题阻止内联定义 column_property() 与类一起定义,则在两者配置后可以将其分配给类。当使用使用声明式基类(即由 DeclarativeBase 超类或遗留函数如 declarative_base() 生成的映射)时,此属性分配具有调用 Mapper.add_property() 的效果,以在事后添加附加属性。

# only works if a declarative base class is in use
User.address_count = column_property(
    select(func.count(Address.id)).where(Address.user_id == User.id).scalar_subquery()
)

当使用不使用声明式基类的映射样式时,例如 registry.mapped() 装饰器时,可以在底层的 Mapper 对象上显式调用 Mapper.add_property() 方法,可以使用 inspect() 获取该对象:

from sqlalchemy.orm import registry

reg = registry()

@reg.mapped
class User:
    __tablename__ = "user"

    # ... additional mapping directives

# later ...

# works for any kind of mapping
from sqlalchemy import inspect

inspect(User).add_property(
    column_property(
        select(func.count(Address.id))
        .where(Address.user_id == User.id)
        .scalar_subquery()
    )
)

另请参阅

向现有的声明式映射类添加额外的列

在映射时从列属性组成

可以创建结合多个 ColumnProperty 对象的映射。当在核心表达式上下文中使用时,ColumnProperty 将被解释为 SQL 表达式,前提是它被现有表达式对象所定位;这通过核心检测对象是否具有返回 SQL 表达式的 __clause_element__() 方法来完成。然而,如果在表达式中将 ColumnProperty 用作领导对象,而没有其他核心 SQL 表达式对象来定位它,那么 ColumnProperty.expression 属性将返回底层 SQL 表达式,以便可以一致地构建 SQL 表达式。下面,File 类包含一个属性 File.path,它将一个字符串标记连接到 File.filename 属性,后者本身就是一个 ColumnProperty

class File(Base):
    __tablename__ = "file"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(64))
    extension = mapped_column(String(8))
    filename = column_property(name + "." + extension)
    path = column_property("C:/" + filename.expression)

File 类在表达式中正常使用时,分配给 filenamepath 的属性可以直接使用。只有在映射定义中直接使用 ColumnProperty 时才需要使用 ColumnProperty.expression 属性:

stmt = select(File.path).where(File.filename == "foo.txt")

使用 column_property() 进行列延迟

ORM 查询指南中介绍的列延迟功能可在映射时应用到由 column_property() 映射的 SQL 表达式上,方法是使用 deferred() 函数代替 column_property()

from sqlalchemy.orm import deferred

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    firstname: Mapped[str] = mapped_column()
    lastname: Mapped[str] = mapped_column()
    fullname: Mapped[str] = deferred(firstname + " " + lastname)

另请参阅

使用 imperative mappers、映射的 SQL 表达式进行延迟加载

向现有的 Declarative 映射类添加 column_property()

如果导入问题阻止 column_property() 在类中内联定义,可以在两者配置后将其分配给类。在使用使用声明基类(即由 DeclarativeBase 超类或遗留函数如 declarative_base() 生成的)的映射时,此属性分配将调用 Mapper.add_property() 来添加一个额外的属性:

# only works if a declarative base class is in use
User.address_count = column_property(
    select(func.count(Address.id)).where(Address.user_id == User.id).scalar_subquery()
)

在使用不使用声明基类的映射样式,例如 registry.mapped() 装饰器时,可以显式调用底层的 Mapper.add_property() 方法,这可以通过 inspect() 获取底层的 Mapper 对象来实现:

from sqlalchemy.orm import registry

reg = registry()

@reg.mapped
class User:
    __tablename__ = "user"

    # ... additional mapping directives

# later ...

# works for any kind of mapping
from sqlalchemy import inspect

inspect(User).add_property(
    column_property(
        select(func.count(Address.id))
        .where(Address.user_id == User.id)
        .scalar_subquery()
    )
)

详见

向现有的声明映射类添加额外列

在映射时从列属性组成

可以创建将多个 ColumnProperty 对象组合在一起的映射。当在核心表达式上下文中使用时,如果 ColumnProperty 被现有表达式对象所定位,则它将被解释为 SQL 表达式;这是通过核心检测到对象具有返回 SQL 表达式的 __clause_element__() 方法来完成的。但是,如果在表达式中使用 ColumnProperty 作为主要对象,而没有其他核心 SQL 表达式对象来定位它,则 ColumnProperty.expression 属性将返回底层的 SQL 表达式,以便可以一致地构建 SQL 表达式。在下面的示例中,File 类包含一个属性 File.path,它将一个字符串令牌连接到 File.filename 属性上,后者本身是一个 ColumnProperty

class File(Base):
    __tablename__ = "file"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(64))
    extension = mapped_column(String(8))
    filename = column_property(name + "." + extension)
    path = column_property("C:/" + filename.expression)

File类在表达式中正常使用时,分配给filenamepath的属性可以直接使用。仅在映射定义中直接使用ColumnProperty时才需要使用ColumnProperty.expression属性:

stmt = select(File.path).where(File.filename == "foo.txt")

使用column_property()进行列延迟

在 ORM 查询指南中引入的列延迟功能可以在映射时应用于由column_property()映射的 SQL 表达式,方法是在映射定义中使用 deferred() 函数代替column_property()

from sqlalchemy.orm import deferred

class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    firstname: Mapped[str] = mapped_column()
    lastname: Mapped[str] = mapped_column()
    fullname: Mapped[str] = deferred(firstname + " " + lastname)

另请参阅

对于命令式映射器,映射 SQL 表达式使用 deferred()

使用简单描述符

在需要发出比column_property()hybrid_property提供的更复杂的 SQL 查询的情况下,可以使用作为属性访问的常规 Python 函数,假设表达式仅需要在已加载的实例上可用。该函数使用 Python 自己的 @property 装饰器来将其标记为只读属性。在函数内部,使用object_session()来定位与当前对象对应的Session,然后用于发出查询:

from sqlalchemy.orm import object_session
from sqlalchemy import select, func

class User(Base):
    __tablename__ = "user"
    id = mapped_column(Integer, primary_key=True)
    firstname = mapped_column(String(50))
    lastname = mapped_column(String(50))

    @property
    def address_count(self):
        return object_session(self).scalar(
            select(func.count(Address.id)).where(Address.user_id == self.id)
        )

简单描述符方法通常作为最后一手之计,但在通常情况下,它的性能不如混合和列属性方法,因为每次访问都需要发出一条 SQL 查询。

查询时 SQL 表达式作为映射属性

除了能够在映射类上配置固定的 SQL 表达式之外,SQLAlchemy ORM 还包括一个功能,即对象可以使用在查询时设置为其状态的任意 SQL 表达式的结果进行加载。通过使用 query_expression() 配置 ORM 映射属性,然后在查询时使用 with_expression() 加载选项来实现这种行为。有关示例映射和用法,请参阅 将任意 SQL 表达式加载到对象上。

更改属性行为

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

本节将讨论用于修改 ORM 映射属性行为的特性和技术,包括那些使用mapped_column()relationship()等映射的属性。

简单的验证器

一个快速添加“验证”程序到属性的方法是使用validates()装饰器。属性验证器可以引发异常,停止突变属性值的过程,或者可以将给定值更改为其他值。像所有属性扩展一样,验证器仅在普通用户代码中调用;在 ORM 填充对象时,它们不会被调用:

from sqlalchemy.orm import validates

class EmailAddress(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)

    @validates("email")
    def validate_email(self, key, address):
        if "@" not in address:
            raise ValueError("failed simple email validation")
        return address

当向集合添加项目时,验证器还会接收集合追加事件:

from sqlalchemy.orm import validates

class User(Base):
    # ...

    addresses = relationship("Address")

    @validates("addresses")
    def validate_address(self, key, address):
        if "@" not in address.email:
            raise ValueError("failed simplified email validation")
        return address

验证函数默认不会为集合移除事件发出,因为典型的期望是被丢弃的值不需要验证。然而,validates()支持通过向装饰器指定include_removes=True来接收这些事件。当设置了此标志时,验证函数必须接收一个额外的布尔参数,如果为True,则表示操作是一个移除:

from sqlalchemy.orm import validates

class User(Base):
    # ...

    addresses = relationship("Address")

    @validates("addresses", include_removes=True)
    def validate_address(self, key, address, is_remove):
        if is_remove:
            raise ValueError("not allowed to remove items from the collection")
        else:
            if "@" not in address.email:
                raise ValueError("failed simplified email validation")
            return address

通过反向引用链接的相互依赖验证器的情况也可以进行定制,使用include_backrefs=False选项;当设置为False时,此选项会阻止验证函数在由反向引用导致的事件发生时发出:

from sqlalchemy.orm import validates

class User(Base):
    # ...

    addresses = relationship("Address", backref="user")

    @validates("addresses", include_backrefs=False)
    def validate_address(self, key, address):
        if "@" not in address:
            raise ValueError("failed simplified email validation")
        return address

在上面的例子中,如果我们像这样分配到Address.user,如some_address.user = some_user,那么validate_address()函数将不会被发出,即使some_user.addresses中发生了追加 - 该事件是由反向引用引起的。

请注意,validates()装饰器是建立在属性事件之上的方便函数。需要更多控制属性更改行为配置的应用程序可以利用此系统,详见AttributeEvents

对象名称 描述
validates(*names, [include_removes, include_backrefs]) 将方法装饰为一个或多个命名属性的“验证器”。
function sqlalchemy.orm.validates(*names: str, include_removes: bool = False, include_backrefs: bool = True) → Callable[[_Fn], _Fn]

将方法装饰为一个或多个命名属性的“验证器”。

将方法指定为验证器,该方法接收属性的名称以及要分配的值,或者在集合的情况下,要添加到集合的值。然后,函数可以引发验证异常以阻止进程继续(在这种情况下,Python 的内置ValueErrorAssertionError异常是合理的选择),或者可以在继续之前修改或替换值。否则,该函数应返回给定的值。

注意,集合的验证器不能在验证过程中发出该集合的加载操作 - 这种用法会引发断言以避免递归溢出。这是一种不支持的可重入条件。

参数:

  • *names – 要验证的属性名称列表。

  • include_removes – 如果为 True,则也将发送“remove”事件 - 验证函数必须接受一个额外参数“is_remove”,其值为布尔值。

  • include_backrefs

    默认为True;如果为False,则验证函数不会在原始操作者是通过 backref 相关的属性事件时发出。这可用于双向validates()使用,其中每个属性操作只应发出一个验证器。

    从版本 2.0.16 开始更改:此参数在版本 2.0.0 到 2.0.15 中无意中默认为False。在 2.0.16 中恢复了其正确的默认值为True

另请参阅

简单验证器 - validates()的使用示例

在核心级别使用自定义数据类型

影响列值的非 ORM 方式,以适合在 Python 中的表示方式与在数据库中的表示方式之间转换数据,可以通过使用应用于映射的Table元数据的自定义数据类型来实现。这在一些编码/解码风格在数据进入数据库和返回时都会发生的情况下更为常见;在 Core 文档的扩充现有类型中了解更多信息。

使用描述符和混合体

影响属性的修改行为的更全面的方法是使用描述符。这在 Python 中通常使用property()函数。描述符的标准 SQLAlchemy 技术是创建一个普通的描述符,并从具有不同名称的映射属性读取/写入。下面我们使用 Python 2.6 风格的属性进行说明:

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    # name the attribute with an underscore,
    # different from the column name
    _email = mapped_column("email", String)

    # then create an ".email" attribute
    # to get/set "._email"
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        self._email = email

上述方法可以工作,但我们可以添加更多内容。虽然我们的 EmailAddress 对象将通过 email 描述符将值传递到 _email 映射属性中,但类级别的 EmailAddress.email 属性没有通常的表达式语义可用于 Select。为了提供这些,我们可以使用 hybrid 扩展,如下所示:

from sqlalchemy.ext.hybrid import hybrid_property

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    _email = mapped_column("email", String)

    @hybrid_property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        self._email = email

.email 属性除了在有 EmailAddress 实例时提供 getter/setter 行为外,还在类级别使用时提供 SQL 表达式,即直接从 EmailAddress 类中使用时:

from sqlalchemy.orm import Session
from sqlalchemy import select

session = Session()

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address@example.com")
).one()
SELECT  address.email  AS  address_email,  address.id  AS  address_id
FROM  address
WHERE  address.email  =  ?
('address@example.com',)
address.email = "otheraddress@example.com"
session.commit()
UPDATE  address  SET  email=?  WHERE  address.id  =  ?
('otheraddress@example.com',  1)
COMMIT 

hybrid_property 还允许我们更改属性的行为,包括在实例级别与类/表达式级别访问属性时定义不同的行为,使用 hybrid_property.expression() 修饰符。例如,如果我们想要自动添加主机名,我们可以定义两组字符串操作逻辑:

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    _email = mapped_column("email", String)

    @hybrid_property
    def email(self):
  """Return the value of _email up until the last twelve
 characters."""

        return self._email[:-12]

    @email.setter
    def email(self, email):
  """Set the value of _email, tacking on the twelve character
 value @example.com."""

        self._email = email + "@example.com"

    @email.expression
    def email(cls):
  """Produce a SQL expression that represents the value
 of the _email column, minus the last twelve characters."""

        return func.substr(cls._email, 0, func.length(cls._email) - 12)

在上面的例子中,访问 EmailAddress 实例的 email 属性将返回 _email 属性的值,从值中移除或添加主机名 @example.com。当我们针对 email 属性进行查询时,会呈现一个产生相同效果的 SQL 函数:

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address")
).one()
SELECT  address.email  AS  address_email,  address.id  AS  address_id
FROM  address
WHERE  substr(address.email,  ?,  length(address.email)  -  ?)  =  ?
(0,  12,  'address') 

阅读更多关于混合属性的信息请参阅 混合属性。 ## 同义词

同义词是一个映射器级别的构造,允许类上的任何属性“镜像”另一个映射的属性。

从最基本的角度来看,同义词是一种使某个属性通过额外的名称轻松可用的方式:

from sqlalchemy.orm import synonym

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    job_status = mapped_column(String(50))

    status = synonym("job_status")

上面的 MyClass 类具有两个属性,.job_status.status,它们将作为一个属性在表达式级别上行为一致:

>>> print(MyClass.job_status == "some_status")
my_table.job_status  =  :job_status_1
>>> print(MyClass.status == "some_status")
my_table.job_status  =  :job_status_1 

并在实例级别:

>>> m1 = MyClass(status="x")
>>> m1.status, m1.job_status
('x', 'x')

>>> m1.job_status = "y"
>>> m1.status, m1.job_status
('y', 'y')

synonym() 可用于任何类型的映射属性,包括映射列和关系,以及同义词本身,它们都是 MapperProperty 的子类。

除了简单的镜像外,synonym() 还可以被设置为引用用户定义的 描述符。我们可以用 @property 来提供我们的 status 同义词:

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    status = mapped_column(String(50))

    @property
    def job_status(self):
        return "Status: " + self.status

    job_status = synonym("status", descriptor=job_status)

在使用 Declarative 时,可以更简洁地使用 synonym_for() 装饰器表达上述模式:

from sqlalchemy.ext.declarative import synonym_for

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    status = mapped_column(String(50))

    @synonym_for("status")
    @property
    def job_status(self):
        return "Status: " + self.status

虽然 synonym() 对于简单的镜像很有用,但是使用描述符增强属性行为的用例更好地使用了现代用法中的 混合属性 功能,后者更加面向 Python 描述符。 从技术上讲,synonym() 可以做到与 hybrid_property 相同的所有事情,因为它还支持注入自定义 SQL 功能,但是混合属性在更复杂的情况下更容易使用。

对象名称 描述
同义词(name, *, [map_column, descriptor, comparator_factory, init, repr, default, default_factory, compare, kw_only, info, doc]) 将属性名称标记为映射属性的同义词,即属性将反映另一个属性的值和表达行为。
function sqlalchemy.orm.synonym(name: str, *, map_column: bool | None = None, descriptor: Any | None = None, comparator_factory: Type[PropComparator[_T]] | None = None, init: _NoArg | bool = _NoArg.NO_ARG, repr: _NoArg | bool = _NoArg.NO_ARG, default: _NoArg | _T = _NoArg.NO_ARG, default_factory: _NoArg | Callable[[], _T] = _NoArg.NO_ARG, compare: _NoArg | bool = _NoArg.NO_ARG, kw_only: _NoArg | bool = _NoArg.NO_ARG, info: _InfoType | None = None, doc: str | None = None) → Synonym[Any]

将属性名称标记为映射属性的同义词,即属性将反映另一个属性的值和表达行为。

例如:

class MyClass(Base):
    __tablename__ = 'my_table'

    id = Column(Integer, primary_key=True)
    job_status = Column(String(50))

    status = synonym("job_status")

参数:

  • name – 现有映射属性的名称。这可以是配置在类上的字符串名称 ORM 映射属性,包括列绑定属性和关系。

  • descriptor – 当在实例级别访问此属性时将用作 getter(和可能的 setter)的 Python descriptor。

  • map_column

    仅适用于经典映射和映射到现有 Table 对象的情况。 如果为 Truesynonym() 构造将定位到与此同义词的属性名称通常关联的映射表上的 Column 对象,并生成一个新的 ColumnProperty,将此 Column 映射到作为“name”参数给定的替代名称;这样,重新定义将 Column 的映射放在不同名称下的常规步骤是不必要的。这通常用于当 Column 要替换为也使用描述符的属性时,即与 synonym.descriptor 参数一起使用:

    my_table = Table(
        "my_table", metadata,
        Column('id', Integer, primary_key=True),
        Column('job_status', String(50))
    )
    
    class MyClass:
        @property
        def _job_status_descriptor(self):
            return "Status: %s" % self._job_status
    
    mapper(
        MyClass, my_table, properties={
            "job_status": synonym(
                "_job_status", map_column=True,
                descriptor=MyClass._job_status_descriptor)
        }
    )
    

    上述,名为 _job_status 的属性自动映射到 job_status 列:

    >>> j1 = MyClass()
    >>> j1._job_status = "employed"
    >>> j1.job_status
    Status: employed
    

    在使用 Declarative 时,为了在同义词中提供一个描述符,请使用sqlalchemy.ext.declarative.synonym_for()辅助程序。但是,请注意,通常应优先选择混合属性功能,特别是在重新定义属性行为时。

  • info – 将填充到此对象的InspectionAttr.info属性中的可选数据字典。

  • comparator_factory

    一个PropComparator的子类,将在 SQL 表达式级别提供自定义比较行为。

    注意

    对于提供重新定义属性的 Python 级别和 SQL 表达式级别行为的用例,请参考使用描述符和混合属性中介绍的混合属性,以获得更有效的技术。

另请参阅

同义词 - 同义词概述

synonym_for() - 面向 Declarative 的辅助程序

使用描述符和混合属性 - 混合属性扩展提供了一种更新的方法,比使用同义词更灵活地增强属性行为。## 操作符定制

SQLAlchemy ORM 和 Core 表达式语言使用的“操作符”是完全可定制的。例如,比较表达式User.name == 'ed'使用了 Python 本身内置的名为operator.eq的操作符 - SQLAlchemy 将与此类操作符关联的实际 SQL 构造可以进行修改。新操作也可以与列表达式关联。列表达式发生的操作符最直接在类型级别重新定义 - 请参阅 Redefining and Creating New Operators 部分进行描述。

ORM 级别的函数,如column_property()relationship()composite()还提供了在 ORM 级别重新定义操作符的功能,通过将PropComparator子类传递给每个函数的comparator_factory参数。在这个级别上定制操作符是一个罕见的用例。请参阅PropComparator的文档以获取概述。## 简单验证器

将“验证”程序快速添加到属性的一种方法是使用 validates() 装饰器。属性验证器可以引发异常,从而停止变异属性值的过程,或者可以将给定值更改为其他内容。验证器,如所有属性扩展一样,仅在正常用户代码中调用;当 ORM 正在填充对象时,不会发出它们:

from sqlalchemy.orm import validates

class EmailAddress(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    email = mapped_column(String)

    @validates("email")
    def validate_email(self, key, address):
        if "@" not in address:
            raise ValueError("failed simple email validation")
        return address

当项目被添加到集合时,验证器也会收到集合追加事件:

from sqlalchemy.orm import validates

class User(Base):
    # ...

    addresses = relationship("Address")

    @validates("addresses")
    def validate_address(self, key, address):
        if "@" not in address.email:
            raise ValueError("failed simplified email validation")
        return address

默认情况下,验证函数不会为集合删除事件发出,因为典型的期望是被丢弃的值不需要验证。但是,validates() 通过将 include_removes=True 指定给装饰器来支持接收这些事件。当设置了此标志时,验证函数必须接收一个额外的布尔参数,如果为 True,则表示该操作是一个删除操作:

from sqlalchemy.orm import validates

class User(Base):
    # ...

    addresses = relationship("Address")

    @validates("addresses", include_removes=True)
    def validate_address(self, key, address, is_remove):
        if is_remove:
            raise ValueError("not allowed to remove items from the collection")
        else:
            if "@" not in address.email:
                raise ValueError("failed simplified email validation")
            return address

通过使用 include_backrefs=False 选项,还可以针对通过反向引用链接的相互依赖验证器的情况进行定制;当设置为 False 时,该选项将阻止验证函数在事件发生时由于反向引用而发出:

from sqlalchemy.orm import validates

class User(Base):
    # ...

    addresses = relationship("Address", backref="user")

    @validates("addresses", include_backrefs=False)
    def validate_address(self, key, address):
        if "@" not in address:
            raise ValueError("failed simplified email validation")
        return address

在上面的例子中,如果我们像这样分配给 Address.usersome_address.user = some_user,即使向 some_user.addresses 追加了一个元素,也不会触发 validate_address() 函数 - 事件是由一个反向引用引起的。

请注意,validates() 装饰器是在属性事件之上构建的一个方便函数。需要更多控制属性更改行为配置的应用程序可以使用此系统,该系统在 AttributeEvents 中描述。

对象名称 描述
验证(*names, [include_removes, include_backrefs]) 将方法装饰为一个或多个命名属性的“验证器”。
function sqlalchemy.orm.validates(*names: str, include_removes: bool = False, include_backrefs: bool = True) → Callable[[_Fn], _Fn]

将方法装饰为一个或多个命名属性的“验证器”。

指定一个方法作为验证器,该方法接收属性的名称以及要分配的值,或者在集合的情况下,要添加到集合的值。该函数然后可以引发验证异常以阻止继续处理过程(在这种情况下,Python 的内置ValueErrorAssertionError异常是合理的选择),或者可以修改或替换值然后继续。该函数否则应返回给定的值。

请注意,集合的验证器不能在验证例程中发出该集合的加载 - 这种用法会引发一个断言以避免递归溢出。这是一个不支持的可重入条件。

参数:

  • *names – 要验证的属性名称列表。

  • include_removes – 如果为 True,则“remove”事件也将发送 - 验证函数必须接受一个额外的参数“is_remove”,它将是一个布尔值。

  • include_backrefs

    默认为True;如果为False,则验证函数在原始生成器是通过 backref 相关的属性事件时不会发出。这可用于双向 validates() 用法,其中每个属性操作只应发出一个验证器。

    从版本 2.0.16 开始更改:此参数意外地在 2.0.0 至 2.0.15 版本中默认为 False。在 2.0.16 版本中恢复了其正确的默认值为True

另请参阅

简单验证器 - validates() 的用法示例

在核心级别使用自定义数据类型

通过使用应用于映射的 Table 元数据的自定义数据类型,可以以适合在 Python 中的表示方式与在数据库中的表示方式之间转换数据的方式来影响列的值的非 ORM 方法。这在某些编码/解码样式在数据进入数据库和返回时都发生的情况下更为常见;在核心文档中阅读更多关于此的内容,参见扩充现有类型。

使用描述符和混合类型

产生修改后的属性行为的更全面的方法是使用描述符。在 Python 中,通常使用 property() 函数来使用这些。描述符的标准 SQLAlchemy 技术是创建一个普通描述符,并从具有不同名称的映射属性读取/写入。下面我们使用 Python 2.6 风格的属性来说明这一点:

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    # name the attribute with an underscore,
    # different from the column name
    _email = mapped_column("email", String)

    # then create an ".email" attribute
    # to get/set "._email"
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        self._email = email

上述方法可行,但我们还可以添加更多内容。虽然我们的EmailAddress对象将值通过email描述符传递到_email映射属性中,但类级别的EmailAddress.email属性不具有通常可用于Select的表达语义。为了提供这些功能,我们使用 hybrid 扩展,如下所示:

from sqlalchemy.ext.hybrid import hybrid_property

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    _email = mapped_column("email", String)

    @hybrid_property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        self._email = email

.email 属性除了在我们有EmailAddress实例时提供 getter/setter 行为外,在类级别使用时也提供了一个 SQL 表达式,即直接从EmailAddress类中:

from sqlalchemy.orm import Session
from sqlalchemy import select

session = Session()

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address@example.com")
).one()
SELECT  address.email  AS  address_email,  address.id  AS  address_id
FROM  address
WHERE  address.email  =  ?
('address@example.com',)
address.email = "otheraddress@example.com"
session.commit()
UPDATE  address  SET  email=?  WHERE  address.id  =  ?
('otheraddress@example.com',  1)
COMMIT 

hybrid_property还允许我们更改属性的行为,包括在实例级别与类/表达式级别访问属性时定义不同的行为,使用hybrid_property.expression()修饰符。例如,如果我们想要自动添加主机名,我们可能会定义两组字符串操作逻辑:

class EmailAddress(Base):
    __tablename__ = "email_address"

    id = mapped_column(Integer, primary_key=True)

    _email = mapped_column("email", String)

    @hybrid_property
    def email(self):
  """Return the value of _email up until the last twelve
 characters."""

        return self._email[:-12]

    @email.setter
    def email(self, email):
  """Set the value of _email, tacking on the twelve character
 value @example.com."""

        self._email = email + "@example.com"

    @email.expression
    def email(cls):
  """Produce a SQL expression that represents the value
 of the _email column, minus the last twelve characters."""

        return func.substr(cls._email, 0, func.length(cls._email) - 12)

以上,访问EmailAddress实例的email属性将返回_email属性的值,从值中删除或添加主机名@example.com。当我们针对email属性进行查询时,将呈现出一个产生相同效果的 SQL 函数:

address = session.scalars(
    select(EmailAddress).where(EmailAddress.email == "address")
).one()
SELECT  address.email  AS  address_email,  address.id  AS  address_id
FROM  address
WHERE  substr(address.email,  ?,  length(address.email)  -  ?)  =  ?
(0,  12,  'address') 

在混合属性中阅读更多内容。

同义词

同义词是一个映射级别的构造,允许类上的任何属性“镜像”另一个被映射的属性。

从最基本的意义上讲,同义词是一种简单的方式,通过额外的名称使某个属性可用:

from sqlalchemy.orm import synonym

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    job_status = mapped_column(String(50))

    status = synonym("job_status")

上述MyClass类有两个属性,.job_status.status,它们将作为一个属性行为,无论在表达式级别还是在实例级别:

>>> print(MyClass.job_status == "some_status")
my_table.job_status  =  :job_status_1
>>> print(MyClass.status == "some_status")
my_table.job_status  =  :job_status_1 

在实例级别上:

>>> m1 = MyClass(status="x")
>>> m1.status, m1.job_status
('x', 'x')

>>> m1.job_status = "y"
>>> m1.status, m1.job_status
('y', 'y')

synonym()可以用于任何一种映射属性,包括映射列和关系,以及同义词本身,这些属性都是MapperProperty的子类。

除了简单的镜像之外,synonym()还可以引用用户定义的描述符。我们可以用@property来提供我们的status同义词:

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    status = mapped_column(String(50))

    @property
    def job_status(self):
        return "Status: " + self.status

    job_status = synonym("status", descriptor=job_status)

在使用声明性时,可以使用synonym_for()装饰器更简洁地表达上述模式:

from sqlalchemy.ext.declarative import synonym_for

class MyClass(Base):
    __tablename__ = "my_table"

    id = mapped_column(Integer, primary_key=True)
    status = mapped_column(String(50))

    @synonym_for("status")
    @property
    def job_status(self):
        return "Status: " + self.status

虽然synonym()对于简单的镜像很有用,但是使用描述符增强属性行为的用例更好地在现代使用中使用混合属性特性来处理,后者更加面向 Python 描述符。从技术上讲,一个synonym()可以做任何一个hybrid_property能做的事情,因为它也支持注入自定义 SQL 功能,但是在更复杂的情况下混合属性更容易使用。

对象名称 描述
synonym(name, *, [map_column, descriptor, comparator_factory, init, repr, default, default_factory, compare, kw_only, info, doc]) 将一个属性名表示为映射属性的同义词,即该属性将反映另一个属性的值和表达式行为。
function sqlalchemy.orm.synonym(name: str, *, map_column: bool | None = None, descriptor: Any | None = None, comparator_factory: Type[PropComparator[_T]] | None = None, init: _NoArg | bool = _NoArg.NO_ARG, repr: _NoArg | bool = _NoArg.NO_ARG, default: _NoArg | _T = _NoArg.NO_ARG, default_factory: _NoArg | Callable[[], _T] = _NoArg.NO_ARG, compare: _NoArg | bool = _NoArg.NO_ARG, kw_only: _NoArg | bool = _NoArg.NO_ARG, info: _InfoType | None = None, doc: str | None = None) → Synonym[Any]

将一个属性名表示为映射属性的同义词,即该属性将反映另一个属性的值和表达式行为。

例如:

class MyClass(Base):
    __tablename__ = 'my_table'

    id = Column(Integer, primary_key=True)
    job_status = Column(String(50))

    status = synonym("job_status")

参数:

  • name – 现有映射属性的名称。这可以引用在类上配置的 ORM 映射属性的字符串名称,包括列绑定属性和关系。

  • descriptor – 一个 Python 描述符,当访问此属性时将用作 getter(和可能的 setter)。

  • map_column

    仅适用于传统映射和对现有表对象的映射。如果为True,则synonym()构造将定位到在此同义词的属性名称通常与该同义词的属性名称相关联的映射表上的Column对象,并生成一个新的ColumnProperty,该属性将此Column映射到作为同义词的“name”参数给定的替代名称;通过这种方式,重新定义Column的映射为不同名称的步骤是不必要的。这通常用于当Column要被替换为也使用描述符的属性时,也就是与synonym.descriptor参数结合使用时:

    my_table = Table(
        "my_table", metadata,
        Column('id', Integer, primary_key=True),
        Column('job_status', String(50))
    )
    
    class MyClass:
        @property
        def _job_status_descriptor(self):
            return "Status: %s" % self._job_status
    
    mapper(
        MyClass, my_table, properties={
            "job_status": synonym(
                "_job_status", map_column=True,
                descriptor=MyClass._job_status_descriptor)
        }
    )
    

    在上面的例子中,名为_job_status的属性会自动映射到job_status列:

    >>> j1 = MyClass()
    >>> j1._job_status = "employed"
    >>> j1.job_status
    Status: employed
    

    当使用声明式时,为了与同义词结合使用提供描述符,请使用sqlalchemy.ext.declarative.synonym_for()助手。但是,请注意,通常应优选混合属性功能,特别是在重新定义属性行为时。

  • info – 可选的数据字典,将填充到此对象的InspectionAttr.info属性中。

  • comparator_factory

    PropComparator的子类,将在 SQL 表达式级别提供自定义比较行为。

    注意

    对于提供重新定义属性的 Python 级别和 SQL 表达式级别行为的用例,请参阅使用描述符和混合中介绍的混合属性,这是一种更有效的技术。

另请参阅

同义词 - 同义词概述

synonym_for() - 一种面向声明式的辅助工具

使用描述符和混合 - 混合属性扩展提供了一种更新的方法,可以更灵活地增强属性行为,比同义词更有效。

运算符定制

SQLAlchemy ORM 和 Core 表达式语言使用的“运算符”是完全可定制的。例如,比较表达式 User.name == 'ed' 使用了 Python 本身内置的名为 operator.eq 的运算符 - SQLAlchemy 关联的实际 SQL 构造可以被修改。新的操作也可以与列表达式关联起来。最直接重新定义列表达式的运算符的方法是在类型级别进行 - 详细信息请参阅重新定义和创建新的运算符。

ORM 级别的函数如column_property()relationship()composite()还提供了在 ORM 级别重新定义运算符的功能,方法是将PropComparator子类传递给每个函数的comparator_factory参数。在这个级别定制运算符的情况很少见。详细信息请参阅PropComparator的文档概述。

posted @ 2024-06-22 11:38  绝不原创的飞龙  阅读(37)  评论(0编辑  收藏  举报