Написание операторов SELECT для отображений наследования

О данном документе

В этом разделе используются отображения ORM, сконфигурированные с помощью функции ORM Inheritance, описанной в разделе Отображение иерархий наследования классов. Основное внимание будет уделено Наследование объединенных таблиц, так как это наиболее сложный случай ORM-запросов.

View the ORM setup for this page.

Выборка из базового класса в сравнении с конкретными подклассами

Оператор SELECT, построенный для класса в объединенной иерархии наследования, будет запрашивать таблицу, с которой сопоставлен класс, а также все существующие супертаблицы, используя JOIN для их объединения. Запрос возвращает объекты, относящиеся к запрашиваемому типу, а также все подтипы запрашиваемого типа, используя значение discriminator в каждой строке для определения правильного типа. Приведенный ниже запрос устанавливается на подкласс Manager Employee, который возвращает результат, содержащий только объекты типа Manager:

>>> from sqlalchemy import select
>>> stmt = select(Manager).order_by(Manager.id)
>>> managers = session.scalars(stmt).all()
{execsql}BEGIN (implicit)
SELECT manager.id, employee.id AS id_1, employee.name, employee.type, employee.company_id, manager.manager_name
FROM employee JOIN manager ON employee.id = manager.id ORDER BY manager.id
[...] ()
{stop}>>> print(managers)
[Manager('Mr. Krabs')]

Если оператор SELECT обращается к базовому классу в иерархии, то по умолчанию в SQL будет включена таблица только этого класса и JOIN не будет использоваться. Как и во всех других случаях, столбец discriminator используется для различения различных запрашиваемых подтипов, в результате чего возвращаются объекты любого возможного подтипа. В возвращаемых объектах будут заполнены атрибуты, соответствующие базовой таблице, а атрибуты, соответствующие подтаблицам, будут находиться в незагруженном состоянии и загружаться автоматически при обращении к ним. Загрузка податрибутов настраивается различными способами, о которых мы расскажем далее в этом разделе.

В приведенном ниже примере создается запрос к суперклассу Employee. Это указывает на то, что в наборе результатов могут находиться объекты любого типа, включая Manager, Engineer и Employee:

>>> from sqlalchemy import select
>>> stmt = select(Employee).order_by(Employee.id)
>>> objects = session.scalars(stmt).all()
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type, employee.company_id
FROM employee ORDER BY employee.id
[...] ()
{stop}>>> print(objects)
[Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]

Выше дополнительные таблицы для Manager и Engineer не были включены в SELECT, что означает, что возвращаемые объекты еще не будут содержать данные, представленные в этих таблицах, в данном примере атрибут .manager_name класса Manager, а также атрибут .engineer_info класса Engineer. Эти атрибуты начинают находиться в состоянии expired и автоматически заполняются при первом обращении к ним с помощью lazy loading:

>>> mr_krabs = objects[0]
>>> print(mr_krabs.manager_name)
{execsql}SELECT manager.manager_name AS manager_manager_name
FROM manager
WHERE ? = manager.id
[...] (1,)
{stop}Eugene H. Krabs

Такое поведение ленивой загрузки нежелательно при загрузке большого количества объектов, в случае если потребляющему приложению необходимо будет обращаться к атрибутам, специфичным для подклассов, так как это является примером проблемы N plus one, при которой на каждую строку выдается дополнительный SQL. Этот дополнительный SQL может повлиять на производительность, а также быть несовместимым с такими подходами, как использование asyncio. Кроме того, в нашем запросе на объекты Employee, поскольку запрос выполняется только к базовой таблице, у нас не было возможности добавить SQL-критерии, включающие специфические для подклассов атрибуты в терминах Manager или Engineer. В следующих двух разделах подробно рассматриваются две конструкции, которые по-разному решают эти две проблемы: опция загрузчика selectin_polymorphic() и конструкция сущности with_polymorphic().

Использование функции selectin_polymorphic()

Для решения проблемы производительности при обращении к атрибутам подклассов может быть использована стратегия загрузчика selectin_polymorphic() для eagerly load этих дополнительных атрибутов сразу для многих объектов. Эта опция загрузчика работает аналогично стратегии загрузчика отношений selectinload(), выдавая дополнительный оператор SELECT к каждой подтаблице для объектов, загруженных в иерархию, используя IN для запроса дополнительных строк на основе первичного ключа.

В качестве аргументов selectinload() принимает базовую сущность, которая запрашивается, а затем последовательность подклассов этой сущности, для которых необходимо загрузить их специфические атрибуты для входящих строк:

>>> from sqlalchemy.orm import selectin_polymorphic
>>> loader_opt = selectin_polymorphic(Employee, [Manager, Engineer])

Затем конструкция selectin_polymorphic() используется в качестве опции загрузчика, передавая ее в метод Select.options() подкласса Select. Пример иллюстрирует использование selectin_polymorphic() для нетерпеливой загрузки столбцов, локальных для подклассов Manager и Engineer:

