Техники загрузки отношений

Важной частью SQLAlchemy является предоставление широкого диапазона контроля над тем, как связанные объекты загружаются при запросе. Под «связанными объектами» мы подразумеваем коллекции или скалярные ассоциации, настроенные на маппере с помощью relationship(). Это поведение может быть настроено во время построения маппера с помощью параметра relationship.lazy к функции relationship(), а также с помощью опций объекта Query.

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

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

«Нет» загрузки относится к отключению загрузки для данного отношения, либо атрибут пуст и просто никогда не загружается, либо при обращении к нему возникает ошибка, чтобы защититься от нежелательной ленивой загрузки.

Основными формами загрузки отношений являются:

  • ленивая загрузка - доступна через lazy='select' или опцию lazyload(), это форма загрузки, которая выдает оператор SELECT во время доступа к атрибутам для ленивой загрузки связанной ссылки на один объект за раз. Ленивая загрузка подробно описана в Ленивая загрузка.

  • ** объединенная загрузка** - доступна через lazy='joined' или опцию joinedload(), эта форма загрузки применяет JOIN к заданному оператору SELECT так, что связанные строки загружаются в один и тот же набор результатов. Подробно об объединенной ускоренной загрузке рассказывается в Присоединился к Eager Loading.

  • загрузка подзапросов - доступна через lazy='subquery' или опцию subqueryload(), эта форма загрузки создает второй оператор SELECT, который переформулирует исходный запрос, встроенный в подзапрос, затем соединяет этот подзапрос со связанной таблицей, чтобы загрузить все члены связанных коллекций / скалярных ссылок одновременно. Подзапрос eager loading подробно описан в Подзапрос Eager Loading.

  • select IN loading - доступна через lazy='selectin' или опцию selectinload(), эта форма загрузки создает второй (или более) оператор SELECT, который собирает идентификаторы первичных ключей родительских объектов в предложение IN, так что все члены связанных коллекций / скалярных ссылок загружаются сразу по первичному ключу. Загрузка Select IN подробно описана в Выберите загрузку IN.

  • raise loading - доступна через lazy='raise', lazy='raise_on_sql' или опцию raiseload(), эта форма загрузки запускается в то же время, когда обычно происходит ленивая загрузка, за исключением того, что она вызывает исключение ORM, чтобы защитить приложение от нежелательной ленивой загрузки. Введение в повышение загрузки находится в Предотвращение нежелательных ленивых загрузок с помощью raiseload.

  • no loading - доступен через lazy='noload' или опцию noload(); этот стиль загрузки превращает атрибут в пустой атрибут (None или []), который никогда не будет загружаться или иметь какой-либо эффект загрузки. Эта редко используемая стратегия ведет себя подобно нетерпеливому загрузчику при загрузке объектов, когда помещается пустой атрибут или коллекция, но для истекших объектов полагается на значение атрибута по умолчанию, возвращаемое при доступе; чистый эффект тот же, за исключением того, появляется ли имя атрибута в коллекции InstanceState.unloaded. noload может быть полезен для реализации атрибута «только для записи», но это использование в настоящее время не тестируется и официально не поддерживается.

Настройка стратегий загрузчика во время отображения

Стратегия загрузчика для конкретного отношения может быть сконфигурирована во время отображения так, чтобы она применялась во всех случаях, когда загружается объект отображаемого типа, при отсутствии каких-либо изменяющих ее опций на уровне запроса. Это настраивается с помощью параметра relationship.lazy в relationship(); обычные значения для этого параметра включают select, joined, subquery и selectin.

Например, для настройки отношения на использование объединенной ускоренной загрузки, когда запрашивается родительский объект:

class Parent(Base):
    __tablename__ = "parent"

    id = Column(Integer, primary_key=True)
    children = relationship("Child", lazy="joined")

Выше, каждый раз, когда загружается коллекция объектов Parent, каждый Parent также будет заполнен коллекцией children, используя строки, полученные путем добавления JOIN к запросу для объектов Parent. Подробнее об этом стиле загрузки см. в Присоединился к Eager Loading.

По умолчанию аргумент relationship.lazy имеет значение "select", что указывает на ленивую загрузку. Дополнительную информацию см. в разделе Ленивая загрузка.

Загрузка отношений с помощью опций загрузчика

Другой и, возможно, более распространенный способ настройки стратегий загрузки - это настройка их на основе каждого запроса по определенным атрибутам с помощью метода Query.options(). Очень детальный контроль над загрузкой отношений доступен с помощью опций загрузчика; наиболее распространенными являются joinedload(), subqueryload(), selectinload() и lazyload(). Опция принимает либо строковое имя атрибута в сравнении с родителем, либо для большей специфичности может принимать атрибут, связанный с классом, напрямую:

# set children to load lazily
session.query(Parent).options(lazyload(Parent.children)).all()

# set children to load eagerly with a join
session.query(Parent).options(joinedload(Parent.children)).all()

Опции загрузчика также могут быть «объединены» с помощью метода chaining, чтобы определить, как загрузка должна происходить на более глубоких уровнях:

session.query(Parent).options(
    joinedload(Parent.children).subqueryload(Child.subelements)
).all()

Опции цепного загрузчика могут быть применены к «лениво» загруженной коллекции. Это означает, что когда коллекция или ассоциация лениво загружается при доступе, указанная опция вступает в силу:

