映射类继承层次结构

SQLAlchemy 支持三种形式的继承

  • 单表继承 – 几种类型的类由单个表表示;

  • 具体表继承 – 每种类型的类由独立的表表示;

  • 连接表继承 – 类层次结构在依赖表之间分解。每个类由其自己的表表示,该表仅包含该类本地的属性。

最常见的继承形式是单表继承和连接表继承,而具体继承则提出了更多的配置挑战。

当在继承关系中配置映射器时,SQLAlchemy 能够多态地加载元素,这意味着单个查询可以返回多种类型的对象。

另请参阅

为继承映射编写 SELECT 语句 - 在 ORM 查询指南

继承映射示例 - 连接继承、单继承和具体继承的完整示例

连接表继承

在连接表继承中,类层次结构中的每个类都由一个不同的表表示。查询层次结构中的特定子类将呈现为沿其继承路径中所有表的 SQL JOIN。如果查询的类是基类,则改为查询基表,可以选择同时包含其他表,或者允许稍后加载特定于子表的属性。

在所有情况下,给定行的最终实例化类都由基类上定义的鉴别器列或 SQL 表达式确定,这将产生与特定子类关联的标量值。

连接继承层次结构中的基类配置有额外的参数,这些参数将指示多态鉴别器列,以及基类本身的可选多态标识符

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


class Base(DeclarativeBase):
    pass


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }

    def __repr__(self):
        return f"{self.__class__.__name__}({self.name!r})"

在上面的示例中,鉴别器是 type 列,无论哪个是使用 Mapper.polymorphic_on 参数配置的。此参数接受面向列的表达式,可以指定为要使用的映射属性的字符串名称,也可以指定为列表达式对象,例如 Columnmapped_column() 构造。

鉴别器列将存储一个值,该值指示行中表示的对象类型。该列可以是任何数据类型,尽管字符串和整数是最常见的。要应用于数据库中特定行的此列的实际数据值是使用 Mapper.polymorphic_identity 参数指定的,如下所述。

虽然多态鉴别器表达式不是严格必要的,但如果需要多态加载,则它是必需的。在基表上建立列是实现此目的的最简单方法,但是非常复杂的继承映射可以使用 SQL 表达式(例如 CASE 表达式)作为多态鉴别器。

注意

目前,整个继承层次结构只能配置一个鉴别器列或 SQL 表达式,通常在层次结构中最底层的类上。“级联”多态鉴别器表达式尚不支持。

接下来,我们定义 EmployeeEngineerManager 子类。每个子类都包含表示其表示的子类唯一属性的列。每个表还必须包含主键列(或列)以及对外键的父表引用

class Engineer(Employee):
    __tablename__ = "engineer"
    id: Mapped[int] = mapped_column(ForeignKey("employee.id"), primary_key=True)
    engineer_name: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }


class Manager(Employee):
    __tablename__ = "manager"
    id: Mapped[int] = mapped_column(ForeignKey("employee.id"), primary_key=True)
    manager_name: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

在上面的示例中,每个映射都在其映射器参数中指定了 Mapper.polymorphic_identity 参数。此值填充由在基映射器上建立的 Mapper.polymorphic_on 参数指定的列。Mapper.polymorphic_identity 参数对于整个层次结构中的每个映射类都应该是唯一的,并且每个映射类只能有一个“标识”;如上所述,不支持某些子类引入第二个标识的“级联”标识。

ORM 使用 Mapper.polymorphic_identity 设置的值来确定行在多态加载时属于哪个类。在上面的示例中,表示 Employee 的每一行在其 type 列中都将具有值 'employee';类似地,每个 Engineer 将获得值 'engineer',每个 Manager 将获得值 'manager'。无论继承映射是像连接表继承那样为子类使用不同的连接表,还是像单表继承那样使用一个表,都应持久化此值并在查询时可供 ORM 使用。Mapper.polymorphic_identity 参数也适用于具体表继承,但实际上并未持久化;有关详细信息,请参阅后面的 具体表继承 部分。

在多态设置中,最常见的情况是外键约束建立在与主键本身相同的列或列上,但这并非必需;与主键不同的列也可以通过外键引用父级。从基表到子类的 JOIN 的构造方式也是直接可自定义的,但这很少是必要的。

