Отображение иерархий наследования классов

SQLAlchemy поддерживает три формы наследования: наследование по одной таблице, когда несколько типов классов представлены одной таблицей, наследование по отдельным таблицам, когда каждый тип класса представлен независимыми таблицами, и наследование по объединенным таблицам, когда иерархия классов разбивается на зависимые таблицы, причем каждый класс представлен собственной таблицей, включающей только те атрибуты, которые локальны для данного класса.

Наиболее распространенными формами наследования являются одиночная и объединенная таблица, в то время как конкретное наследование представляет собой более сложные конфигурационные задачи.

:term:`polymorphically`W

Наследование объединенных таблиц

При табличном наследовании каждый класс в иерархии классов представлен отдельной таблицей. Запрос к конкретному подклассу в иерархии будет выглядеть как 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» для каждой подтаблицы, чтобы учесть те столбцы, которые не являются членами данного конкретного подкласса.

См.также

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 по отображению наследования; мы можем загружать и сохранять экземпляры 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.

См.также

AbstractConcreteBase

Классическая и полуклассическая полиморфная конфигурация бетона

Декларативные конфигурации, проиллюстрированные 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, однако из-за текущих ограничений это пока не поддерживается.

Вернуться на верх