session.query(Parent).options(
    lazyload(Parent.children).subqueryload(Child.subelements)
).all()

Выше, запрос вернет Parent объектов без загруженных коллекций children. При первом обращении к коллекции children на конкретном объекте Parent, она будет лениво загружать связанные объекты, но дополнительно применять ускоренную загрузку к коллекции subelements на каждом члене children.

Приведенные выше примеры, использующие Query, теперь называются запросами 1.x style. Система опций доступна и для запросов 2.0 style, использующих метод Select.options():

stmt = select(Parent).options(lazyload(Parent.children).subqueryload(Child.subelements))

result = session.execute(stmt)

Под капотом Query в конечном итоге использует вышеупомянутый механизм, основанный на select.

Добавление критериев к параметрам загрузчика

Атрибуты отношений, используемые для указания опций загрузчика, включают возможность добавления дополнительных критериев фильтрации в предложение ON создаваемого соединения или в критерии WHERE, в зависимости от стратегии загрузчика. Этого можно достичь с помощью метода PropComparator.and_(), который передаст опцию таким образом, что загружаемые результаты будут ограничены заданными критериями фильтрации:

session.query(A).options(lazyload(A.bs.and_(B.id > 5)))

При использовании ограничивающих критериев, если определенная коллекция уже загружена, она не будет обновлена; чтобы убедиться, что новые критерии имеют место, примените опцию Query.populate_existing():

session.query(A).options(lazyload(A.bs.and_(B.id > 5))).populate_existing()

Для того чтобы добавить критерии фильтрации ко всем вхождениям сущности во всем запросе, независимо от стратегии загрузчика или места ее появления в процессе загрузки, смотрите функцию with_loader_criteria().

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

Указание подпараметров с помощью Load.options()

При использовании цепочки методов стиль загрузчика каждой ссылки в пути указывается в явном виде. Для перемещения по пути без изменения существующего стиля загрузчика конкретного атрибута можно использовать метод/функцию defaultload():

session.query(A).options(defaultload(A.atob).joinedload(B.btoc)).all()

Аналогичный подход можно использовать для указания сразу нескольких подвариантов, используя метод Load.options():

session.query(A).options(
    defaultload(A.atob).options(joinedload(B.btoc), joinedload(B.btod))
).all()

Добавлено в версии 1.3.6: добавлено Load.options()

См.также

Отложенная загрузка по нескольким предприятиям - иллюстрирует примеры комбинирования опций загрузчика, ориентированного на отношения и столбцы.

Примечание

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

session.query(Parent).options(
    lazyload(Parent.children).subqueryload(Child.subelements)
).all()

Если срок действия коллекции children на конкретном объекте Parent, загруженном приведенным выше запросом, истек (например, когда транзакция объекта Session зафиксирована или откачена, или используется Session.expire_all()), при следующем обращении к коллекции Parent.children для ее повторной загрузки коллекция Child.subelements снова будет загружена с помощью подзапроса eager loading. Это сохраняется, даже если к вышеупомянутому объекту Parent обращаются из последующего запроса, в котором указан другой набор опций. Чтобы изменить опции существующего объекта без его удаления и повторной загрузки, они должны быть заданы явно в сочетании с методом Query.populate_existing():

# change the options on Parent objects that were already loaded
session.query(Parent).populate_existing().options(
    lazyload(Parent.children).lazyload(Child.subelements)
).all()

Если объекты, загруженные выше, полностью очищены от Session, например, из-за сборки мусора или того, что использовался Session.expunge_all(), «липкие» опции также исчезнут, и вновь созданные объекты будут использовать новые опции при повторной загрузке.

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

Ленивая загрузка

По умолчанию все межобъектные связи имеют ленивую загрузку. Скалярный атрибут или атрибут коллекции, связанный с relationship(), содержит триггер, который срабатывает при первом обращении к атрибуту. Этот триггер обычно выполняет вызов SQL в точке доступа, чтобы загрузить связанный объект или объекты:

>>> jack.addresses
SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE ? = addresses.user_id [5]
[<Address(u'jack@google.com')>, <Address(u'j25@yahoo.com')>]

Единственный случай, когда SQL не выдается, это для простых отношений «многие-к-одному», когда связанный объект может быть идентифицирован только по первичному ключу, и этот объект уже присутствует в текущем Session. По этой причине, хотя ленивая загрузка может быть дорогой для связанных коллекций, в случае, когда загружается множество объектов с простыми отношениями «многие-к-одному» против относительно небольшого набора возможных целевых объектов, ленивая загрузка может быть способна ссылаться на эти объекты локально, не выдавая столько операторов SELECT, сколько существует родительских объектов.

Такое поведение по умолчанию «загрузка при обращении к атрибуту» известно как «ленивая» или «выборочная» загрузка - название «выборочная» потому, что оператор «SELECT» обычно выдается при первом обращении к атрибуту.

Ленивая загрузка может быть включена для данного атрибута, который обычно настроен другим способом, с помощью опции lazyload() loader:

from sqlalchemy.orm import lazyload

# force lazy loading for an attribute that is set to
# load some other way normally
session.query(User).options(lazyload(User.addresses))

Предотвращение нежелательных ленивых загрузок с помощью raiseload

