映射类继承层次结构

SQLAlchemy 支持三种形式的继承

  • 单表继承 - 多种类型的类由一张表表示;

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

  • 联接表继承 - 类层次结构在依赖表之间划分。每个类由它自己的表表示,该表只包含该类特有的属性。

最常见的继承形式是单表继承和联接表继承,而具体表继承则在配置方面更具挑战性。

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

另请参阅

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

继承映射食谱 - 联接、单表和具体表继承的完整示例

联接表继承

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

在所有情况下,给定行的最终实例化类都是通过 鉴别器 列或 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 表达式,通常配置在层次结构中最高的基类上。“级联”多态鉴别器表达式尚不支持。

接下来,我们定义 EngineerManager 作为 Employee 的子类。每个子类都包含表示它们所代表的子类特有属性的列。每个表还必须包含一个主键列(或列),以及指向父表的外部键引用。

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 对象将自动填充 employee.type 列,在这种情况下的正确“鉴别器”值为 "engineer""manager""employee",具体取决于情况。

联合继承的关系

联合表继承完全支持关系。涉及联合继承类的关系应针对层次结构中与外键约束相对应的类;如下所示,由于 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 属性,该属性始终针对 employeemanager 表的组合进行加载。

加载联合继承映射

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

单表继承

单表继承将所有子类的所有属性表示在一个表中。具有该类独有属性的特定子类将在表中的列中持久化这些属性,如果行引用的是不同类型的对象,则这些列中的值将为 NULL。

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

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

单表继承配置与联合表继承非常相似,只是只有基类指定了 __tablename__。基表上还需要一个鉴别器列,以便区分不同的类。

即使子类共享所有属性的基表,但在使用 Declarative 时,仍然可以在子类上指定 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.

上述情况向 Declarative 映射系统提出了一个歧义,可以通过使用 mapped_column.use_existing_column 参数在 mapped_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 类型支持。

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

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 中的新内容。

在构建任何类型的继承层次结构时,映射类可以包含 Mapper.polymorphic_abstract 参数设置为 True,这表明该类应该像平常一样进行映射,但不会直接实例化,也不会包含 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,可以启用具体类的多态加载。

警告

具体表继承比联接表继承或单表继承 **复杂得多**,并且在功能方面 **受到更多限制**,特别是在与关系、急切加载和多态加载一起使用时。当以多态方式使用时,它会生成包含并集的 **非常大的查询**,这些查询的性能不如简单的联接。强烈建议,如果需要关系加载和多态加载的灵活性,只要有可能,就应使用联接表继承或单表继承。如果不需要多态加载,则可以使用普通的非继承映射,如果每个类完全引用它自己的表。

虽然联接表继承和单表继承在“多态”加载方面很流畅,但在具体继承中则是一件比较麻烦的事情。因此,当 **不需要多态加载** 时,具体继承更适合。建立涉及具体继承类的关系也比较麻烦。

要将类设置为使用具体继承,请在 __mapper_args__ 中添加 Mapper.concrete 参数。这表示在声明式以及映射中,超类表不应被视为映射的一部分

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() 构建的并集语句。

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

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

有几种策略可以解决此循环,但是声明式提供了帮助程序类 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() 函数在所有其他类设置完毕后,创建所有具体映射表的联合,然后将此语句与已存在的基类映射器配置。

在选择时,多态联合会产生类似于以下的查询

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

上面的联合查询需要为每个子表制造“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 构建一个特殊的映射器,才能实现这一点。

为了修改我们的具体继承示例以说明能够进行多态加载的“抽象”基类,我们只有一个 engineer 表和一个 manager 表,没有 employee 表,但是 Employee 映射器将直接映射到“多态联合”,而不是在本地指定到 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 对象,以便首先创建“多态联合”,然后将其应用于映射。为了阐明 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() 生成联合

from sqlalchemy.orm import polymorphic_union

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

使用上面的 Table 对象,可以使用“半经典”风格来生成映射,其中我们使用 Declarative 以及 __table__ 参数;上面的多态联合通过 __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",
)

可以使用“半经典”或“经典”风格来映射“抽象”示例。不同之处在于,我们不是将“多态联合”应用于 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() 函数能够覆盖具体使用的加载样式,但是由于当前的限制,这还没有得到支持。