>>> from sqlalchemy.orm import selectin_polymorphic
>>> loader_opt = selectin_polymorphic(Employee, [Manager, Engineer])
>>> stmt = select(Employee).order_by(Employee.id).options(loader_opt)
>>> objects = session.scalars(stmt).all()
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type, employee.company_id
FROM employee ORDER BY employee.id
[...] ()
SELECT manager.id AS manager_id, employee.id AS employee_id,
employee.type AS employee_type, manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
[...] (1,)
SELECT engineer.id AS engineer_id, employee.id AS employee_id,
employee.type AS employee_type, engineer.engineer_info AS engineer_engineer_info
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
[...] (2, 3)
{stop}>>> print(objects)
[Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]

В приведенном примере показано, что для получения дополнительных атрибутов, таких как Engineer.engineer_info, а также Manager.manager_name, выполняются два дополнительных оператора SELECT. Теперь мы можем получить доступ к этим податрибутам загруженных объектов без каких-либо дополнительных SQL-операторов:

>>> print(objects[0].manager_name)
Eugene H. Krabs

Совет

Вариант загрузчика selectin_polymorphic() пока не оптимизирован для того, чтобы базовая таблица employee не включалась во вторые два запроса «eager load»; поэтому в приведенном примере мы видим JOIN из employee в manager и engineer, несмотря на то, что столбцы из employee уже загружены. Это отличается от стратегии отношений selectinload(), которая является более сложной в этом отношении и может исключить JOIN, если в нем нет необходимости.

Комбинирование дополнительных опций загрузчика с загрузками подкласса selectin_polymorphic()

Операторы SELECT, выдаваемые selectin_polymorphic(), сами по себе являются операторами ORM, поэтому мы можем добавить и другие опции загрузчика (например, документированные в Техники загрузки отношений), которые ссылаются на конкретные подклассы. Например, если бы мы считали, что отображатель Manager имеет отношение one to many к сущности Paperwork, мы могли бы объединить использование selectin_polymorphic() и selectinload() для нетерпеливой загрузки этой коллекции на все объекты Manager, где податрибуты объектов Manager также загружались бы нетерпеливо:

>>> from sqlalchemy.orm import selectinload
>>> from sqlalchemy.orm import selectin_polymorphic
>>> stmt = (
...     select(Employee)
...     .order_by(Employee.id)
...     .options(
...         selectin_polymorphic(Employee, [Manager, Engineer]),
...         selectinload(Manager.paperwork),
...     )
... )
>>> objects = session.scalars(stmt).all()
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type, employee.company_id
FROM employee ORDER BY employee.id
[...] ()
SELECT manager.id AS manager_id, employee.id AS employee_id, employee.type AS employee_type, manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
[...] (1,)
SELECT paperwork.manager_id AS paperwork_manager_id, paperwork.id AS paperwork_id, paperwork.document_name AS paperwork_document_name
FROM paperwork
WHERE paperwork.manager_id IN (?)
[...] (1,)
SELECT engineer.id AS engineer_id, employee.id AS employee_id, employee.type AS employee_type, engineer.engineer_info AS engineer_engineer_info
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
[...] (2, 3)
{stop}>>> print(objects[0])
Manager('Mr. Krabs')
>>> print(objects[0].paperwork)
[Paperwork('Secret Recipes'), Paperwork('Krabby Patty Orders')]

Применение функции selectin_polymorphic() к уже существующей eager-загрузке

Помимо возможности добавления опций загрузчика в правую часть загрузки selectin_polymorphic(), мы также можем указать selectin_polymorphic() на цель существующей загрузки. Поскольку наше отображение setup включает родительскую сущность Company с сущностями Company.employees relationship(), ссылающимися на сущности Employee, мы можем проиллюстрировать SELECT против сущности Company, который нетерпеливо загружает все объекты Employee, а также все атрибуты их подтипов следующим образом, применяя Load.selectin_polymorphic() в качестве опции цепного загрузчика; В этой форме первый аргумент является неявным из предыдущей опции загрузчика (в данном случае selectinload()), поэтому мы указываем только дополнительные целевые подклассы, которые мы хотим загрузить: :

>>> stmt = select(Company).options(
...     selectinload(Company.employees).selectin_polymorphic([Manager, Engineer])
... )
>>> for company in session.scalars(stmt):
...     print(f"company: {company.name}")
...     print(f"employees: {company.employees}")
{execsql}SELECT company.id, company.name
FROM company
[...] ()
SELECT employee.company_id AS employee_company_id, employee.id AS employee_id,
employee.name AS employee_name, employee.type AS employee_type
FROM employee
WHERE employee.company_id IN (?)
[...] (1,)
SELECT manager.id AS manager_id, employee.id AS employee_id, employee.name AS employee_name,
employee.type AS employee_type, employee.company_id AS employee_company_id,
manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
[...] (1,)
SELECT engineer.id AS engineer_id, employee.id AS employee_id, employee.name AS employee_name,
employee.type AS employee_type, employee.company_id AS employee_company_id,
engineer.engineer_info AS engineer_engineer_info
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
[...] (2, 3)
{stop}company: Krusty Krab
employees: [Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]