Стратегия lazyload() вызывает эффект, который является одной из наиболее распространенных проблем, упоминаемых в объектно-реляционном отображении; N plus one problem, который гласит, что для любых N объектов, доступ к их атрибутам, загруженным в ленивом режиме, означает, что будет выпущено N+1 операторов SELECT. В SQLAlchemy для решения проблемы N+1 обычно используется система ускоренной загрузки. Однако ускоренная загрузка требует, чтобы атрибуты, которые должны быть загружены, были указаны с помощью Query заранее. Проблема кода, который может получить доступ к другим атрибутам, не загруженным с нетерпением, когда ленивая загрузка нежелательна, может быть решена с помощью стратегии raiseload(); эта стратегия загрузчика заменяет поведение ленивой загрузки выдачей информативной ошибки:

from sqlalchemy.orm import raiseload

session.query(User).options(raiseload(User.addresses))

Объект User, загруженный из приведенного выше запроса, не будет иметь загруженной коллекции .addresses; если какой-либо код позже попытается получить доступ к этому атрибуту, возникнет исключение ORM.

raiseload() может использоваться с так называемым спецификатором «подстановочного знака», чтобы указать, что все отношения должны использовать эту стратегию. Например, чтобы установить только один атрибут как eager loading, а все остальные как raise:

session.query(Order).options(joinedload(Order.items), raiseload("*"))

Приведенный выше подстановочный знак будет применяться ко все отношениям не только на Order кроме items, но и ко всем отношениям на объектах Item. Чтобы установить raiseload() только для объектов Order, укажите полный путь с помощью Load:

from sqlalchemy.orm import Load

session.query(Order).options(joinedload(Order.items), Load(Order).raiseload("*"))

И наоборот, чтобы установить повышение только для объектов Item:

session.query(Order).options(joinedload(Order.items).raiseload("*"))

Опция raiseload() применяется только к атрибутам отношений. Для атрибутов, ориентированных на столбцы, опция defer() поддерживает опцию defer.raiseload, которая работает аналогичным образом.

Изменено в версии 1.4.0: Стратегии «raiseload» не происходят в процессе смыва единицы работы, начиная с SQLAlchemy 1.4.0. Это означает, что если единице работы необходимо загрузить определенный атрибут для завершения своей работы, она выполнит загрузку. Не всегда легко предотвратить загрузку конкретного отношения в рамках процесса UOW, особенно в случае менее распространенных типов отношений. Случай lazy=»raise» больше предназначен для явного доступа к атрибутам в пространстве приложения.

Присоединился к Eager Loading

Объединенная ускоренная загрузка - это самый фундаментальный стиль ускоренной загрузки в ORM. Он работает путем подключения JOIN (по умолчанию LEFT OUTER join) к оператору SELECT, эмитируемому Query, и заполняет целевой скаляр/коллекцию из того же набора результатов, что и родительский.

На уровне отображения это выглядит следующим образом:

class Address(Base):
    # ...

    user = relationship(User, lazy="joined")

Joined eager loading обычно применяется как опция к запросу, а не как опция загрузки по умолчанию на отображении, в частности, когда используется для коллекций, а не для ссылок «многие к одному». Это достигается с помощью опции загрузчика joinedload():

>>> jack = (
...     session.query(User).options(joinedload(User.addresses)).filter_by(name="jack").all()
... )
SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? ['jack']

JOIN, создаваемый по умолчанию, является LEFT OUTER JOIN, чтобы позволить ведущему объекту не ссылаться на связанную строку. Для атрибута, который гарантированно имеет элемент, например, ссылка «многие-к-одному» на связанный объект, где ссылающийся внешний ключ NOT NULL, запрос можно сделать более эффективным, используя внутреннее соединение; это доступно на уровне отображения с помощью флага relationship.innerjoin:

class Address(Base):
    # ...

    user_id = Column(ForeignKey("users.id"), nullable=False)
    user = relationship(User, lazy="joined", innerjoin=True)

На уровне опций запроса, с помощью флага joinedload.innerjoin:

session.query(Address).options(joinedload(Address.user, innerjoin=True))

При применении JOIN в цепочке, включающей OUTER JOIN, JOIN будет гнездиться вправо:

>>> session.query(User).options(
...     joinedload(User.addresses).joinedload(Address.widgets, innerjoin=True)
... ).all()
SELECT widgets_1.id AS widgets_1_id, widgets_1.name AS widgets_1_name, addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users LEFT OUTER JOIN ( addresses AS addresses_1 JOIN widgets AS widgets_1 ON addresses_1.widget_id = widgets_1.id ) ON users.id = addresses_1.user_id

В старых версиях SQLite вышеупомянутый вложенный правый JOIN может быть преобразован как вложенный подзапрос. Старые версии SQLAlchemy во всех случаях преобразуют вложенные справа JOIN в подзапросы.

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

Использование with_for_update в контексте отношений нетерпеливой загрузки официально не поддерживается и не рекомендуется SQLAlchemy и может не работать с некоторыми запросами на различных бэкендах баз данных. Когда with_for_update успешно используется с запросом, включающим joinedload(), SQLAlchemy попытается выдать SQL, который блокирует все задействованные таблицы.

Joined eager loading и пакетная обработка наборов результатов

Центральной концепцией объединенной ускоренной загрузки при применении к коллекциям является то, что объект Query должен де-дублировать строки относительно ведущей сущности, которая запрашивается. Например, если бы объект User, который мы загрузили, ссылался на три объекта Address, результат SQL-оператора имел бы три строки; однако Query возвращает только один объект User. По мере получения дополнительных строк для объекта User, только что загруженного в предыдущей строке, дополнительные столбцы, ссылающиеся на новые объекты Address, направляются в дополнительные результаты в коллекции User.addresses данного конкретного объекта.