完成连接继承映射后,针对 Employee 的查询将返回 EmployeeEngineerManager 对象的组合。新保存的 EngineerManagerEmployee 对象将自动使用正确的“鉴别器”值(在本例中为 "engineer""manager""employee",具体取决于情况)填充 employee.type 列。

与连接继承的关系

连接表继承完全支持关系。涉及连接继承类的关系应以层次结构中也对应于外键约束的类为目标;如下所示,由于 employee 表具有返回 company 表的外键约束,因此在 CompanyEmployee 之间建立了关系

from __future__ import annotations

from sqlalchemy.orm import relationship


class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    employees: Mapped[List[Employee]] = relationship(back_populates="company")


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]
    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }


class Manager(Employee): ...


class Engineer(Employee): ...

如果外键约束位于对应于子类的表上,则关系应改为以该子类为目标。在下面的示例中,从 managercompany 存在外键约束,因此关系在 ManagerCompany 类之间建立

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    managers: Mapped[List[Manager]] = relationship(back_populates="company")


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }


class Manager(Employee):
    __tablename__ = "manager"
    id: Mapped[int] = mapped_column(ForeignKey("employee.id"), primary_key=True)
    manager_name: Mapped[str]

    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="managers")

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }


class Engineer(Employee): ...

上面,Manager 类将具有 Manager.company 属性;Company 将具有 Company.managers 属性,该属性始终针对 employee 表和 manager 表的连接一起加载。

加载连接继承映射

有关继承加载技术的背景信息,包括在映射器配置时以及查询时要查询的表的配置,请参阅 为继承映射编写 SELECT 语句 部分。

单表继承

单表继承表示单个表中所有子类的所有属性。具有特定于该类的属性的特定子类将它们持久化在表中的列中,如果该行引用不同类型的对象,则这些列在其他情况下为 NULL。

查询层次结构中的特定子类将呈现为针对基表的 SELECT,其中将包含一个 WHERE 子句,该子句将行限制为那些在鉴别器列或表达式中具有特定值或值的行。

与连接表继承相比,单表继承具有简单性的优势;查询效率更高,因为只需要涉及一个表即可加载每个表示类的对象。

单表继承配置看起来很像连接表继承,不同之处在于只有基类指定 __tablename__。基表上还需要一个鉴别器列,以便可以将类彼此区分开。

即使子类共享基表的所有属性,当使用声明式时,也可以在子类上指定 mapped_column 对象,指示该列仅映射到该子类;mapped_column 将应用于相同的基本 Table 对象

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "employee",
    }


class Manager(Employee):
    manager_data: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }


class Engineer(Employee):
    engineer_info: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

请注意,派生类 Manager 和 Engineer 的映射器省略了 __tablename__,表明它们没有自己的映射表。此外,还包括带有 nullable=Truemapped_column() 指令;由于为这些类声明的 Python 类型不包括 Optional[],因此该列通常将映射为 NOT NULL,这将是不合适的,因为此列仅期望为对应于该特定子类的那些行填充。

使用 use_existing_column 解决列冲突

请注意,在上一节中,manager_nameengineer_info 列“向上移动”以应用于 Employee.__table__,这是由于它们在没有自己表的子类上的声明造成的。当两个子类想要指定相同的列时,会出现一个棘手的情况,如下所示

from datetime import datetime


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "employee",
    }


class Engineer(Employee):
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }
    start_date: Mapped[datetime] = mapped_column(nullable=True)


class Manager(Employee):
    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }
    start_date: Mapped[datetime] = mapped_column(nullable=True)

上面,在 EngineerManager 上声明的 start_date 列都将导致错误

sqlalchemy.exc.ArgumentError: Column 'start_date' on class Manager conflicts
with existing column 'employee.start_date'.  If using Declarative,
consider using the use_existing_column parameter of mapped_column() to
resolve conflicts.

上述场景向声明式映射系统提出了歧义,可以通过在 mapped_column() 上使用 mapped_column.use_existing_column 参数来解决,该参数指示 mapped_column() 在继承的超类中查找并使用已映射的列(如果已存在),否则映射新列

from sqlalchemy import DateTime


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
        "polymorphic_identity": "employee",
    }


class Engineer(Employee):
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

    start_date: Mapped[datetime] = mapped_column(
        nullable=True, use_existing_column=True
    )


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

    start_date: Mapped[datetime] = mapped_column(
        nullable=True, use_existing_column=True
    )