См.также

Повышенная загрузка полиморфных подтипов - иллюстрирует эквивалентный пример, как и выше, с использованием вместо него with_polymorphic()

Настройка функции selectin_polymorphic() на картографах

Поведение selectin_polymorphic() может быть настроено на конкретных мапперах так, чтобы оно происходило по умолчанию, с помощью параметра Mapper.polymorphic_load, использующего значение "selectin" на основе каждого подкласса. Приведенный ниже пример иллюстрирует использование этого параметра в рамках подклассов Engineer и Manager:

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

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


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, ForeignKey("employee.id"), primary_key=True)
    engineer_info = mapped_column(String(30))

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


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, ForeignKey("employee.id"), primary_key=True)
    manager_name = mapped_column(String(30))

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

При таком отображении операторы SELECT, обращенные к классу Employee, будут автоматически предполагать использование selectin_polymorphic(Employee, [Engineer, Manager]) в качестве опции загрузчика при выдаче оператора.

Использование функции with_polymorphic()

В отличие от selectin_polymorphic(), которая влияет только на загрузку объектов, конструкция with_polymorphic() влияет на то, как будет выглядеть SQL-запрос для полиморфной структуры, чаще всего в виде серии LEFT OUTER JOIN к каждой из входящих в нее подтаблиц. Такая структура соединений называется полиморфной селектируемой. Обеспечивая просмотр сразу нескольких подтаблиц, with_polymorphic() предоставляет возможность написания оператора SELECT сразу для нескольких унаследованных классов с возможностью добавления критериев фильтрации по отдельным подтаблицам.

with_polymorphic() - это, по сути, специальная форма конструкции aliased(). Она принимает в качестве аргументов форму, аналогичную форме selectin_polymorphic(), которая представляет собой базовую сущность, к которой производится запрос, а затем последовательность подклассов этой сущности, для которых необходимо загрузить их специфические атрибуты для входящих строк:

>>> from sqlalchemy.orm import with_polymorphic
>>> employee_poly = with_polymorphic(Employee, [Engineer, Manager])

Для того чтобы указать, что все подклассы должны быть частью сущности, with_polymorphic() также принимает строку "*", которая может быть передана вместо последовательности классов для указания всех классов (обратите внимание, что это пока не поддерживается selectin_polymorphic()):

>>> employee_poly = with_polymorphic(Employee, "*")

Приведенный ниже пример иллюстрирует ту же операцию, что и в предыдущем разделе, для одновременной загрузки всех столбцов для Manager и Engineer:

>>> stmt = select(employee_poly).order_by(employee_poly.id)
>>> objects = session.scalars(stmt).all()
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type, employee.company_id,
manager.id AS id_1, manager.manager_name, engineer.id AS id_2, engineer.engineer_info
FROM employee
LEFT OUTER JOIN manager ON employee.id = manager.id
LEFT OUTER JOIN engineer ON employee.id = engineer.id ORDER BY employee.id
[...] ()
{stop}>>> print(objects)
[Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]

Как и в случае с selectin_polymorphic(), атрибуты подклассов уже загружены:

>>> print(objects[0].manager_name)
Eugene H. Krabs

Поскольку в селекте по умолчанию, создаваемом with_polymorphic(), используется LEFT OUTER JOIN, с точки зрения базы данных запрос не так хорошо оптимизирован, как при использовании подхода selectin_polymorphic(), при котором простые операторы SELECT, использующие только JOIN, выдаются на основе каждой таблицы.

Фильтрация атрибутов подклассов с помощью функции with_polymorphic()

Конструкция with_polymorphic() делает доступными атрибуты включенных отображателей подклассов, включая пространства имен, позволяющие ссылаться на подклассы. Созданная в предыдущем разделе конструкция employee_poly включает атрибуты с именами .Engineer и .Manager, которые предоставляют пространство имен для Engineer и Manager в терминах полиморфного SELECT. В приведенном ниже примере мы можем использовать конструкцию or_() для создания критериев сразу для двух классов:

>>> from sqlalchemy import or_
>>> employee_poly = with_polymorphic(Employee, [Engineer, Manager])
>>> stmt = (
...     select(employee_poly)
...     .where(
...         or_(
...             employee_poly.Manager.manager_name == "Eugene H. Krabs",
...             employee_poly.Engineer.engineer_info
...             == "Senior Customer Engagement Engineer",
...         )
...     )
...     .order_by(employee_poly.id)
... )
>>> objects = session.scalars(stmt).all()
{execsql}SELECT employee.id, employee.name, employee.type, employee.company_id, manager.id AS id_1,
manager.manager_name, engineer.id AS id_2, engineer.engineer_info
FROM employee
LEFT OUTER JOIN manager ON employee.id = manager.id
LEFT OUTER JOIN engineer ON employee.id = engineer.id
WHERE manager.manager_name = ? OR engineer.engineer_info = ?
ORDER BY employee.id
[...] ('Eugene H. Krabs', 'Senior Customer Engagement Engineer')
{stop}>>> print(objects)
[Manager('Mr. Krabs'), Engineer('Squidward')]