Этот процесс очень прозрачен, однако он означает, что объединенная ускоренная загрузка несовместима с «пакетными» результатами запросов, предоставляемыми методом Query.yield_per() при использовании для загрузки коллекции. Однако объединенная ускоренная загрузка, используемая для скалярных ссылок, совместима с Query.yield_per(). Метод Query.yield_per() приведет к исключению, если используется объединенный ускоренный загрузчик на основе коллекции.

Для «пакетной» обработки запросов с произвольно большими наборами результатов, сохраняя при этом совместимость с коллекцией на основе объединенной ускоренной загрузки, выполните несколько операторов SELECT, каждый из которых ссылается на подмножество строк с помощью условия WHERE, например, в оконном режиме. В качестве альтернативы рассмотрите возможность использования ускоренной загрузки «select IN», которая потенциально совместима с Query.yield_per(), при условии, что используемый драйвер базы данных поддерживает несколько одновременных курсоров (драйверы SQLite, PostgreSQL, но не MySQL или SQL Server ODBC).

Дзен присоединенной загрузки

Поскольку объединенная ускоренная загрузка имеет много сходства с использованием Query.join(), она часто вызывает путаницу в том, когда и как ее следует использовать. Очень важно понять различие: в то время как Query.join() используется для изменения результатов запроса, joinedload() делает все возможное, чтобы не изменить результаты запроса, и вместо этого скрывает эффекты визуализированного соединения, позволяя присутствовать только связанным объектам.

Философия стратегий загрузчика заключается в том, что к конкретному запросу можно применить любой набор схем загрузки, и результаты не изменятся - меняется только количество SQL-запросов, необходимых для полной загрузки связанных объектов и коллекций. В начале конкретный запрос может использовать все ленивые загрузки. После его использования в контексте может выясниться, что к определенным атрибутам или коллекциям обращаются постоянно, и что было бы эффективнее изменить стратегию загрузчика для них. Стратегия может быть изменена без каких-либо других изменений в запросе, результаты останутся идентичными, но будет выдано меньше SQL-запросов. В теории (и практически на практике) ничто, что вы можете сделать с Query, не заставит его загрузить другой набор первичных или связанных объектов на основе изменения стратегии загрузчика.

Как joinedload(), в частности, достигает этого результата, не оказывая никакого влияния на возвращаемые строки сущности, заключается в том, что он создает анонимный псевдоним соединений, которые он добавляет к вашему запросу, так что на них не могут ссылаться другие части запроса. Например, запрос ниже использует joinedload() для создания LEFT OUTER JOIN от users к addresses, однако ORDER BY, добавленный к Address.email_address, не действителен - сущность Address не названа в запросе:

>>> jack = (
...     session.query(User)
...     .options(joinedload(User.addresses))
...     .filter(User.name == "jack")
...     .order_by(Address.email_address)
...     .all()
... )
SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? ORDER BY addresses.email_address <-- this part is wrong ! ['jack']

Выше, ORDER BY addresses.email_address не является корректным, поскольку addresses отсутствует в списке FROM. Правильный способ загрузить записи User и упорядочить их по адресу электронной почты - использовать Query.join():

>>> jack = (
...     session.query(User)
...     .join(User.addresses)
...     .filter(User.name == "jack")
...     .order_by(Address.email_address)
...     .all()
... )
SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id WHERE users.name = ? ORDER BY addresses.email_address ['jack']

Приведенное выше утверждение, конечно, отличается от предыдущего тем, что столбцы из addresses вообще не включаются в результат. Мы можем добавить joinedload() обратно, чтобы было два джоина - один тот, по которому мы упорядочиваем, другой используется анонимно для загрузки содержимого коллекции User.addresses:

>>> jack = (
...     session.query(User)
...     .join(User.addresses)
...     .options(joinedload(User.addresses))
...     .filter(User.name == "jack")
...     .order_by(Address.email_address)
...     .all()
... )
SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? ORDER BY addresses.email_address ['jack']

Выше мы видим, что мы используем Query.join() для того, чтобы предоставить условия JOIN, которые мы хотели бы использовать в последующих критериях запроса, в то время как использование joinedload() касается только загрузки коллекции User.addresses для каждого User в результате. В этом случае два джойна, скорее всего, кажутся избыточными, что и происходит. Если бы мы хотели использовать только один JOIN для загрузки коллекции и упорядочивания, мы бы использовали опцию contains_eager(), описанную ниже в Маршрутизация явных соединений/заявлений в загружаемые коллекции. Но чтобы понять, почему joinedload() делает то, что делает, рассмотрим, если бы мы фильтровали на конкретном Address:

>>> jack = (
...     session.query(User)
...     .join(User.addresses)
...     .options(joinedload(User.addresses))
...     .filter(User.name == "jack")
...     .filter(Address.email_address == "someaddress@foo.com")
...     .all()
... )
SELECT addresses_1.id AS addresses_1_id, addresses_1.email_address AS addresses_1_email_address, addresses_1.user_id AS addresses_1_user_id, users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id LEFT OUTER JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id WHERE users.name = ? AND addresses.email_address = ? ['jack', 'someaddress@foo.com']