上面,当映射 Manager 时,start_date 列已存在于 Employee 类上,它已由 Engineer 映射提供。mapped_column.use_existing_column 参数指示 mapped_column() 应首先在 Employee 的映射 Table 上查找请求的 Column,如果存在,则保持现有映射。如果不存在,mapped_column() 将正常映射列,将其添加为 Employee 超类引用的 Table 中的列之一。

2.0.0b4 版本新增: - 添加了 mapped_column.use_existing_column,它提供了一种与 2.0 兼容的方式来有条件地映射继承子类上的列。先前的方法将 declared_attr 与父 .__table__ 上的查找结合使用,也可以继续工作,但缺少 PEP 484 类型提示支持。

mixin 类(请参阅 使用 Mixin 组合映射层次结构)可以使用类似的概念,从可重用的 mixin 类定义一系列特定的列和/或其他映射属性

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": type,
        "polymorphic_identity": "employee",
    }


class HasStartDate:
    start_date: Mapped[datetime] = mapped_column(
        nullable=True, use_existing_column=True
    )


class Engineer(HasStartDate, Employee):
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }


class Manager(HasStartDate, Employee):
    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }

与单表继承的关系

单表继承完全支持关系。配置方式与连接继承相同;外键属性应位于关系的“外键”侧的同一类上

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    employees: Mapped[List[Employee]] = relationship(back_populates="company")


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]
    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }


class Manager(Employee):
    manager_data: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }


class Engineer(Employee):
    engineer_info: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

此外,与连接继承的情况一样,我们可以创建涉及特定子类的关系。查询时,SELECT 语句将包含一个 WHERE 子句,该子句将类选择限制为该子类或多个子类

class Company(Base):
    __tablename__ = "company"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    managers: Mapped[List[Manager]] = relationship(back_populates="company")


class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }


class Manager(Employee):
    manager_name: Mapped[str] = mapped_column(nullable=True)

    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))
    company: Mapped[Company] = relationship(back_populates="managers")

    __mapper_args__ = {
        "polymorphic_identity": "manager",
    }


class Engineer(Employee):
    engineer_info: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
    }

上面,Manager 类将具有 Manager.company 属性;Company 将具有 Company.managers 属性,该属性始终针对 employee 加载,并带有额外的 WHERE 子句,该子句将行限制为 type = 'manager' 的行。

使用 polymorphic_abstract 构建更深层次的层次结构

2.0 版本新增。

在构建任何类型的继承层次结构时,映射类可以包含设置为 TrueMapper.polymorphic_abstract 参数,这表明该类应正常映射,但不希望直接实例化,并且不包含 Mapper.polymorphic_identity。然后可以将子类声明为此映射类的子类,这些子类本身可以包含 Mapper.polymorphic_identity,因此可以正常使用。这允许一系列子类通过公共基类一次性引用,该基类在层次结构中被认为是“抽象的”,无论是在查询中还是在 relationship() 声明中。此用法不同于将 __abstract__ 属性与声明式一起使用,后者使目标类完全未映射,因此本身不可用作映射类。Mapper.polymorphic_abstract 可以应用于层次结构中任何级别的任何类或多个类,包括同时应用于多个级别。

例如,假设 ManagerPrincipal 都将针对超类 Executive 进行分类,而 EngineerSysadmin 将针对超类 Technologist 进行分类。ExecutiveTechnologist 都永远不会实例化,因此没有 Mapper.polymorphic_identity。可以使用 Mapper.polymorphic_abstract 配置这些类,如下所示

class Employee(Base):
    __tablename__ = "employee"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "polymorphic_on": "type",
    }


class Executive(Employee):
    """An executive of the company"""

    executive_background: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {"polymorphic_abstract": True}


class Technologist(Employee):
    """An employee who works with technology"""

    competencies: Mapped[str] = mapped_column(nullable=True)

    __mapper_args__ = {"polymorphic_abstract": True}


class Manager(Executive):
    """a manager"""

    __mapper_args__ = {"polymorphic_identity": "manager"}


class Principal(Executive):
    """a principal of the company"""

    __mapper_args__ = {"polymorphic_identity": "principal"}


class Engineer(Technologist):
    """an engineer"""

    __mapper_args__ = {"polymorphic_identity": "engineer"}