Использование сглаживания с помощью функции with_polymorphic

Конструкция with_polymorphic(), являясь частным случаем aliased(), также обеспечивает основную возможность aliased(), которая заключается в «алиасинге» полиморфного selectable. В частности, это означает, что в одном операторе можно одновременно использовать две или более сущностей with_polymorphic(), относящихся к одной иерархии классов.

Для использования этой возможности в связке с объединенным наследованием обычно требуется передать два параметра, with_polymorphic.aliased и with_polymorphic.flat. Параметр with_polymorphic.aliased указывает, что на полиморфный selectable следует ссылаться по псевдониму, уникальному для данной конструкции. Параметр with_polymorphic.flat специфичен для полиморфного селекта по умолчанию LEFT OUTER JOIN и указывает на то, что в операторе должна использоваться более оптимизированная форма псевдонимов.

Для иллюстрации этой возможности в приведенном ниже примере выдается SELECT для двух отдельных полиморфных сущностей, Employee, объединенных с Engineer, и Employee, объединенных с Manager. Поскольку обе эти полиморфные сущности будут включать в свой полиморфный select базовую таблицу employee, то для того, чтобы различать эту таблицу в двух разных контекстах, необходимо применить псевдонимы. Эти две полиморфные сущности рассматриваются как две отдельные таблицы, и поэтому обычно должны быть каким-либо образом объединены друг с другом, как показано ниже, где сущности объединены по столбцу company_id вместе с некоторыми дополнительными ограничивающими критериями для сущностей Employee / Manager:

>>> manager_employee = with_polymorphic(Employee, [Manager], aliased=True, flat=True)
>>> engineer_employee = with_polymorphic(Employee, [Engineer], aliased=True, flat=True)
>>> stmt = (
...     select(manager_employee, engineer_employee)
...     .join(
...         engineer_employee,
...         engineer_employee.company_id == manager_employee.company_id,
...     )
...     .where(
...         or_(
...             manager_employee.name == "Mr. Krabs",
...             manager_employee.Manager.manager_name == "Eugene H. Krabs",
...         )
...     )
...     .order_by(engineer_employee.name, manager_employee.name)
... )
>>> for manager, engineer in session.execute(stmt):
...     print(f"{manager} {engineer}")
{execsql}SELECT
employee_1.id, employee_1.name, employee_1.type, employee_1.company_id,
manager_1.id AS id_1, manager_1.manager_name,
employee_2.id AS id_2, employee_2.name AS name_1, employee_2.type AS type_1,
employee_2.company_id AS company_id_1, engineer_1.id AS id_3, engineer_1.engineer_info
FROM employee AS employee_1
LEFT OUTER JOIN manager AS manager_1 ON employee_1.id = manager_1.id
JOIN
   (employee AS employee_2 LEFT OUTER JOIN engineer AS engineer_1 ON employee_2.id = engineer_1.id)
ON employee_2.company_id = employee_1.company_id
WHERE employee_1.name = ? OR manager_1.manager_name = ?
ORDER BY employee_2.name, employee_1.name
[...] ('Mr. Krabs', 'Eugene H. Krabs')
{stop}Manager('Mr. Krabs') Manager('Mr. Krabs')
Manager('Mr. Krabs') Engineer('SpongeBob')
Manager('Mr. Krabs') Engineer('Squidward')

В приведенном примере поведение with_polymorphic.flat заключается в том, что полиморфные селекторы остаются в виде LEFT OUTER JOIN своих отдельных таблиц, которые сами получают анонимные псевдонимы. Также создается право-вложенный JOIN.

При опущении параметра with_polymorphic.flat обычное поведение заключается в том, что каждый полиморфный selectable заключен в подзапрос, что приводит к более многословной форме:

