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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: SqlAlchemy 2.0 中文文档(六)

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


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

在处理映射类继承层次结构中所记录的映射继承模式时,使用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)
)

用命名约定,如配置约束命名约定中所述。虽然命名约定在创建新的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)
)
相关文章
|
5月前
|
测试技术 API 数据库
SqlAlchemy 2.0 中文文档(九)(5)
SqlAlchemy 2.0 中文文档(九)
28 0
|
5月前
|
存储 SQL API
SqlAlchemy 2.0 中文文档(四)(5)
SqlAlchemy 2.0 中文文档(四)
36 3
|
5月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(四)(3)
SqlAlchemy 2.0 中文文档(四)
49 3
|
5月前
|
SQL API 数据库
SqlAlchemy 2.0 中文文档(一)(5)
SqlAlchemy 2.0 中文文档(一)
116 1
|
5月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(三)(2)
SqlAlchemy 2.0 中文文档(三)
35 1
|
5月前
|
SQL API 数据库
SqlAlchemy 2.0 中文文档(四)(2)
SqlAlchemy 2.0 中文文档(四)
49 1
|
5月前
|
SQL 数据库 Python
SqlAlchemy 2.0 中文文档(十)(3)
SqlAlchemy 2.0 中文文档(十)
36 1
|
5月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(三)(1)
SqlAlchemy 2.0 中文文档(三)
45 1
|
5月前
|
存储 Python
SqlAlchemy 2.0 中文文档(七)(5)
SqlAlchemy 2.0 中文文档(七)
26 1
|
5月前
|
SQL 测试技术 数据库
SqlAlchemy 2.0 中文文档(三)(4)
SqlAlchemy 2.0 中文文档(三)
28 0