class SysAdmin(Technologist):
    """a systems administrator"""

    __mapper_args__ = {"polymorphic_identity": "sysadmin"}

在上面的示例中,新类 TechnologistExecutive 是普通的映射类,并且还指示要添加到名为 executive_backgroundcompetencies 的超类的新列。但是,它们都缺少 Mapper.polymorphic_identity 的设置;这是因为不希望直接实例化 TechnologistExecutive;我们始终会拥有 ManagerPrincipalEngineerSysAdmin 之一。但是,我们可以查询 PrincipalTechnologist 角色,以及使它们成为 relationship() 的目标。下面的示例演示了 Technologist 对象的 SELECT 语句

session.scalars(select(Technologist)).all()
SELECT employee.id, employee.name, employee.type, employee.competencies FROM employee WHERE employee.type IN (?, ?) [...] ('engineer', 'sysadmin')

TechnologistExecutive 抽象映射类也可以像任何其他映射类一样,成为 relationship() 映射的目标。我们可以扩展上面的示例以包含 Company,以及单独的集合 Company.technologistsCompany.principals

class Company(Base):
    __tablename__ = "company"
    id = Column(Integer, primary_key=True)

    executives: Mapped[List[Executive]] = relationship()
    technologists: Mapped[List[Technologist]] = relationship()


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

    # foreign key to "company.id" is added
    company_id: Mapped[int] = mapped_column(ForeignKey("company.id"))

    # rest of mapping is the same
    name: Mapped[str]
    type: Mapped[str]

    __mapper_args__ = {
        "polymorphic_on": "type",
    }


# Executive, Technologist, Manager, Principal, Engineer, SysAdmin
# classes from previous example would follow here unchanged

使用上面的映射,我们可以跨 Company.technologistsCompany.executives 单独使用连接和关系加载技术

session.scalars(
    select(Company)
    .join(Company.technologists)
    .where(Technologist.competency.ilike("%java%"))
    .options(selectinload(Company.executives))
).all()
SELECT company.id FROM company JOIN employee ON company.id = employee.company_id AND employee.type IN (?, ?) WHERE lower(employee.competencies) LIKE lower(?) [...] ('engineer', 'sysadmin', '%java%') SELECT employee.company_id AS employee_company_id, employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type, employee.executive_background AS employee_executive_background FROM employee WHERE employee.company_id IN (?) AND employee.type IN (?, ?) [...] (1, 'manager', 'principal')

另请参阅

__abstract__ - 声明式参数,允许声明式类在层次结构中完全未映射,同时仍然从映射的超类扩展。

加载单继承映射

单表继承的加载技术与连接表继承的加载技术基本相同,并且这两种映射类型之间提供了高度的抽象,因此可以轻松地在它们之间切换,以及在单个层次结构中混合它们(只需从要单继承的任何子类中省略 __tablename__)。有关继承加载技术的文档,包括在映射器配置时以及查询时要查询的类的配置,请参阅 为继承映射编写 SELECT 语句单继承映射的 SELECT 语句 部分。

具体表继承

具体继承将每个子类映射到其自己的不同表中,每个表都包含生成该类实例所需的所有列。默认情况下,具体继承配置以非多态方式查询;对特定类的查询将仅查询该类的表,并且仅返回该类的实例。通过在映射器中配置一个特殊的 SELECT(通常作为所有表的 UNION 产生)来启用具体类的多态加载。

警告

具体表继承比连接表继承或单表继承更复杂得多,并且在功能上更加受限,尤其是在与关系、预加载和多态加载一起使用时。当以多态方式使用时,它会产生带有 UNION 的非常大的查询,这些查询的性能不如简单的连接。强烈建议,如果需要关系加载和多态加载的灵活性,则尽可能使用连接表继承或单表继承。如果不需要多态加载,那么如果每个类完全引用自己的表,则可以使用普通的非继承映射。

连接表继承和单表继承在“多态”加载方面很流畅,而具体表继承则显得更加笨拙。因此,当不需要多态加载时,具体表继承更合适。建立涉及具体表继承类的关系也比较麻烦。

要将一个类建立为使用具体表继承,请在 __mapper_args__ 中添加 Mapper.concrete 参数。这向 Declarative 以及映射表明,超类表不应被视为映射的一部分