>>> manager_employee = with_polymorphic(Employee, [Manager], aliased=True)
>>> engineer_employee = with_polymorphic(Employee, [Engineer], aliased=True)
>>> stmt = (
...     select(manager_employee, engineer_employee)
...     .join(
...         engineer_employee,
...         engineer_employee.company_id == manager_employee.company_id,
...     )
...     .where(
...         or_(
...             manager_employee.name == "Mr. Krabs",
...             manager_employee.Manager.manager_name == "Eugene H. Krabs",
...         )
...     )
...     .order_by(engineer_employee.name, manager_employee.name)
... )
>>> print(stmt)
{printsql}SELECT anon_1.employee_id, anon_1.employee_name, anon_1.employee_type,
anon_1.employee_company_id, anon_1.manager_id, anon_1.manager_manager_name, anon_2.employee_id AS employee_id_1,
anon_2.employee_name AS employee_name_1, anon_2.employee_type AS employee_type_1,
anon_2.employee_company_id AS employee_company_id_1, anon_2.engineer_id, anon_2.engineer_engineer_info
FROM
(SELECT employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type,
employee.company_id AS employee_company_id,
manager.id AS manager_id, manager.manager_name AS manager_manager_name
FROM employee LEFT OUTER JOIN manager ON employee.id = manager.id) AS anon_1
JOIN
(SELECT employee.id AS employee_id, employee.name AS employee_name, employee.type AS employee_type,
employee.company_id AS employee_company_id, engineer.id AS engineer_id, engineer.engineer_info AS engineer_engineer_info
FROM employee LEFT OUTER JOIN engineer ON employee.id = engineer.id) AS anon_2
ON anon_2.employee_company_id = anon_1.employee_company_id
WHERE anon_1.employee_name = :employee_name_2 OR anon_1.manager_manager_name = :manager_manager_name_1
ORDER BY anon_2.employee_name, anon_1.employee_name

Приведенная выше форма исторически была более переносимой для бэкендов, которые не всегда поддерживали право-вложенные JOIN, и, кроме того, она может быть уместна, когда «полиморфный селект», используемый with_polymorphic(), не является простым LEFT OUTER JOIN таблиц, как это бывает при использовании таких отображений, как concrete table inheritance, а также при использовании альтернативных полиморфных селектов в целом.

Настройка функции with_polymorphic() на картографах

Как и в случае с selectin_polymorphic(), конструкция with_polymorphic() также поддерживает версию с настройкой маппера, которая может быть настроена двумя различными способами: либо на базовом классе с помощью параметра mapper.with_polymorphic, либо в более современной форме с помощью параметра Mapper.polymorphic_load на основе каждого подкласса, передавая значение "inline".

Предупреждение

Для сопоставлений с объединенным наследованием предпочтите явное использование with_polymorphic() внутри запросов или для неявной загрузки подклассов используйте Mapper.polymorphic_load с "selectin", вместо использования параметра mapper.with_polymorphic на уровне маппера, описанного в данном разделе. Этот параметр вызывает сложную эвристику, предназначенную для переписывания предложений FROM в операторах SELECT, что может помешать построению более сложных запросов, особенно тех, которые содержат вложенные подзапросы, ссылающиеся на одну и ту же отображаемую сущность.

Например, мы можем изложить наше отображение Employee с помощью Mapper.polymorphic_load как "inline", как показано ниже:

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

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


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, ForeignKey("employee.id"), primary_key=True)
    engineer_info = mapped_column(String(30))

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


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, ForeignKey("employee.id"), primary_key=True)
    manager_name = mapped_column(String(30))

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

С приведенным выше отображением операторы SELECT, обращенные к классу Employee, будут автоматически предполагать использование with_polymorphic(Employee, [Engineer, Manager]) в качестве первичной сущности при выдаче оператора:

print(select(Employee))
{printsql}SELECT employee.id, employee.name, employee.type, engineer.id AS id_1,
engineer.engineer_info, manager.id AS id_2, manager.manager_name
FROM employee
LEFT OUTER JOIN engineer ON employee.id = engineer.id
LEFT OUTER JOIN manager ON employee.id = manager.id

При использовании mapper-level «with polymorphic» запросы также могут напрямую обращаться к сущностям подклассов, где они неявно представляют объединенные таблицы в полиморфном запросе. Выше мы можем свободно ссылаться на Manager и Engineer непосредственно против сущности по умолчанию Employee:

print(
    select(Employee).where(
        or_(Manager.manager_name == "x", Engineer.engineer_info == "y")
    )
)
{printsql}SELECT employee.id, employee.name, employee.type, engineer.id AS id_1,
engineer.engineer_info, manager.id AS id_2, manager.manager_name
FROM employee
LEFT OUTER JOIN engineer ON employee.id = engineer.id
LEFT OUTER JOIN manager ON employee.id = manager.id
WHERE manager.manager_name = :manager_name_1
OR engineer.engineer_info = :engineer_info_1

Однако если бы нам потребовалось ссылаться на сущность Employee или ее подсубъекты в отдельных, псевдоориентированных контекстах, мы бы снова напрямую использовали with_polymorphic() для определения этих псевдоориентированных сущностей, как показано в Использование сглаживания с помощью функции with_polymorphic.

Для более централизованного управления полиморфным selectable можно использовать более традиционную форму управления полиморфным selectable на уровне картографа, которая представляет собой параметр Mapper.with_polymorphic, настраиваемый на базовый класс. Этот параметр принимает аргументы, сопоставимые с конструкцией with_polymorphic(), однако при отображении объединенного наследования обычно используется обычная звездочка, указывающая на то, что все подтаблицы должны быть LEFT OUTER JOINED, как в примере:

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

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