Выше мы видим, что два JOIN имеют совершенно разные роли. Один будет соответствовать ровно одной строке, которая является соединением User и Address, где Address.email_address=='someaddress@foo.com'. Другой LEFT OUTER JOIN будет соответствовать все строкам Address, относящимся к User, и используется только для заполнения коллекции User.addresses для тех объектов User, которые возвращаются.

Изменив использование joinedload() на другой стиль загрузки, мы можем изменить способ загрузки коллекции полностью независимо от SQL, используемого для получения нужных нам строк User. Ниже мы изменим joinedload() на subqueryload():

>>> jack = (
...     session.query(User)
...     .join(User.addresses)
...     .options(subqueryload(User.addresses))
...     .filter(User.name == "jack")
...     .filter(Address.email_address == "someaddress@foo.com")
...     .all()
... )
SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users JOIN addresses ON users.id = addresses.user_id WHERE users.name = ? AND addresses.email_address = ? ['jack', 'someaddress@foo.com'] # ... subqueryload() emits a SELECT in order # to load all address records ...

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

Подзапрос Eager Loading

Ускоренная загрузка Subqueryload настраивается так же, как и объединенная ускоренная загрузка; для параметра relationship.lazy мы указываем "subquery", а не "joined", а для опции мы используем опцию subqueryload(), а не joinedload().

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

>>> jack = (
...     session.query(User)
...     .options(subqueryload(User.addresses))
...     .filter_by(name="jack")
...     .all()
... )
SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users WHERE users.name = ? ('jack',) SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id, anon_1.users_id AS anon_1_users_id FROM ( SELECT users.id AS users_id FROM users WHERE users.name = ?) AS anon_1 JOIN addresses ON anon_1.users_id = addresses.user_id ORDER BY anon_1.users_id, addresses.id ('jack',)

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

К недостаткам подзапроса можно отнести то, что сложность исходного запроса переносится на запросы отношений, что в сочетании с использованием подзапроса может на некоторых бэкендах в некоторых случаях (в частности, MySQL) приводить к значительному замедлению запросов. Кроме того, стратегия загрузки подзапросов может загружать только полное содержимое всех коллекций одновременно, поэтому она несовместима с «пакетной» загрузкой, обеспечиваемой Query.yield_per(), как для коллекций, так и для скалярных отношений.

Более новый стиль загрузки, предоставляемый selectinload(), решает эти ограничения subqueryload().

Важность заказа

Запрос, использующий subqueryload() в сочетании с ограничивающим модификатором, таким как Query.first(), Query.limit() или Query.offset(), должен всегда включать Query.order_by() против уникального столбца(ов), такого как первичный ключ, чтобы дополнительные запросы, выдаваемые subqueryload(), включали то же упорядочивание, которое используется родительским запросом. Без этого есть вероятность, что внутренний запрос может вернуть неправильные строки:

# incorrect, no ORDER BY
session.query(User).options(subqueryload(User.addresses)).first()

# incorrect if User.name is not unique
session.query(User).options(subqueryload(User.addresses)).order_by(User.name).first()

# correct
session.query(User).options(subqueryload(User.addresses)).order_by(
    User.name, User.id
).first()

Выберите загрузку IN

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

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

Загрузка «Select IN» обеспечивается с помощью аргумента "selectin" к relationship.lazy или с помощью опции загрузчика selectinload(). Этот стиль загрузки выдает SELECT, который ссылается на значения первичного ключа родительского объекта или, в случае отношения «многие-к-одному», на значения дочерних объектов, внутри предложения IN, чтобы загрузить связанные ассоциации:

>>> jack = (
...     session.query(User)
...     .options(selectinload(User.addresses))
...     .filter(or_(User.name == "jack", User.name == "ed"))
...     .all()
... )
SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users WHERE users.name = ? OR users.name = ? ('jack', 'ed') SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses WHERE addresses.user_id IN (?, ?) (5, 7)

Выше, второй SELECT относится к addresses.user_id IN (5, 7), где «5» и «7» являются значениями первичных ключей для предыдущих двух загруженных объектов User; после того, как партия объектов полностью загружена, их значения первичных ключей вводятся в предложение IN для второго SELECT. Поскольку связь между User и Address имеет простое [1] условие первичного соединения и предусматривает, что значения первичного ключа для User могут быть получены из Address.user_id, в операторе вообще нет соединений или подзапросов.

Изменено в версии 1.3: selectin loading можно опустить JOIN для простой коллекции «один-ко-многим».

Для простых загрузок [1] многие-к-одному, JOIN также не нужен, так как используется значение внешнего ключа из родительского объекта:

>>> session.query(Address).options(selectinload(Address.user)).all()
SELECT addresses.id AS addresses_id, addresses.email_address AS addresses_email_address, addresses.user_id AS addresses_user_id FROM addresses SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname FROM users WHERE users.id IN (?, ?) (1, 2)

Изменено в версии 1.3.6: Загрузка selectin может также опускать JOIN для простых отношений «многие-к-одному».

Загрузка Select IN также поддерживает отношения «многие-ко-многим», где в настоящее время будет JOIN по всем трем таблицам, чтобы сопоставить строки с одной стороны с другой.