class Employee(Base):
    __tablename__ = "employee"

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


class Manager(Employee):
    __tablename__ = "manager"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(50))

    __mapper_args__ = {
        "concrete": True,
    }


class Engineer(Employee):
    __tablename__ = "engineer"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(50))

    __mapper_args__ = {
        "concrete": True,
    }

应注意两个关键点

  • 我们必须在每个子类上显式定义所有列,即使是同名的列也是如此。例如,Employee.name 这样的列不会为我们复制到由 ManagerEngineer 映射的表中。

  • 虽然 EngineerManager 类在继承关系中与 Employee 映射,但它们仍然不包括多态加载。这意味着,如果我们查询 Employee 对象,则根本不会查询 managerengineer 表。

具体多态加载配置

具体继承的多态加载要求针对每个应具有多态加载的基类配置一个专门的 SELECT。此 SELECT 需要能够单独访问所有映射的表,并且通常是使用 SQLAlchemy 助手 polymorphic_union() 构建的 UNION 语句。

为继承映射编写 SELECT 语句 中所讨论的,任何类型的映射器继承配置都可以配置为默认从特殊的可选对象加载,方法是使用 Mapper.with_polymorphic 参数。当前公共 API 要求此参数在首次构造 Mapper 时设置。

但是,在 Declarative 的情况下,映射器和映射的 Table 都是在定义映射类的那一刻同时创建的。这意味着 Mapper.with_polymorphic 参数还不能提供,因为与子类对应的 Table 对象尚未定义。

有几种策略可以解决此循环依赖问题,但 Declarative 提供了辅助类 ConcreteBaseAbstractConcreteBase,它们在幕后处理此问题。

使用 ConcreteBase,我们可以以几乎与我们进行其他形式的继承映射相同的方式设置具体映射

from sqlalchemy.ext.declarative import ConcreteBase
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


class Employee(ConcreteBase, Base):
    __tablename__ = "employee"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "concrete": True,
    }


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

在上面,Declarative 在映射器“初始化”时为 Employee 类设置了多态可选对象;这是映射器的后期配置步骤,用于解析其他依赖的映射器。ConcreteBase 助手使用 polymorphic_union() 函数在设置完所有其他类后创建所有具体映射表的 UNION,然后使用已存在的基类映射器配置此语句。

在选择时,多态 union 生成如下查询

session.scalars(select(Employee)).all()
SELECT pjoin.id, pjoin.name, pjoin.type, pjoin.manager_data, pjoin.engineer_info FROM ( SELECT employee.id AS id, employee.name AS name, CAST(NULL AS VARCHAR(40)) AS manager_data, CAST(NULL AS VARCHAR(40)) AS engineer_info, 'employee' AS type FROM employee UNION ALL SELECT manager.id AS id, manager.name AS name, manager.manager_data AS manager_data, CAST(NULL AS VARCHAR(40)) AS engineer_info, 'manager' AS type FROM manager UNION ALL SELECT engineer.id AS id, engineer.name AS name, CAST(NULL AS VARCHAR(40)) AS manager_data, engineer.engineer_info AS engineer_info, 'engineer' AS type FROM engineer ) AS pjoin

上面的 UNION 查询需要为每个子表制造“NULL”列,以便容纳那些不属于该特定子类的列。

另请参阅

ConcreteBase

抽象具体类

到目前为止,具体映射显示了子类以及映射到各个表的基类。在具体继承用例中,常见的情况是基类不体现在数据库中,只有子类。换句话说,基类是“抽象的”。

通常,当人们希望将两个不同的子类映射到各个表,并保持基类未映射时,这可以很容易地实现。当使用 Declarative 时,只需使用 __abstract__ 指示符声明基类即可

from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


class Employee(Base):
    __abstract__ = True


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))

在上面,我们实际上并没有使用 SQLAlchemy 的继承映射工具;我们可以正常加载和持久化 ManagerEngineer 的实例。但是,当我们需要多态查询时,情况会发生变化,也就是说,我们希望发出 select(Employee) 并返回 ManagerEngineer 实例的集合。这使我们回到了具体继承的领域,我们必须针对 Employee 构建一个特殊的映射器才能实现此目的。