class Engineer(Employee):
    __tablename__ = "engineer"
    id = mapped_column(Integer, ForeignKey("employee.id"), primary_key=True)
    engineer_info = mapped_column(String(30))

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


class Manager(Employee):
    __tablename__ = "manager"
    id = mapped_column(Integer, ForeignKey("employee.id"), primary_key=True)
    manager_name = mapped_column(String(30))

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

В целом, формат LEFT OUTER JOIN, используемый with_polymorphic() и такими опциями, как Mapper.with_polymorphic, может быть громоздким с точки зрения оптимизаторов SQL и баз данных; для общей загрузки атрибутов подклассов в объединенных отображениях наследования, вероятно, следует предпочесть подход selectin_polymorphic() или его эквивалент на уровне отображения - установку Mapper.polymorphic_load в "selectin", используя with_polymorphic() только по мере необходимости на основе отдельных запросов.

Присоединение к определенным подтипам или сущностям with_polymorphic()

Поскольку сущность with_polymorphic() является частным случаем сущности aliased(), то для того, чтобы рассматривать полиморфную сущность как объект присоединения, в частности, при использовании конструкции relationship() в качестве условия ON, мы используем ту же технику для регулярных псевдонимов, которая подробно описана в Использование Relationship для объединения смежных целей, наиболее кратко - с помощью PropComparator.of_type(). В приведенном ниже примере мы иллюстрируем присоединение от родительской сущности Company вдоль отношения «один-ко-многим» Company.employees, которое настроено в setup для связи с объектами Employee, используя сущность with_polymorphic() в качестве цели:

>>> employee_plus_engineer = with_polymorphic(Employee, [Engineer])
>>> stmt = (
...     select(Company.name, employee_plus_engineer.name)
...     .join(Company.employees.of_type(employee_plus_engineer))
...     .where(
...         or_(
...             employee_plus_engineer.name == "SpongeBob",
...             employee_plus_engineer.Engineer.engineer_info
...             == "Senior Customer Engagement Engineer",
...         )
...     )
... )
>>> for company_name, emp_name in session.execute(stmt):
...     print(f"{company_name} {emp_name}")
{execsql}SELECT company.name, employee.name AS name_1
FROM company JOIN (employee LEFT OUTER JOIN engineer ON employee.id = engineer.id) ON company.id = employee.company_id
WHERE employee.name = ? OR engineer.engineer_info = ?
[...] ('SpongeBob', 'Senior Customer Engagement Engineer')
{stop}Krusty Krab SpongeBob
Krusty Krab Squidward

Более непосредственно, PropComparator.of_type() также используется с отображениями наследования любого типа, чтобы ограничить присоединение по relationship() к конкретному подтипу цели relationship(). Приведенный выше запрос можно записать строго в терминах целей Engineer следующим образом:

>>> stmt = (
...     select(Company.name, Engineer.name)
...     .join(Company.employees.of_type(Engineer))
...     .where(
...         or_(
...             Engineer.name == "SpongeBob",
...             Engineer.engineer_info == "Senior Customer Engagement Engineer",
...         )
...     )
... )
>>> for company_name, emp_name in session.execute(stmt):
...     print(f"{company_name} {emp_name}")
{execsql}SELECT company.name, employee.name AS name_1
FROM company JOIN (employee JOIN engineer ON employee.id = engineer.id) ON company.id = employee.company_id
WHERE employee.name = ? OR engineer.engineer_info = ?
[...] ('SpongeBob', 'Senior Customer Engagement Engineer')
{stop}Krusty Krab SpongeBob
Krusty Krab Squidward

Выше было отмечено, что присоединение к цели Engineer напрямую, а не через «полиморфный selectable» with_polymorphic(Employee, [Engineer]), имеет полезное свойство - использование внутреннего JOIN, а не LEFT OUTER JOIN, что в целом более эффективно с точки зрения оптимизатора SQL.

Повышенная загрузка полиморфных подтипов

Использование PropComparator.of_type(), проиллюстрированное на примере метода Select.join() в предыдущем разделе, может быть эквивалентно применено и к relationship loader options, например selectinload() и joinedload().

В качестве базового примера, если мы хотим загрузить объекты Company и дополнительно нетерпеливо загрузить все элементы Company.employees с помощью конструкции with_polymorphic() по всей иерархии, мы можем написать:

>>> all_employees = with_polymorphic(Employee, "*")
>>> stmt = select(Company).options(selectinload(Company.employees.of_type(all_employees)))
>>> for company in session.scalars(stmt):
...     print(f"company: {company.name}")
...     print(f"employees: {company.employees}")
{execsql}SELECT company.id, company.name
FROM company
[...] ()
SELECT employee.company_id AS employee_company_id, employee.id AS employee_id,
employee.name AS employee_name, employee.type AS employee_type, manager.id AS manager_id,
manager.manager_name AS manager_manager_name, engineer.id AS engineer_id,
engineer.engineer_info AS engineer_engineer_info
FROM employee
LEFT OUTER JOIN manager ON employee.id = manager.id
LEFT OUTER JOIN engineer ON employee.id = engineer.id
WHERE employee.company_id IN (?)
[...] (1,)
company: Krusty Krab
employees: [Manager('Mr. Krabs'), Engineer('SpongeBob'), Engineer('Squidward')]

