SqlAlchemy 2.0 中文文档(六)(3)

简介: SqlAlchemy 2.0 中文文档(六)

SqlAlchemy 2.0 中文文档(六)(2)https://developer.aliyun.com/article/1560700


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

原文: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"}


SqlAlchemy 2.0 中文文档(六)(4)https://developer.aliyun.com/article/1560707

相关文章
|
5月前
|
SQL 前端开发 数据库
SqlAlchemy 2.0 中文文档(六)(1)
SqlAlchemy 2.0 中文文档(六)
54 0
|
5月前
|
测试技术 API 数据库
SqlAlchemy 2.0 中文文档(九)(5)
SqlAlchemy 2.0 中文文档(九)
31 0
|
5月前
|
SQL JSON 关系型数据库
SqlAlchemy 2.0 中文文档(五)(5)
SqlAlchemy 2.0 中文文档(五)
53 4
|
5月前
|
存储 SQL API
SqlAlchemy 2.0 中文文档(四)(5)
SqlAlchemy 2.0 中文文档(四)
36 3
|
5月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(四)(3)
SqlAlchemy 2.0 中文文档(四)
51 3
|
5月前
|
SQL 存储 数据库
SqlAlchemy 2.0 中文文档(一)(3)
SqlAlchemy 2.0 中文文档(一)
77 1
|
5月前
|
SQL 数据库 数据库管理
SqlAlchemy 2.0 中文文档(一)(2)
SqlAlchemy 2.0 中文文档(一)
134 1
|
5月前
|
SQL API 数据库
SqlAlchemy 2.0 中文文档(四)(1)
SqlAlchemy 2.0 中文文档(四)
41 1
|
5月前
|
SQL API 数据库
SqlAlchemy 2.0 中文文档(四)(2)
SqlAlchemy 2.0 中文文档(四)
49 1
|
5月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(三)(2)
SqlAlchemy 2.0 中文文档(三)
37 1