为了修改我们的具体继承示例以说明能够进行多态加载的“抽象”基类,我们将只有 engineermanager 表,而没有 employee 表,但是 Employee 映射器将直接映射到“多态 union”,而不是在本地指定给 Mapper.with_polymorphic 参数。

为了帮助实现这一点,Declarative 提供了 ConcreteBase 类的变体,称为 AbstractConcreteBase,它可以自动实现此目的

from sqlalchemy.ext.declarative import AbstractConcreteBase
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
    pass


class Employee(AbstractConcreteBase, Base):
    strict_attrs = True

    name = mapped_column(String(50))


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }


Base.registry.configure()

在上面,调用了 registry.configure() 方法,这将触发 Employee 类被实际映射;在配置步骤之前,该类没有映射,因为它将从中查询的子表尚未定义。此过程比 ConcreteBase 的过程更复杂,因为基类的整个映射必须延迟到所有子类都已声明之后。使用如上所示的映射,只能持久化 ManagerEngineer 的实例;针对 Employee 类的查询将始终生成 ManagerEngineer 对象。

使用上述映射,可以根据 Employee 类及其上本地声明的任何属性(例如 Employee.name)生成查询

>>> stmt = select(Employee).where(Employee.name == "n1")
>>> print(stmt)
SELECT pjoin.id, pjoin.name, pjoin.type, pjoin.manager_data, pjoin.engineer_info FROM ( SELECT engineer.id AS id, engineer.name AS name, engineer.engineer_info AS engineer_info, CAST(NULL AS VARCHAR(40)) AS manager_data, 'engineer' AS type FROM engineer UNION ALL SELECT manager.id AS id, manager.name AS name, CAST(NULL AS VARCHAR(40)) AS engineer_info, manager.manager_data AS manager_data, 'manager' AS type FROM manager ) AS pjoin WHERE pjoin.name = :name_1

AbstractConcreteBase.strict_attrs 参数指示 Employee 类应仅直接映射那些本地于 Employee 类的属性,在本例中为 Employee.name 属性。其他属性(例如 Manager.manager_dataEngineer.engineer_info)仅存在于其对应的子类上。当未设置 AbstractConcreteBase.strict_attrs 时,所有子类属性(例如 Manager.manager_dataEngineer.engineer_info)都会映射到基类 Employee 类。这是一种传统的使用模式,可能更方便查询,但其效果是所有子类共享整个层次结构的全套属性;在上面的示例中,不使用 AbstractConcreteBase.strict_attrs 将导致生成无用的 Engineer.manager_nameManager.engineer_info 属性。

2.0 版本新增: AbstractConcreteBase 添加了 AbstractConcreteBase.strict_attrs 参数,它可以生成更清晰的映射;默认值为 False,以允许传统映射继续像在 1.x 版本中那样工作。

另请参阅

AbstractConcreteBase

经典和半经典具体多态配置

使用 ConcreteBaseAbstractConcreteBase 说明的 Declarative 配置等效于另外两种形式的配置,这两种形式的配置显式使用了 polymorphic_union()。这些配置形式显式使用了 Table 对象,以便可以首先创建“多态 union”,然后将其应用于映射。此处说明这些是为了澄清 polymorphic_union() 函数在映射方面的作用。

例如,半经典映射使用了 Declarative,但单独建立了 Table 对象

metadata_obj = Base.metadata

employees_table = Table(
    "employee",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)

managers_table = Table(
    "manager",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("manager_data", String(50)),
)

engineers_table = Table(
    "engineer",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("engineer_info", String(50)),
)

接下来,使用 polymorphic_union() 生成 UNION

from sqlalchemy.orm import polymorphic_union

pjoin = polymorphic_union(
    {
        "employee": employees_table,
        "manager": managers_table,
        "engineer": engineers_table,
    },
    "type",
    "pjoin",
)

使用上面的 Table 对象,可以使用“半经典”样式生成映射,我们在其中将 Declarative 与 __table__ 参数结合使用;我们的多态 union 通过 __mapper_args__ 传递到 Mapper.with_polymorphic 参数

class Employee(Base):
    __table__ = employee_table
    __mapper_args__ = {
        "polymorphic_on": pjoin.c.type,
        "with_polymorphic": ("*", pjoin),
        "polymorphic_identity": "employee",
    }


class Engineer(Employee):
    __table__ = engineer_table
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }


class Manager(Employee):
    __table__ = manager_table
    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

