Отображение иерархий наследования классов¶
SQLAlchemy поддерживает три формы наследования: наследование по одной таблице, когда несколько типов классов представлены одной таблицей, наследование по отдельным таблицам, когда каждый тип класса представлен независимыми таблицами, и наследование по объединенным таблицам, когда иерархия классов разбивается на зависимые таблицы, причем каждый класс представлен собственной таблицей, включающей только те атрибуты, которые локальны для данного класса.
Наиболее распространенными формами наследования являются одиночная и объединенная таблица, в то время как конкретное наследование представляет собой более сложные конфигурационные задачи.
:term:`polymorphically`W
См.также
Написание операторов SELECT для отображений наследования - в Руководство по составлению запросов в ORM
:ref:`examples_inheritance`<
Наследование объединенных таблиц¶
При табличном наследовании каждый класс в иерархии классов представлен отдельной таблицей. Запрос к конкретному подклассу в иерархии будет выглядеть как SQL JOIN по всем таблицам, находящимся на пути его наследования. Если запрашиваемый класс является базовым, то вместо него запрашивается базовая таблица с возможностью одновременного включения других таблиц или последующей загрузки атрибутов, характерных для подтаблиц.
:term:`discriminator`I
Базовый класс в объединенной иерархии наследования настраивается с дополнительными аргументами, которые будут указывать на колонку полиморфного дискриминатора, и, опционально, на полиморфный идентификатор самого базового класса:
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
Column
mapped_column()
I
:paramref:`_orm.Mapper.polymorphic_identity`T
Хотя выражение полиморфного дискриминатора не является строго обязательным, оно необходимо, если требуется полиморфная загрузка. Наиболее простым способом достижения этой цели является создание столбца в базовой таблице, однако в очень сложных схемах наследования в качестве полиморфного дискриминатора могут использоваться выражения SQL, например, выражение CASE.
Примечание
В настоящее время для всей иерархии наследования** может быть настроен только один столбец дискриминатора или SQL-выражение, как правило, для самого базового класса в иерархии. «Каскадные» полиморфные дискриминаторные выражения пока не поддерживаются.
Engineer
Manager
Employee
W
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
I
Mapper.polymorphic_identity
Employee
'employee'
type
Engineer
'engineer'
Manager
'manager'
Mapper.polymorphic_identity
Наследование конкретной таблицы T
В полиморфных системах чаще всего ограничение внешнего ключа устанавливается на тот же столбец или столбцы, что и сам первичный ключ, однако это не обязательно; столбец, отличный от первичного ключа, также может ссылаться на родительский через внешний ключ. Способ построения JOIN от базовой таблицы к подклассам также может быть непосредственно настроен, однако это редко бывает необходимо.
Employee
Employee
Engineer
Manager
Engineer
Manager
Employee
employee.type
"engineer"
"manager"
"employee"
W
Отношения с объединенным наследованием¶
employee
company
Company
Employee
R
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):
...
manager
company
Manager
Company
I
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
A
Загрузка сопоставлений объединенного наследования¶
:ref:`inheritance_loading_toplevel`S
Наследование одной таблицы¶
Однотабличное наследование представляет все атрибуты всех подклассов в рамках одной таблицы. Определенный подкласс, имеющий атрибуты, уникальные для этого класса, сохраняет их в столбцах таблицы, которые в противном случае являются NULL, если строка относится к объекту другого типа.
Запрос на определенный подкласс в иерархии будет выглядеть как SELECT по базовой таблице, включающий предложение WHERE, ограничивающее строки определенным значением или значениями, присутствующими в дискриминационном столбце или выражении.
Наследование по одной таблице имеет преимущество в простоте по сравнению с наследованием по объединенным таблицам; запросы намного эффективнее, так как для загрузки объектов каждого представленного класса необходимо задействовать только одну таблицу.
``__tablename__``S
mapped_column
mapped_column
Table
E
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",
}
__tablename__
mapped_column()
nullable=True
Optional[]
NOT NULL
N
``use_existing_column``R¶
manager_name
engineer_info
Employee.__table__
N
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)
start_date
Engineer
Manager
A
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.use_existing_column
mapped_column()
mapped_column()
T
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()
Column
Table
Employee
mapped_column()
Table
Employee
A
Добавлено в версии 2.0.0b4: mapped_column.use_existing_column
declared_attr
.__table__
PEP 484 -
:ref:`orm_mixins_toplevel`A
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__ в Declarative, который оставляет целевой класс полностью не отображенным и, таким образом, не может использоваться как отображенный класс сам по себе. Mapper.polymorphic_abstract
может быть применен к любому классу или классам на любом уровне иерархии, в том числе на нескольких уровнях одновременно.
В качестве примера, предположим, что Manager
и Principal
классифицируются относительно суперкласса Executive
, а Engineer
и Sysadmin
классифицируются относительно суперкласса Technologist
. Ни Executive
, ни Technologist
никогда не инстанцируются, поэтому не имеют 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": "engineer"}
В приведенном выше примере новые классы Technologist
и Executive
являются обычными сопоставленными классами, а также указывают на новые столбцы, которые будут добавлены к суперклассу под названием executive_background
и competencies
. Однако в них обоих отсутствует параметр для Mapper.polymorphic_identity
; это потому, что не предполагается, что Technologist
или Executive
будут когда-либо инстанцированы непосредственно; у нас всегда будет один из Manager
, Principal
, Engineer
или SysAdmin
. Однако мы можем запросить роли Principal
и Technologist
, а также попросить их быть целями relationship()
. Приведенный ниже пример демонстрирует оператор SELECT для объектов Technologist
:
session.scalars(select(Technologist)).all()
{execsql}
SELECT employee.id, employee.name, employee.type, employee.competencies
FROM employee
WHERE employee.type IN (?, ?)
[...] ('engineer', 'sysadmin')
Абстрактные отображаемые классы Technologist
и Executive
также могут быть сделаны объектами отображений relationship()
, как и любой другой отображаемый класс. Мы можем расширить приведенный выше пример, включив в него Company
, с отдельными коллекциями Company.technologists
и Company.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.technologists
и Company.executives
по отдельности:
session.scalars(
select(Company)
.join(Company.technologists)
.where(Technologist.competency.ilike("%java%"))
.options(selectinload(Company.executives))
).all()
{execsql}
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, который обычно производится как объединение всех таблиц.
Предупреждение
Наследование конкретных таблиц значительно сложнее, чем наследование объединенных или одиночных таблиц, и значительно более ограничено в функциональности, особенно в отношении использования с отношениями, нетерпеливой загрузкой и полиморфной загрузкой. При полиморфном использовании это приводит к очень большим запросам с UNIONS, которые не будут работать так же хорошо, как простые объединения. Настоятельно рекомендуется, если требуется гибкость в загрузке отношений и полиморфной загрузке, использовать объединенное или однотабличное наследование, если это вообще возможно. Если полиморфная загрузка не требуется, то можно использовать обычные ненаследуемые отображения, если каждый класс полностью ссылается на свою собственную таблицу.
В то время как объединенное наследование и наследование одной таблицы свободно работают с «полиморфной» загрузкой, это более неудобное дело в конкретном наследовании. По этой причине конкретное наследование более уместно, когда полиморфная загрузка не требуется. Установление отношений, включающих классы конкретного наследования, также более неудобно.
Чтобы определить класс как использующий конкретное наследование, добавьте параметр Mapper.concrete
внутри __mapper_args__
. Это указывает 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
здесь, не копируется в таблицы, отображаемыеManager
илиEngineer
для нас.Хотя классы
Engineer
иManager
отображены в отношения наследования сEmployee
, они все еще не включают полиморфную загрузку. То есть, если мы запрашиваем объектыEmployee
, таблицыmanager
иengineer
вообще не запрашиваются.
Конфигурация бетонной полиморфной нагрузки¶
Полиморфная загрузка с конкретным наследованием требует, чтобы специализированный SELECT был настроен на каждый базовый класс, который должен иметь полиморфную загрузку. Этот SELECT должен быть способен обращаться ко всем сопоставленным таблицам по отдельности и обычно представляет собой оператор UNION, построенный с помощью помощника SQLAlchemy polymorphic_union()
.
Как обсуждалось в Написание операторов SELECT для отображений наследования, конфигурации наследования mapper любого типа могут быть настроены на загрузку из специального selectable по умолчанию с помощью аргумента Mapper.with_polymorphic
. Текущий публичный API требует, чтобы этот аргумент был установлен в Mapper
при его первом создании.
Однако в случае с Declarative и отображаемый Table
создается сразу, в момент определения отображаемого класса. Это означает, что аргумент Mapper.with_polymorphic
еще не может быть предоставлен, поскольку объекты Table
, соответствующие подклассам, еще не определены.
Существует несколько стратегий для решения этого цикла, однако Declarative предоставляет вспомогательные классы 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,
}
Выше, Declarative устанавливает полиморфный selectable для класса Employee
во время «инициализации» маппера; это поздний шаг конфигурации для мапперов, который разрешает другие зависимые мапперы. Помощник ConcreteBase
использует функцию polymorphic_union()
для создания UNION всех concrete-mapped таблиц после настройки всех других классов, а затем настраивает это утверждение с уже существующим маппером базового класса.
После выбора полиморфное объединение выдает запрос, подобный этому:
session.scalars(select(Employee)).all()
{execsql}
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» для каждой подтаблицы, чтобы учесть те столбцы, которые не являются членами данного конкретного подкласса.
См.также
Абстрактные конкретные классы¶
Конкретные отображения, проиллюстрированные до сих пор, показывают как подклассы, так и базовый класс, отображенные на отдельные таблицы. В конкретном случае использования наследования обычно базовый класс не представлен в базе данных, только подклассы. Другими словами, базовый класс является «абстрактным».
Обычно, когда нужно отобразить два разных подкласса на отдельные таблицы, а базовый класс оставить без отображения, это можно сделать очень просто. При использовании 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 по отображению наследования; мы можем загружать и сохранять экземпляры Manager
и Engineer
обычным образом. Однако ситуация меняется, когда нам нужно запросить полиморфно, то есть мы хотим выдать select(Employee)
и получить обратно коллекцию экземпляров Manager
и Engineer
. Это возвращает нас в область конкретного наследования, и мы должны построить специальный отображатель для 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
, поскольку отображение всего базового класса должно быть отложено до тех пор, пока не будут объявлены все подклассы. При таком отображении, как описано выше, могут сохраняться только экземпляры Manager
и Engineer
; запрос к классу Employee
всегда будет выдавать объекты Manager
и Engineer
.
Используя приведенное выше отображение, можно создавать запросы в терминах класса Employee
и любых атрибутов, локально объявленных для него, таких как Employee.name
:
>>> stmt = select(Employee).where(Employee.name == "n1")
>>> print(stmt)
{printsql}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.
См.также
Классическая и полуклассическая полиморфная конфигурация бетона¶
Декларативные конфигурации, проиллюстрированные ConcreteBase
и AbstractConcreteBase
, эквивалентны двум другим формам конфигурации, которые используют polymorphic_union()
явно. Эти конфигурационные формы используют объект Table
явно, так что «полиморфный союз» может быть сначала создан, а затем применен к отображениям. Они проиллюстрированы здесь, чтобы прояснить роль функции polymorphic_union()
в плане отображения.
Например, semi-classical mapping использует 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
, мы применяем его непосредственно как отображаемый selectable на нашем базовом отображателе. Полуклассическое отображение показано ниже:
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 имеет особое поведение, заключающееся в том, что relationship()
, размещенный на Employee
, который ссылается на Company
не работает против классов 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,
}
Вышеуказанное ограничение связано с текущей реализацией, в том числе с тем, что конкретные наследуемые классы не разделяют ни один из атрибутов суперкласса и поэтому нуждаются в установке отдельных отношений.
Загрузка конкретных отображений наследования¶
Возможности загрузки с наследованием в concrete ограничены; как правило, если полиморфная загрузка настроена на маппере с помощью одного из декларативных миксинов concrete, ее нельзя изменить во время запроса в текущих версиях SQLAlchemy. Обычно функция with_polymorphic()
могла бы переопределить стиль загрузки, используемый concrete, однако из-за текущих ограничений это пока не поддерживается.