Написание операторов SELECT для отображений наследования¶
О данном документе
В этом разделе используются отображения ORM, сконфигурированные с помощью функции ORM Inheritance, описанной в разделе Отображение иерархий наследования классов. Основное внимание будет уделено Наследование объединенных таблиц, так как это наиболее сложный случай ORM-запросов.
Выборка из базового класса в сравнении с конкретными подклассами¶
Оператор 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 для отображений с одним наследованием¶
Настройка наследования одной таблицы
В этом разделе рассматривается наследование по одной таблице, описанное в Наследование одной таблицы, которое использует одну таблицу для представления нескольких классов в иерархии.
В отличие от объединенных отображений наследования, построение операторов 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, ...]) |
Производит конструкцию |
- 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.