Написание операторов 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.