Приведенный выше запрос можно непосредственно сравнить с версией selectin_polymorphic(), проиллюстрированной в предыдущем разделе Применение функции selectin_polymorphic() к уже существующей eager-загрузке.

См.также

Применение функции selectin_polymorphic() к уже существующей eager-загрузке - иллюстрирует эквивалентный пример, как и выше, с использованием вместо него selectin_polymorphic()

Операторы SELECT для отображений с одним наследованием

Настройка наследования одной таблицы

В этом разделе рассматривается наследование по одной таблице, описанное в Наследование одной таблицы, которое использует одну таблицу для представления нескольких классов в иерархии.

View the ORM setup for this section.

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

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

Например, запрос к отображению примера с одним наследованием Employee загрузит объекты типа Manager, Engineer и Employee с помощью простого SELECT из таблицы:

>>> stmt = select(Employee).order_by(Employee.id)
>>> for obj in session.scalars(stmt):
...     print(f"{obj}")
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type
FROM employee ORDER BY employee.id
[...] ()
{stop}Manager('Mr. Krabs')
Engineer('SpongeBob')
Engineer('Squidward')

При выдаче загрузки для конкретного подкласса в SELECT добавляются дополнительные критерии, ограничивающие строки, например, как показано ниже, где выполняется SELECT против сущности Engineer:

>>> stmt = select(Engineer).order_by(Engineer.id)
>>> objects = session.scalars(stmt).all()
{execsql}SELECT employee.id, employee.name, employee.type, employee.engineer_info
FROM employee
WHERE employee.type IN (?) ORDER BY employee.id
[...] ('engineer',)
{stop}>>> for obj in objects:
...     print(f"{obj}")
Engineer('SpongeBob')
Engineer('Squidward')

Оптимизация нагрузки на атрибуты при одиночном наследовании

Стандартное поведение отображений одиночного наследования в отношении выбора атрибутов подклассов аналогично поведению объединенного наследования, т.е. атрибуты, специфичные для подклассов, по умолчанию выбираются вторым SELECT. В приведенном ниже примере загружается один Employee типа Manager, но поскольку запрашиваемый класс Employee, то атрибут Manager.manager_name по умолчанию отсутствует, и при обращении к нему выдается дополнительный SELECT:

>>> mr_krabs = session.scalars(select(Employee).where(Employee.name == "Mr. Krabs")).one()
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type
FROM employee
WHERE employee.name = ?
[...] ('Mr. Krabs',)
{stop}>>> mr_krabs.manager_name
{execsql}SELECT employee.manager_name AS employee_manager_name
FROM employee
WHERE employee.id = ? AND employee.type IN (?)
[...] (1, 'manager')
{stop}'Eugene H. Krabs'

Чтобы изменить это поведение, те же общие концепции, которые используются для нетерпеливой загрузки дополнительных атрибутов при загрузке объединенного наследования, применимы и к одиночному наследованию, включая использование опции selectin_polymorphic(), а также опции with_polymorphic(), последняя из которых просто включает дополнительные столбцы и с точки зрения SQL является более эффективной для отображателей одиночного наследования:

>>> employees = with_polymorphic(Employee, "*")
>>> stmt = select(employees).order_by(employees.id)
>>> objects = session.scalars(stmt).all()
{execsql}BEGIN (implicit)
SELECT employee.id, employee.name, employee.type,
employee.manager_name, employee.engineer_info
FROM employee ORDER BY employee.id
[...] ()
{stop}>>> for obj in objects:
...     print(f"{obj}")
Manager('Mr. Krabs')
Engineer('SpongeBob')
Engineer('Squidward')
>>> objects[0].manager_name
'Eugene H. Krabs'

Поскольку накладные расходы на загрузку отображений подклассов с одним наследованием обычно минимальны, поэтому рекомендуется включать в отображения с одним наследованием параметр Mapper.polymorphic_load с настройкой "inline" для тех подклассов, для которых загрузка специфических атрибутов подклассов ожидается часто. Пример, иллюстрирующий setup, модифицированный для включения этого параметра, приведен ниже:

>>> class Base(DeclarativeBase):
...     pass
>>> class Employee(Base):
...     __tablename__ = "employee"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     name: Mapped[str]
...     type: Mapped[str]
...
...     def __repr__(self):
...         return f"{self.__class__.__name__}({self.name!r})"
...
...     __mapper_args__ = {
...         "polymorphic_identity": "employee",
...         "polymorphic_on": "type",
...     }
>>> class Manager(Employee):
...     manager_name: Mapped[str] = mapped_column(nullable=True)
...     __mapper_args__ = {
...         "polymorphic_identity": "manager",
...         "polymorphic_load": "inline",
...     }
>>> class Engineer(Employee):
...     engineer_info: Mapped[str] = mapped_column(nullable=True)
...     __mapper_args__ = {
...         "polymorphic_identity": "engineer",
...         "polymorphic_load": "inline",
...     }