О таком виде погрузки следует знать следующее:

  • Оператор SELECT, выдаваемый стратегией загрузчика «selectin», в отличие от «subquery», не требует подзапроса и не наследует никаких ограничений производительности исходного запроса; поиск является простым поиском первичного ключа и должен иметь высокую производительность.

  • Особые требования к упорядочиванию subqueryload, описанные в Важность заказа, также не применяются к selectin-загрузке; selectin всегда ссылается непосредственно на родительский первичный ключ и не может вернуть неверный результат.

  • Загрузка «selectin», в отличие от объединенной или подзапросной загрузки, всегда выдает свой SELECT в терминах непосредственных родительских объектов, только что загруженных, а не исходного типа объекта на вершине цепочки. Таким образом, при многоуровневой загрузке «selectin» загрузка не требует никаких JOIN для простых отношений «один ко многим» или «многие к одному». В сравнении, загрузка с соединением и подзапросом всегда относится к нескольким JOINам вплоть до исходного родителя.

  • Стратегия выдает SELECT для 500 значений родительского первичного ключа за раз, поскольку первичные ключи преобразуются в большое выражение IN в SQL-запросе. Некоторые базы данных, например Oracle, имеют жесткое ограничение на размер выражения IN, и в целом размер строки SQL не должен быть произвольно большим.

  • Поскольку загрузка «selectin» опирается на IN, для отображения с составными первичными ключами необходимо использовать форму IN «tuple», которая выглядит как WHERE (table.column_a, table.column_b) IN ((?, ?), (?, ?), (?, ?)). Этот синтаксис в настоящее время не поддерживается на SQL Server, а для SQLite требуется как минимум версия 3.15. В SQLAlchemy нет специальной логики для предварительной проверки того, какие платформы поддерживают этот синтаксис, а какие нет; при запуске на неподдерживаемой платформе база данных немедленно выдаст ошибку. Преимущество SQLAlchemy в том, что если SQLAlchemy просто запускает SQL для того, чтобы он не сработал, то если конкретная база данных действительно начнет поддерживать этот синтаксис, она будет работать без каких-либо изменений в SQLAlchemy (как это было в случае с SQLite).

В целом, загрузка «selectin», вероятно, превосходит загрузку «subquery» eager по большинству параметров, за исключением синтаксических требований к составным первичным ключам и, возможно, того, что она может выдавать много операторов SELECT для больших наборов результатов. Как всегда, разработчики должны уделять время просмотру операторов и результатов, генерируемых их приложениями в процессе разработки, чтобы убедиться, что все работает эффективно.

Какой вид загрузки использовать?

Выбор типа загрузки обычно сводится к оптимизации компромисса между количеством выполнения SQL, сложностью SQL и количеством получаемых данных. Рассмотрим два примера: relationship(), который ссылается на коллекцию, и relationship(), который ссылается на скалярную ссылку «многие к одному».

  • Коллекция «Один ко многим

  • При использовании ленивой загрузки по умолчанию, если вы загрузите 100 объектов, а затем обратитесь к коллекции каждого из них, будет выдан 101 SQL-запрос, хотя каждый запрос обычно представляет собой простой SELECT без каких-либо объединений.

  • При использовании объединенной загрузки загрузка 100 объектов и их коллекций вызовет только один SQL-запрос. Однако общее количество извлеченных строк будет равно сумме размеров всех коллекций, плюс одна дополнительная строка для каждого родительского объекта, имеющего пустую коллекцию. Каждая строка также будет содержать полный набор столбцов, представленных родительскими объектами, повторяющийся для каждого элемента коллекции - SQLAlchemy не будет повторно получать эти столбцы, кроме столбцов первичного ключа, однако большинство DBAPI (за некоторыми исключениями) в любом случае будут передавать полные данные каждого родителя по проводу на клиентское соединение. Поэтому объединенная ускоренная загрузка имеет смысл только тогда, когда размер коллекций относительно невелик. LEFT OUTER JOIN также может быть более производительным по сравнению с INNER join.

  • При использовании загрузки подзапросов загрузка 100 объектов вызовет два SQL-запроса. Во втором операторе будет получено общее количество строк, равное сумме размеров всех коллекций. Используется INNER JOIN, и запрашивается минимум родительских столбцов, только первичные ключи. Поэтому загрузка подзапросов имеет смысл, когда коллекции больше.

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

  • Используя selectin loading, загрузка 100 объектов также выдаст два SQL-запроса, второй из которых обращается к 100 первичным ключам загруженных объектов. selectin loading, однако, выводит не более 500 значений первичных ключей в один SELECT-запрос; таким образом, для коллекции свинца, превышающей 500, будет выдан SELECT-запрос для каждой партии из 500 выбранных объектов.

  • Использование нескольких уровней глубины с селективной загрузкой не приводит к «картезианской» проблеме, которую имеют объединенные и подзапросы с жаждой загрузки; запросы для селективной загрузки имеют наилучшие характеристики производительности и наименьшее количество строк. Единственная оговорка заключается в том, что в зависимости от размера результата вывода может быть выдано более одного SELECT.

  • Загрузка selectin, в отличие от объединенной (при использовании коллекций) и нетерпеливой загрузки подзапросов (все виды отношений), потенциально совместима с пакетной обработкой наборов результатов, предоставляемой Query.yield_per() при условии наличия соответствующего драйвера базы данных, поэтому может позволить пакетную обработку больших наборов результатов.

  • Ссылка «Многие к одному

  • При использовании ленивой загрузки по умолчанию, загрузка 100 объектов, как в случае с коллекцией, приведет к 101 SQL-запросу. Однако из этого есть существенное исключение, заключающееся в том, что если ссылка «многие-к-одному» является простой ссылкой внешнего ключа на первичный ключ цели, каждая ссылка будет проверяться первой в текущей карте идентификации с помощью Query.get(). Таким образом, если коллекция объектов ссылается на относительно небольшой набор целевых объектов, или полный набор возможных целевых объектов уже загружен в сессию и на них имеются сильные ссылки, использование значения по умолчанию lazy=“select“ является наиболее эффективным способом.

  • При использовании объединенной загрузки загрузка 100 объектов вызовет только один SQL-запрос. Объединение будет представлять собой LEFT OUTER JOIN, а общее количество строк во всех случаях будет равно 100. Если вы знаете, что у каждого родителя определенно есть потомок (т.е. ссылка внешнего ключа NOT NULL), то объединенную загрузку можно настроить так, чтобы relationship.innerjoin было установлено значение True, которое обычно указывается в relationship(). Для загрузки объектов, где существует множество возможных целевых ссылок, которые, возможно, еще не были загружены, объединенная загрузка с помощью INNER JOIN является чрезвычайно эффективной.

  • При загрузке подзапросов будет выполнена вторая загрузка для всех дочерних объектов, поэтому при загрузке 100 объектов будет выполнено два SQL-запроса. Вероятно, здесь нет особых преимуществ по сравнению с объединенной загрузкой, за исключением, пожалуй, того, что при загрузке подзапросов можно использовать INNER JOIN во всех случаях, в то время как при объединенной загрузке требуется, чтобы внешний ключ был NOT NULL.

  • При селективной загрузке также будет выполнена вторая загрузка для всех дочерних объектов (и, как уже говорилось, для больших результатов она будет выдавать SELECT на 500 строк), поэтому для загрузки 100 объектов будет выполнено два SQL-запроса. Сам запрос все равно должен соединиться с родительской таблицей, так что опять же, нет особых преимуществ в селективной загрузке для многих к одному по сравнению с объединенной загрузкой, за исключением использования INNER JOIN во всех случаях.

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

