SqlAlchemy 2.0 中文文档(六)(2)https://developer.aliyun.com/article/1560700
使用混合类组合映射层次结构
在使用 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
,它包括两个混合类CommonMixin
和HasLogRecord
,以及一个补充类LogRecord
,该类也包括CommonMixin
,演示了在混合类和基类上支持的各种构造,包括:
- 使用
mapped_column()
、Mapped
或Column
声明的列将从混合类或基类复制到要映射的目标类;上面通过列属性CommonMixin.id
和HasLogRecord.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
没有定义 CommonMixin
或 HasLogRecord
定义的任何变量,即 __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()
、Mapped
和Column
。
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
方法提供声明性混合类,从而消除了复制关系及其可能绑定到列的内容时可能出现的任何歧义。下面是一个示例,其中结合了外键列和关系,以便两个类Foo
和Bar
都可以通过多对一引用到一个公共目标类:
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)
使用上面的映射,Foo
和Bar
中的每一个都包含一个访问.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,它引用了Target
和cls
上的待定映射列:
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()
结合继承的Table
和Mapper
参数
使用 mixin 的一个常见方法是创建一个 def __tablename__(cls)
函数,动态生成映射的 Table
名称。
这个方法可以用于生成继承映射层次结构中的表名称,就像下面的示例一样,该示例创建一个 mixin,根据类名给每个类生成一个简单的表名称。下面的示例说明了如何为 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
mixin 类的子类,该类生成新的表名称,因此它们都将具有生成的 __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()
。此函数将返回 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"}
SqlAlchemy 2.0 中文文档(六)(4)https://developer.aliyun.com/article/1560707