При использовании приведенного выше отображения классы Manager и Engineer будут автоматически включать свои столбцы в операторы SELECT по отношению к сущности Employee:

>>> print(select(Employee))
{printsql}SELECT employee.id, employee.name, employee.type,
employee.manager_name, employee.engineer_info
FROM employee

Наследование Загрузка API

Object Name Description

selectin_polymorphic(base_cls, classes)

Указывает, что для всех атрибутов, характерных для подкласса, должна происходить ускоренная загрузка.

with_polymorphic(base, classes[, selectable, flat, ...])

Производит конструкцию AliasedClass, задающую столбцы для отображений-потомков заданной базы.

function sqlalchemy.orm.with_polymorphic(base: Union[Type[_O], Mapper[_O]], classes: Union[Literal['*'], Iterable[Type[Any]]], selectable: Union[Literal[False, None], FromClause] = False, flat: bool = False, polymorphic_on: Optional[ColumnElement[Any]] = None, aliased: bool = False, innerjoin: bool = False, adapt_on_names: bool = False, _use_mapper_path: bool = False) AliasedClass[_O]

Производит конструкцию AliasedClass, задающую столбцы для отображений-потомков заданной базы.

Использование этого метода гарантирует, что таблицы каждого потомка мэппера будут включены в предложение FROM, и позволит использовать критерий filter() для этих таблиц. Результирующие экземпляры также будут иметь уже загруженные колонки, так что «пост-выборка» этих колонок не потребуется.

См.также

Использование функции with_polymorphic() - полное обсуждение with_polymorphic().

Параметры:
  • base – Базовый класс для псевдонимов.

  • classes – один класс или сопоставитель, или список классов/сопоставителей, которые наследуются от базового класса. Также это может быть строка '*', в этом случае в предложение FROM будут добавлены все сопоставленные классы по убыванию.

  • aliased – если флаг True, то selectable будет алиасирован. Для JOIN это означает, что JOIN будет выбираться внутри подзапроса, если только флаг with_polymorphic.flat не установлен в True, что рекомендуется для более простых случаев использования.

  • flat – Boolean, будет передаваться в вызов FromClause.alias(), так что псевдонимы объектов Join будут псевдонимами отдельных таблиц внутри объединения, а не созданием подзапроса. Это поддерживается всеми современными базами данных в отношении право-вложенных объединений и, как правило, дает более эффективные запросы. Установка этого флага рекомендуется при условии, что результирующий SQL является функциональным.

  • selectable – таблица или подзапрос, который будет использоваться вместо сгенерированного предложения FROM. Этот аргумент необходим, если какой-либо из желаемых классов использует конкретное наследование таблиц, поскольку SQLAlchemy в настоящее время не может автоматически генерировать UNIONы между таблицами. Если он используется, то аргумент selectable должен представлять собой полный набор таблиц и столбцов, отображаемых каждым отображаемым классом. В противном случае неучтенные сопоставленные столбцы приведут к тому, что их таблица будет добавлена непосредственно в предложение FROM, что, как правило, приводит к некорректным результатам. Если оставить значение по умолчанию False, то для выбора строк используется полиморфный selectable, назначенный базовому мэпперу. Однако можно передать и значение None, что позволит обойти сконфигурированный полиморфный selectable и вместо него построить специальный selectable для заданных целевых классов; для наследования объединенных таблиц это будет join, включающий все целевые картографы и их подклассы.

  • polymorphic_on – столбец, который будет использоваться в качестве столбца-«дискриминатора» для данного selectable. Если он не задан, то будет использоваться атрибут polymorphic_on отображения базовых классов, если таковой имеется. Это полезно для отображений, которые по умолчанию не имеют полиморфного поведения при загрузке.

  • innerjoin – если True, то будет использоваться INNER JOIN. Это следует указывать только в том случае, если запрос выполняется только для одного конкретного подтипа

  • adapt_on_names – Передает параметр aliased.adapt_on_names псевдоориентированному объекту. Это может быть полезно в ситуациях, когда заданный selectable не связан напрямую с существующим mapped selectable. … versionadded:: 1.4.33

function sqlalchemy.orm.selectin_polymorphic(base_cls: _EntityType[Any], classes: Iterable[Type[Any]]) _AbstractLoad

Указывает, что для всех атрибутов, характерных для подкласса, должна происходить ускоренная загрузка.

При этом используется дополнительный SELECT с IN против всех совпадающих значений первичного ключа, что является аналогом первичного запроса для параметра "selectin" mapper.polymorphic_load.

Добавлено в версии 1.2.

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