Поддерживается задание полиморфных опций для каждой отдельной нагрузки. Примеры использования метода Стремительная загрузка специфических или полиморфных подтипов в сочетании с функцией PropComparator.of_type() см. в разделе with_polymorphic().

Стратегии загрузки подстановочных знаков

Каждый из joinedload(), subqueryload(), lazyload(), selectinload(), noload() и raiseload() может быть использован для установки стиля загрузки relationship() по умолчанию для конкретного запроса, влияющего на все relationship() - сопоставленные атрибуты, не указанные в Query. Эта возможность доступна при передаче строки '*' в качестве аргумента к любой из этих опций:

session.query(MyClass).options(lazyload("*"))

Выше, опция lazyload('*') заменит установку lazy всех конструкций relationship(), используемых для данного запроса, за исключением тех, которые используют стиль загрузки 'dynamic'. Если некоторые отношения указывают lazy='joined' или lazy='subquery', например, использование lazyload('*') в одностороннем порядке заставит все эти отношения использовать загрузку 'select', например, выдавать оператор SELECT при обращении к каждому атрибуту.

Опция не заменяет опции загрузчика, указанные в запросе, такие как eagerload(), subqueryload() и т.д. В приведенном ниже запросе для отношения widget по-прежнему будет использоваться объединенная загрузка:

session.query(MyClass).options(lazyload("*"), joinedload(MyClass.widget))

Если передано несколько опций '*', последняя из них отменяет ранее переданные.

Стратегии загрузки подстановочных знаков для каждого объекта

Вариантом стратегии загрузчика подстановочных знаков является возможность задавать стратегию на основе каждого объекта. Например, при запросе User и Address мы можем указать всем отношениям на Address использовать только ленивую загрузку, сначала применив объект Load, а затем указав * в качестве цепочки опций:

session.query(User, Address).options(Load(Address).lazyload("*"))

Выше, все отношения на Address будут установлены на ленивую загрузку.

Маршрутизация явных соединений/заявлений в загружаемые коллекции

Поведение joinedload() таково, что соединения создаются автоматически, используя анонимные псевдонимы в качестве целей, результаты которых направляются в коллекции и скалярные ссылки на загруженные объекты. Часто бывает так, что запрос уже включает необходимые соединения, которые представляют определенную коллекцию или скалярную ссылку, и соединения, добавленные функцией joinedload, являются избыточными - но вы все равно хотите, чтобы коллекции/ссылки были заполнены.

Для этого SQLAlchemy предоставляет опцию contains_eager(). Эта опция используется так же, как и опция joinedload(), за исключением того, что предполагается, что в Query будут явно указаны соответствующие соединения. Ниже мы указываем соединение между User и Address и дополнительно устанавливаем его в качестве основы для нетерпеливой загрузки User.addresses:

class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)
    addresses = relationship("Address")


class Address(Base):
    __tablename__ = "address"

    # ...


q = session.query(User).join(User.addresses).options(contains_eager(User.addresses))

Если часть оператора «eager» является «aliased», путь должен быть указан с помощью PropComparator.of_type(), что позволяет передать конкретную конструкцию aliased():

# use an alias of the Address entity
adalias = aliased(Address)

# construct a Query object which expects the "addresses" results
query = (
    session.query(User)
    .outerjoin(User.addresses.of_type(adalias))
    .options(contains_eager(User.addresses.of_type(adalias)))
)