或者,相同的 Table 对象可以以完全“经典”的样式使用,而无需使用 Declarative。下面说明了类似于 Declarative 提供的构造函数

class Employee:
    def __init__(self, **kw):
        for k in kw:
            setattr(self, k, kw[k])


class Manager(Employee):
    pass


class Engineer(Employee):
    pass


employee_mapper = mapper_registry.map_imperatively(
    Employee,
    pjoin,
    with_polymorphic=("*", pjoin),
    polymorphic_on=pjoin.c.type,
)
manager_mapper = mapper_registry.map_imperatively(
    Manager,
    managers_table,
    inherits=employee_mapper,
    concrete=True,
    polymorphic_identity="manager",
)
engineer_mapper = mapper_registry.map_imperatively(
    Engineer,
    engineers_table,
    inherits=employee_mapper,
    concrete=True,
    polymorphic_identity="engineer",
)

“抽象”示例也可以使用“半经典”或“经典”样式进行映射。区别在于,我们不是将“多态 union”应用于 Mapper.with_polymorphic 参数,而是将其直接作为映射的可选对象应用于我们的最基层的映射器。下面说明了半经典映射

from sqlalchemy.orm import polymorphic_union

pjoin = polymorphic_union(
    {
        "manager": managers_table,
        "engineer": engineers_table,
    },
    "type",
    "pjoin",
)


class Employee(Base):
    __table__ = pjoin
    __mapper_args__ = {
        "polymorphic_on": pjoin.c.type,
        "with_polymorphic": "*",
        "polymorphic_identity": "employee",
    }


class Engineer(Employee):
    __table__ = engineer_table
    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }


class Manager(Employee):
    __table__ = manager_table
    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }

在上面,我们以与之前相同的方式使用 polymorphic_union(),只是我们省略了 employee 表。

另请参阅

命令式映射 - 关于命令式或“经典”映射的背景信息

与具体继承的关系

在具体继承场景中,映射关系具有挑战性,因为不同的类不共享表。如果关系仅涉及特定类,例如我们之前示例中的 CompanyManager 之间的关系,则不需要特殊步骤,因为这只是两个相关的表。

但是,如果 Company 要与 Employee 建立一对多关系,表明集合可能同时包含 EngineerManager 对象,则意味着 Employee 必须具有多态加载能力,并且要关联的每个表都必须具有返回 company 表的外键。以下是此类配置的示例

from sqlalchemy.ext.declarative import ConcreteBase


class Company(Base):
    __tablename__ = "company"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    employees = relationship("Employee")


class Employee(ConcreteBase, Base):
    __tablename__ = "employee"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    company_id = mapped_column(ForeignKey("company.id"))

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "concrete": True,
    }


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

具体继承和关系的下一个复杂性涉及当我们希望 EmployeeManagerEngineer 中的一个或全部都指回 Company 时。对于这种情况,SQLAlchemy 具有特殊的行为,即放置在 Employee 上并链接到 Companyrelationship() 对于 ManagerEngineer不起作用,当在实例级别执行时。相反,必须将不同的 relationship() 应用于每个类。为了在充当 Company.employees 反向关系的三个单独关系方面实现双向行为,在每个关系之间使用了 relationship.back_populates 参数

from sqlalchemy.ext.declarative import ConcreteBase


class Company(Base):
    __tablename__ = "company"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    employees = relationship("Employee", back_populates="company")


class Employee(ConcreteBase, Base):
    __tablename__ = "employee"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    company_id = mapped_column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "employee",
        "concrete": True,
    }


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    manager_data = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "manager",
        "concrete": True,
    }


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(50))
    engineer_info = mapped_column(String(40))
    company_id = mapped_column(ForeignKey("company.id"))
    company = relationship("Company", back_populates="employees")

    __mapper_args__ = {
        "polymorphic_identity": "engineer",
        "concrete": True,
    }

上述限制与当前实现有关,包括具体继承类不共享超类的任何属性,因此需要设置不同的关系。

加载具体继承映射

使用具体继承进行加载的选项有限;通常,如果在映射器上使用声明性具体混合类之一配置了多态加载,则在当前 SQLAlchemy 版本中无法在查询时修改它。通常,with_polymorphic() 函数能够覆盖具体继承使用的加载样式,但由于当前的限制,尚不支持此功能。