SqlAlchemy 2.0 中文文档(九)(4)https://developer.aliyun.com/article/1560677
具体继承的关系
在具体继承的情况下,映射关系是具有挑战性的,因为不同的类不共享表格。如果关系仅涉及特定类,例如在我们先前的示例中Company
和Manager
之间的关系,那么不需要特殊步骤,因为这只是两个相关的表。
但是,如果Company
要对Employee
有一对多的关系,表明集合可能包含Engineer
和Manager
对象,那么这意味着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, }
具体继承和关系的下一个复杂性涉及当我们希望Employee
、Manager
和Engineer
中的一个或全部自己参考Company
时。对于这种情况,SQLAlchemy 具有特殊行为,即在Employee
上放置到Company
的relationship()
在实例级别时不适用于Manager
和Engineer
类。相反,必须对每个类应用不同的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()
函数应该能够覆盖具体使用的加载方式,但由于当前的限制,这尚不受支持。
具体多态加载配置
具有具体继承的多态加载要求针对应该具有多态加载的每个基类配置一个专用的 SELECT。这个 SELECT 需要能够单独访问所有映射的表,并且通常是使用 SQLAlchemy 助手polymorphic_union()
构造的 UNION 语句。
如为继承映射编写 SELECT 语句所讨论的,任何类型的映射器继承配置都可以使用Mapper.with_polymorphic
参数默认配置从特殊的可选项加载。当前的公共 API 要求在首次构造Mapper
时设置此参数。
但是,在声明式编程中,映射器和被映射的Table
同时创建,即在定义映射类的时候。这意味着Mapper.with_polymorphic
参数还不能提供,因为对应于子类的Table
对象尚未定义。
有几种策略可用于解决这种循环,然而,声明式提供了处理此问题的助手类ConcreteBase
和AbstractConcreteBase
。
使用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, }
在上述情况下,声明式在映射器“初始化”时为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
上述的 UNION 查询需要为每个子表制造“NULL”列,以适应那些不是特定子类成员的列。
另请参见
ConcreteBase
抽象具体类
到目前为止,所示的具体映射显示了子类以及基类映射到单独的表中。在具体继承用例中,常见的情况是基类在数据库中不表示,只有子类。换句话说,基类是“抽象的”。
通常,当一个人想要将两个不同的子类映射到单独的表中,并且保留基类未映射时,这可以非常容易地实现。当使用声明式时,只需使用__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 的继承映射功能;我们可以正常加载和持久化Manager
和Engineer
的实例。然而,当我们需要进行多态查询时,情况就会发生变化,也就是说,我们希望发出select(Employee)
并返回一组Manager
和Engineer
实例。这将我们带回到具体继承的领域,我们必须构建一个针对Employee
的特殊映射器才能实现这一点。
要修改我们的具体继承示例,以说明能够进行多态加载的“抽象”基类,我们将只有一个engineer
和一个manager
表,而没有employee
表,但Employee
映射器将直接映射到“多态联合”,而不是将其局部指定给Mapper.with_polymorphic
参数。
为了帮助解决这个问题,Declarative 提供了一种名为AbstractConcreteBase
的ConcreteBase
类的变体,可以自动实现这一点:
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
的过程更复杂,因为必须延迟基类的整个映射,直到所有的子类都已声明。通过像上面这样的映射,只能持久化Manager
和Engineer
的实例;对Employee
类进行查询将始终生成Manager
和Engineer
对象。
使用上述映射,可以按照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_data
和 Engineer.engineer_info
,仅存在于其对应的子类中。当未设置 AbstractConcreteBase.strict_attrs
时,所有子类属性,如 Manager.manager_data
和 Engineer.engineer_info
,都映射到基类 Employee
。这是一种传统的使用模式,可能更方便查询,但其效果是所有子类共享整个层次结构的完整属性集;在上述示例中,不使用 AbstractConcreteBase.strict_attrs
将导致生成不必要的 Engineer.manager_name
和 Manager.engineer_info
属性。
2.0 版本新增:增加了 AbstractConcreteBase.strict_attrs
参数到 AbstractConcreteBase
中,以产生更清晰的映射;默认值为 False,以允许传统映射继续像 1.x 版本中那样工作。
另请参阅
AbstractConcreteBase
经典和半经典具有多态性的具体配置
用ConcreteBase
和AbstractConcreteBase
说明的声明性配置等同于另外两种明确使用polymorphic_union()
的配置形式。这些配置形式明确使用Table
对象,以便首先创建“多态联合”,然后应用于映射。这里举例说明了这些配置形式,以阐明polymorphic_union()
函数在映射方面的作用。
一个半经典映射的例子利用了声明性,但是单独建立了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
对象,可以使用“半经典”风格生成映射,其中我们与__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, }
或者,完全可以使用完全“经典”风格,而根本不使用声明性,使用与声明性提供的类似的构造函数:
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
表。
另请参见
命令式映射 - 关于命令式或“经典”映射的背景信息
具体继承关系
在具体继承的情况下,映射关系是具有挑战性的,因为不同的类不共享一个表。如果关系只涉及特定的类,比如我们之前示例中的 Company
和 Manager
之间的关系,那么不需要特殊步骤,因为这只是两个相关的表。
然而,如果 Company
要与 Employee
有一对多的关系,表明集合可能包括 Engineer
和 Manager
对象,这意味着 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, }
具体继承和关系的下一个复杂性涉及当我们希望 Employee
、Manager
和 Engineer
中的一个或全部自身参考 Company
时。对于这种情况,SQLAlchemy 在 Employee
上有特殊的行为,即一个链接到 Company
的 relationship()
放置在 Employee
上,当在实例级别执行时,不适用于 Manager
和 Engineer
类。相反,必须对每个类应用不同的 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()
函数应该能够覆盖具体加载使用的样式,然而由于当前的限制,这还不被支持。