# get results normally
r = query.all()
SELECT users.user_id AS users_user_id, users.user_name AS users_user_name, adalias.address_id AS adalias_address_id, adalias.user_id AS adalias_user_id, adalias.email_address AS adalias_email_address, (...other columns...) FROM users LEFT OUTER JOIN email_addresses AS email_addresses_1 ON users.user_id = email_addresses_1.user_id

Путь, указанный в качестве аргумента для contains_eager(), должен быть полным путем от начальной сущности. Например, если бы мы загружали Users->orders->Order->items->Item, опция использовалась бы как:

query(User).options(contains_eager(User.orders).contains_eager(Order.items))

Использование contains_eager() для загрузки результата коллекции с пользовательской фильтрацией

Когда мы используем contains_eager(), мы создаем себе SQL, который будет использоваться для заполнения коллекций. Отсюда естественно следует, что мы можем выбрать модификацию значений, предназначенных для хранения коллекции, написав наш SQL для загрузки подмножества элементов для коллекций или скалярных атрибутов.

В качестве примера, мы можем загрузить объект User и нетерпеливо загрузить только определенные адреса в его коллекцию .addresses, фильтруя объединенные данные, маршрутизируя их с помощью contains_eager(), также используя Query.populate_existing() для обеспечения перезаписи любых уже загруженных коллекций:

q = (
    session.query(User)
    .join(User.addresses)
    .filter(Address.email_address.like("%@aol.com"))
    .options(contains_eager(User.addresses))
    .populate_existing()
)

Приведенный выше запрос загрузит только объекты User, которые содержат по крайней мере Address объект, содержащий подстроку 'aol.com' в своем поле email; коллекция User.addresses будет содержать только эти записи Address, и не любые другие записи Address, которые на самом деле связаны с коллекцией.

Совет

Во всех случаях SQLAlchemy ORM не перезаписывает уже загруженные атрибуты и коллекции, если это не указано. Поскольку используется identity map, часто бывает так, что запрос ORM возвращает объекты, которые на самом деле уже присутствовали и были загружены в память. Поэтому, при использовании contains_eager() для заполнения коллекции альтернативным способом, обычно полезно использовать Query.populate_existing(), как показано выше, чтобы уже загруженная коллекция была обновлена новыми данными. Query.populate_existing() сбросит все атрибуты, которые уже присутствовали, включая ожидающие изменения, поэтому перед его использованием убедитесь, что все данные сброшены. Достаточно использовать Session с его поведением по умолчанию autoflush.

Примечание

Настроенная коллекция, которую мы загружаем с помощью contains_eager(), не является «липкой»; то есть, при следующей загрузке этой коллекции она будет загружена с обычным содержимым по умолчанию. Коллекция может быть перезагружена, если срок действия объекта истек, что происходит всякий раз, когда используются методы Session.commit(), Session.rollback(), предполагающие настройки сессии по умолчанию, или методы Session.expire_all() или Session.expire().

Создание пользовательских правил загрузки

Deep Alchemy

Это продвинутая техника! Следует проявлять большую осторожность и проводить испытания.

ORM имеет различные граничные случаи, когда значение атрибута доступно локально, однако сам ORM об этом не знает. Существуют также случаи, когда желательна определяемая пользователем система загрузки атрибутов. Для поддержки случаев использования пользовательских систем загрузки предусмотрена ключевая функция set_committed_value(). Эта функция в основном эквивалентна собственной функции Python setattr(), за исключением того, что при ее применении к целевому объекту обходится система «истории атрибутов» SQLAlchemy, которая используется для определения изменений во время промывки; атрибут назначается таким же образом, как если бы ORM загружал его из базы данных.

Использование set_committed_value() может быть объединено с другим ключевым событием, известным как InstanceEvents.load(), для создания поведения атрибутов-населения при загрузке объекта. Одним из таких примеров является двунаправленный случай «один-к-одному», когда загрузка стороны «многие-к-одному» объекта «один-к-одному» должна также подразумевать значение стороны «один-ко-многим». SQLAlchemy ORM не учитывает обратные ссылки при загрузке связанных объектов, и рассматривает «один-к-одному» как еще один «один-ко-многим», который просто оказывается одной строкой.

Учитывая следующее отображение:

from sqlalchemy import Integer, ForeignKey, Column
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    b = relationship("B", backref=backref("a", uselist=False), lazy="joined")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)

Если мы запросим строку A, а затем запросим a.b.a, мы получим дополнительный SELECT:

>>> a1.b.a
SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE ? = a.b_id

Этот SELECT является избыточным, поскольку b.a является тем же значением, что и a1. Мы можем создать правило загрузки, которое будет заполнять его для нас:

from sqlalchemy import event
from sqlalchemy.orm import attributes


@event.listens_for(A, "load")
def load_b(target, context):
    if "b" in target.__dict__:
        attributes.set_committed_value(target.b, "a", target)

Теперь при запросе A мы получим A.b из объединенного eager load, и A.b.a из нашего события:

a1 = s.query(A).first()
SELECT a.id AS a_id, a.b_id AS a_b_id, b_1.id AS b_1_id FROM a LEFT OUTER JOIN b AS b_1 ON b_1.id = a.b_id LIMIT ? OFFSET ? (1, 0)
assert a1.b.a is a1

API загрузчика отношений

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