Работа с большими коллекциями

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

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

Этот раздел включает функции API, предназначенные для того, чтобы позволить использовать relationship() с большими коллекциями, сохраняя при этом достаточную производительность.

Писать только отношения

Стратегия загрузчика write only является основным средством конфигурирования relationship(), который будет оставаться доступным для записи, но не будет загружать свое содержимое в память. Ниже показана конфигурация ORM только для записи в современной декларативной форме с указанием типов:

>>> from decimal import Decimal
>>> from datetime import datetime

>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import func
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship
>>> from sqlalchemy.orm import Session
>>> from sqlalchemy.orm import WriteOnlyMapped

>>> class Base(DeclarativeBase):
...     pass

>>> class Account(Base):
...     __tablename__ = "account"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     identifier: Mapped[str]
...
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         cascade="all, delete-orphan",
...         passive_deletes=True,
...         order_by="AccountTransaction.timestamp",
...     )
...
...     def __repr__(self):
...         return f"Account(identifier={self.identifier!r})"

>>> class AccountTransaction(Base):
...     __tablename__ = "account_transaction"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_id: Mapped[int] = mapped_column(
...         ForeignKey("account.id", ondelete="cascade")
...     )
...     description: Mapped[str]
...     amount: Mapped[Decimal]
...     timestamp: Mapped[datetime] = mapped_column(default=func.now())
...
...     def __repr__(self):
...         return (
...             f"AccountTransaction(amount={self.amount:.2f}, "
...             f"timestamp={self.timestamp.isoformat()!r})"
...         )
...
...     __mapper_args__ = {"eager_defaults": True}

Выше отношение account_transactions настроено не с помощью обычной аннотации Mapped, а с помощью аннотации WriteOnlyMapped типа, которая во время выполнения присвоит loader strategy из lazy="write_only" целевому relationship(). Аннотация WriteOnlyMapped является альтернативной формой аннотации Mapped, которая указывает на использование типа коллекции WriteOnlyCollection на экземплярах объекта.

Приведенная выше конфигурация relationship() также включает несколько элементов, которые определяют, какие действия предпринимать при удалении объектов Account, а также при удалении объектов AccountTransaction из коллекции account_transactions. Этими элементами являются:

Добавлено в версии 2.0: Добавлены загрузчики отношений «Только запись».

Создание и сохранение новых коллекций, доступных только для записи

Коллекция только для записи позволяет прямое присвоение коллекции в целом только для объектов transient или pending. С нашим приведенным выше отображением это означает, что мы можем создать новый объект Account с последовательностью объектов AccountTransaction, которые будут добавлены в Session. В качестве источника объектов для начала может быть использована любая итерабельность Python, где ниже мы используем Python list:

>>> new_account = Account(
...     identifier="account_01",
...     account_transactions=[
...         AccountTransaction(description="initial deposit", amount=Decimal("500.00")),
...         AccountTransaction(description="transfer", amount=Decimal("1000.00")),
...         AccountTransaction(description="withdrawal", amount=Decimal("-29.50")),
...     ],
... )

>>> with Session(engine) as session:
...     session.add(new_account)
...     session.commit()
{execsql}BEGIN (implicit)
INSERT INTO account (identifier) VALUES (?)
[...] ('account_01',)
INSERT INTO account_transaction (account_id, description, amount, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, timestamp
[... (insertmanyvalues) 1/3 (ordered; batch not supported)] (1, 'initial deposit', 500.0)
INSERT INTO account_transaction (account_id, description, amount, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, timestamp
[insertmanyvalues 2/3 (ordered; batch not supported)] (1, 'transfer', 1000.0)
INSERT INTO account_transaction (account_id, description, amount, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, timestamp
[insertmanyvalues 3/3 (ordered; batch not supported)] (1, 'withdrawal', -29.5)
COMMIT

Как только объект помещен в базу данных (т.е. находится в состоянии persistent или detached), коллекция может быть расширена новыми элементами, а также может быть удалена. Однако коллекция больше не может быть переназначена с полной заменой коллекции, поскольку такая операция требует полной загрузки предыдущей коллекции в память для согласования старых записей с новыми:

>>> new_account.account_transactions = [
...     AccountTransaction(description="some transaction", amount=Decimal("10.00"))
... ]
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: Collection "Account.account_transactions" does not
support implicit iteration; collection replacement operations can't be used

Добавление новых предметов в существующую коллекцию

Для коллекций постоянных объектов, доступных только для записи, модификации коллекции с помощью процессов unit of work могут происходить только с использованием методов WriteOnlyCollection.add(), WriteOnlyCollection.add_all() и WriteOnlyCollection.remove():

>>> from sqlalchemy import select
>>> session = Session(engine, expire_on_commit=False)
>>> existing_account = session.scalar(select(Account).filter_by(identifier="account_01"))
{execsql}BEGIN (implicit)
SELECT account.id, account.identifier
FROM account
WHERE account.identifier = ?
[...] ('account_01',)
{stop}
>>> existing_account.account_transactions.add_all(
...     [
...         AccountTransaction(description="paycheck", amount=Decimal("2000.00")),
...         AccountTransaction(description="rent", amount=Decimal("-800.00")),
...     ]
... )
>>> session.commit()
{execsql}INSERT INTO account_transaction (account_id, description, amount, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, timestamp
[... (insertmanyvalues) 1/2 (ordered; batch not supported)] (1, 'paycheck', 2000.0)
INSERT INTO account_transaction (account_id, description, amount, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) RETURNING id, timestamp
[insertmanyvalues 2/2 (ordered; batch not supported)] (1, 'rent', -800.0)
COMMIT

Добавленные выше объекты хранятся в очереди ожидания внутри Session до следующей промывки, после чего они вставляются в базу данных, предполагая, что добавленные объекты были ранее transient.

Запрос элементов

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

Вместо этого, WriteOnlyCollection включает SQL-генерирующие помощники, такие как WriteOnlyCollection.select(), которые генерируют конструкцию Select, предварительно настроенную с правильными критериями WHERE / FROM для текущей родительской строки, которая затем может быть модифицирована для ВЫБОРА любого диапазона строк, а также вызвана с помощью таких функций, как server side cursors для процессов, которые хотят итерационно просмотреть всю коллекцию в эффективном для памяти режиме.

Сгенерированный оператор показан ниже. Обратите внимание, что он также включает критерий ORDER BY, обозначенный в примере отображения параметром relationship.order_by relationship(); этот критерий был бы опущен, если бы параметр не был настроен:

>>> print(existing_account.account_transactions.select())
{printsql}SELECT account_transaction.id, account_transaction.account_id, account_transaction.description,
account_transaction.amount, account_transaction.timestamp
FROM account_transaction
WHERE :param_1 = account_transaction.account_id ORDER BY account_transaction.timestamp

Мы можем использовать эту конструкцию Select вместе с Session для запроса объектов AccountTransaction, проще всего с помощью метода Session.scalars(), который вернет Result, непосредственно возвращающий объекты ORM. Типично, хотя и не обязательно, что Select будет модифицирован для ограничения возвращаемых записей; в примере ниже добавлены дополнительные критерии WHERE для загрузки только «дебетовых» операций по счету, а также «LIMIT 10» для получения только первых десяти строк:

>>> account_transactions = session.scalars(
...     existing_account.account_transactions.select()
...     .where(AccountTransaction.amount < 0)
...     .limit(10)
... ).all()
{execsql}BEGIN (implicit)
SELECT account_transaction.id, account_transaction.account_id, account_transaction.description,
account_transaction.amount, account_transaction.timestamp
FROM account_transaction
WHERE ? = account_transaction.account_id AND account_transaction.amount < ?
ORDER BY account_transaction.timestamp  LIMIT ? OFFSET ?
[...] (1, 0, 10, 0)
{stop}>>> print(account_transactions)
[AccountTransaction(amount=-29.50, timestamp='...'), AccountTransaction(amount=-800.00, timestamp='...')]

Удаление предметов

Отдельные элементы, загруженные в состоянии persistent против текущего Session, могут быть помечены для удаления из коллекции с помощью метода WriteOnlyCollection.remove(). При выполнении этой операции процесс flush будет неявно считать объект уже частью коллекции. Приведенный ниже пример иллюстрирует удаление отдельного элемента AccountTransaction, что в соответствии с настройками cascade приводит к DELETE этого ряда:

>>> existing_transaction = account_transactions[0]
>>> existing_account.account_transactions.remove(existing_transaction)
>>> session.commit()
{execsql}DELETE FROM account_transaction WHERE account_transaction.id = ?
[...] (3,)
COMMIT

Как и в случае с любой ORM-сопоставленной коллекцией, удаление объекта может привести либо к деассоциации объекта из коллекции, оставляя объект в базе данных, либо к выдаче DELETE для его строки, основываясь на конфигурации delete-orphan в relationship().

Удаление коллекции без удаления включает установку столбцов внешнего ключа в NULL для отношения one-to-many или удаление соответствующей строки ассоциации для отношения many-to-many.

Массовая вставка новых элементов

WriteOnlyCollection может генерировать конструкции DML, такие как объекты Insert, которые могут быть использованы в контексте ORM для создания поведения массовой вставки. См. раздел ORM Bulk INSERT Statements для обзора массовых вставок ORM.

Коллекции от одной до многих

Только для регулярной коллекции один ко многим метод WriteOnlyCollection.insert() создаст конструкцию Insert, в которой предварительно установлены критерии VALUES, соответствующие родительскому объекту. Поскольку этот критерий VALUES полностью относится к связанной таблице, оператор может быть использован для INSERT новых строк, которые в то же время станут новыми записями в связанной коллекции:

>>> session.execute(
...     existing_account.account_transactions.insert(),
...     [
...         {"description": "transaction 1", "amount": Decimal("47.50")},
...         {"description": "transaction 2", "amount": Decimal("-501.25")},
...         {"description": "transaction 3", "amount": Decimal("1800.00")},
...         {"description": "transaction 4", "amount": Decimal("-300.00")},
...     ],
... )
{execsql}BEGIN (implicit)
INSERT INTO account_transaction (account_id, description, amount, timestamp) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
[...] [(1, 'transaction 1', 47.5), (1, 'transaction 2', -501.25), (1, 'transaction 3', 1800.0), (1, 'transaction 4', -300.0)]
<...>
{stop}
>>> session.commit()
COMMIT

Коллекции «многие ко многим

Для коллекции many to many collection отношения между двумя классами включают третью таблицу, которая конфигурируется с помощью параметра relationship.secondary в relationship. Для массовой вставки строк в коллекцию такого типа с помощью WriteOnlyCollection, новые записи могут быть сначала вставлены отдельно, получены с помощью RETURNING, а затем эти записи переданы в метод WriteOnlyCollection.add_all(), где процесс единицы работы продолжит сохранять их как часть коллекции.

Предположим, что класс BankAudit ссылается на множество AccountTransaction записей с помощью таблицы «многие ко многим»:

>>> from sqlalchemy import Table, Column
>>> audit_to_transaction = Table(
...     "audit_transaction",
...     Base.metadata,
...     Column("audit_id", ForeignKey("audit.id", ondelete="CASCADE"), primary_key=True),
...     Column(
...         "transaction_id",
...         ForeignKey("account_transaction.id", ondelete="CASCADE"),
...         primary_key=True,
...     ),
... )
>>> class BankAudit(Base):
...     __tablename__ = "audit"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     account_transactions: WriteOnlyMapped["AccountTransaction"] = relationship(
...         secondary=audit_to_transaction, passive_deletes=True
...     )

Чтобы проиллюстрировать эти две операции, мы добавляем больше объектов AccountTransaction с помощью bulk insert, которые мы извлекаем с помощью RETURNING, добавляя returning(AccountTransaction) к оператору bulk INSERT (обратите внимание, что мы могли бы так же легко использовать существующие объекты AccountTransaction):

>>> new_transactions = session.scalars(
...     existing_account.account_transactions.insert().returning(AccountTransaction),
...     [
...         {"description": "odd trans 1", "amount": Decimal("50000.00")},
...         {"description": "odd trans 2", "amount": Decimal("25000.00")},
...         {"description": "odd trans 3", "amount": Decimal("45.00")},
...     ],
... ).all()
{execsql}BEGIN (implicit)
INSERT INTO account_transaction (account_id, description, amount, timestamp) VALUES
(?, ?, ?, CURRENT_TIMESTAMP), (?, ?, ?, CURRENT_TIMESTAMP), (?, ?, ?, CURRENT_TIMESTAMP)
RETURNING id, account_id, description, amount, timestamp
[...] (1, 'odd trans 1', 50000.0, 1, 'odd trans 2', 25000.0, 1, 'odd trans 3', 45.0)
{stop}

Когда список объектов AccountTransaction готов, метод WriteOnlyCollection.add_all() используется для связывания сразу многих строк с новым объектом BankAudit:

>>> bank_audit = BankAudit()
>>> session.add(bank_audit)
>>> bank_audit.account_transactions.add_all(new_transactions)
>>> session.commit()
{execsql}INSERT INTO audit DEFAULT VALUES
[...] ()
INSERT INTO audit_transaction (audit_id, transaction_id) VALUES (?, ?)
[...] [(1, 10), (1, 11), (1, 12)]
COMMIT

Массовое UPDATE и DELETE элементов

Подобно тому, как WriteOnlyCollection может генерировать конструкции Select с заранее установленными критериями WHERE, он также может генерировать конструкции Update и Delete с теми же критериями WHERE, чтобы позволить критериально-ориентированные операторы UPDATE и DELETE для элементов в большой коллекции.

Коллекции от одной до многих

Как и в случае с INSERT, эта функция наиболее проста при работе с коллекциями один ко многим.

В приведенном ниже примере метод WriteOnlyCollection.update() используется для генерации оператора UPDATE, который генерируется над элементами коллекции, находя строки, в которых «сумма» равна -800 и добавляя к ним сумму 200:

>>> session.execute(
...     existing_account.account_transactions.update()
...     .values(amount=AccountTransaction.amount + 200)
...     .where(AccountTransaction.amount == -800),
... )
{execsql}BEGIN (implicit)
UPDATE account_transaction SET amount=(account_transaction.amount + ?)
WHERE ? = account_transaction.account_id AND account_transaction.amount = ?
[...] (200, 1, -800)
{stop}<...>

Аналогичным образом, WriteOnlyCollection.delete() создаст оператор DELETE, который вызывается тем же способом:

>>> session.execute(
...     existing_account.account_transactions.delete().where(
...         AccountTransaction.amount.between(0, 30)
...     ),
... )
{execsql}DELETE FROM account_transaction WHERE ? = account_transaction.account_id
AND account_transaction.amount BETWEEN ? AND ? RETURNING id
[...] (1, 0, 30)
<...>
{stop}

Коллекции «многие ко многим

Совет

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

Для массового UPDATE и DELETE коллекций many to many collections, для того чтобы оператор UPDATE или DELETE относился к первичному ключу родительского объекта, таблица ассоциации должна быть явной частью оператора UPDATE/DELETE, что требует либо поддержки бэкендом нестандартных синтаксисов SQL, либо дополнительных явных шагов при построении оператора UPDATE или DELETE.

Для бэкендов, поддерживающих многотабличные версии UPDATE, метод WriteOnlyCollection.update() должен работать без дополнительных шагов для коллекции многие-ко-многим, как в примере ниже, где UPDATE выдается для объектов AccountTransaction в терминах коллекции многие-ко-многим BankAudit.account_transactions:

>>> session.execute(
...     bank_audit.account_transactions.update().values(
...         description=AccountTransaction.description + " (audited)"
...     )
... )
{execsql}UPDATE account_transaction SET description=(account_transaction.description || ?)
FROM audit_transaction WHERE ? = audit_transaction.audit_id
AND account_transaction.id = audit_transaction.transaction_id RETURNING id
[...] (' (audited)', 1)
{stop}<...>

Приведенный выше оператор автоматически использует синтаксис «UPDATE..FROM», поддерживаемый SQLite и другими, чтобы назвать дополнительную таблицу audit_transaction в предложении WHERE.

Для UPDATE или DELETE коллекции «многие-ко-многим», где синтаксис многотабличности недоступен, критерии «многие-ко-многим» могут быть перенесены в SELECT, который, например, может быть объединен с IN для сопоставления строк. Здесь нам по-прежнему помогает WriteOnlyCollection, поскольку мы используем метод WriteOnlyCollection.select() для создания этого SELECT, используя метод Select.with_only_columns() для создания scalar subquery:

>>> from sqlalchemy import update
>>> subq = bank_audit.account_transactions.select().with_only_columns(AccountTransaction.id)
>>> session.execute(
...     update(AccountTransaction)
...     .values(description=AccountTransaction.description + " (audited)")
...     .where(AccountTransaction.id.in_(subq))
... )
{execsql}UPDATE account_transaction SET description=(account_transaction.description || ?)
WHERE account_transaction.id IN (SELECT account_transaction.id
FROM audit_transaction
WHERE ? = audit_transaction.audit_id AND account_transaction.id = audit_transaction.transaction_id)
RETURNING id
[...] (' (audited)', 1)
<...>

Коллекции только для записи - Документация API

Object Name Description

WriteOnlyCollection

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

WriteOnlyMapped

Представляет сопоставленный ORM тип атрибута для отношения «только запись».

class sqlalchemy.orm.WriteOnlyCollection

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

WriteOnlyCollection используется в отображении при использовании стратегии ленивой загрузки "write_only" с relationship(). Подробнее об этой конфигурации см. в разделе Писать только отношения.

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

Классная подпись

класс sqlalchemy.orm.WriteOnlyCollection (sqlalchemy.orm.writeonly.AbstractCollectionWriter)

method sqlalchemy.orm.WriteOnlyCollection.add(item: _T) None

Добавьте элемент к этому WriteOnlyCollection.

Данный элемент будет сохранен в базе данных в терминах коллекции родительского экземпляра при следующем flush.

method sqlalchemy.orm.WriteOnlyCollection.add_all(iterator: Iterable[_T]) None

Добавляет итерабель элементов к данному WriteOnlyCollection.

Данные элементы будут сохранены в базе данных в терминах коллекции родительского экземпляра при следующей вспышке.

method sqlalchemy.orm.WriteOnlyCollection.delete() Delete

Произведите Delete, который будет ссылаться на строки в терминах данного экземпляра-локального WriteOnlyCollection.

method sqlalchemy.orm.WriteOnlyCollection.insert() Insert

Для коллекций «один-ко-многим» создайте Insert, который будет вставлять новые строки в терминах данного экземпляра-локального WriteOnlyCollection.

Эта конструкция поддерживается только для Relationship, который нет включает параметр relationship.secondary. Для отношений, которые ссылаются на таблицу «многие-ко-многим», используйте обычные методы массовой вставки для создания новых объектов, а затем используйте AbstractCollectionWriter.add_all(), чтобы связать их с коллекцией.

method sqlalchemy.orm.WriteOnlyCollection.remove(item: _T) None

Удалить элемент из этого WriteOnlyCollection.

Данный элемент будет удален из коллекции родительского экземпляра при следующей очистке.

method sqlalchemy.orm.WriteOnlyCollection.select() Select[Tuple[_T]]

Произведите конструкцию Select, которая представляет строки в данном экземпляре-локальном WriteOnlyCollection.

method sqlalchemy.orm.WriteOnlyCollection.update() Update

Произведите Update, который будет ссылаться на строки в терминах данного экземпляра-локального WriteOnlyCollection.

class sqlalchemy.orm.WriteOnlyMapped

Представляет сопоставленный ORM тип атрибута для отношения «только запись».

Аннотация типа WriteOnlyMapped может быть использована в отображении Annotated Declarative Table, чтобы указать, что стратегия загрузчика lazy="write_only" должна быть использована для конкретного relationship().

Например:

class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    addresses: WriteOnlyMapped[Address] = relationship(
        cascade="all,delete-orphan"
    )

Справочную информацию см. в разделе Писать только отношения.

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

См.также

Писать только отношения - полный фон

DynamicMapped - включает унаследованную поддержку Query

Классная подпись

класс sqlalchemy.orm.WriteOnlyMapped (sqlalchemy.orm.base._MappedAnnotationBase)

Динамические загрузчики отношений

Legacy Feature

Стратегия «динамического» ленивого загрузчика - это унаследованная форма того, что сейчас является стратегией «только запись», описанной в разделе Писать только отношения.

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

Динамический загрузчик также не совместим с расширением Асинхронный ввод/вывод (asyncio). Его можно использовать с некоторыми ограничениями, как указано в Asyncio dynamic guidelines, но опять же следует предпочесть WriteOnlyCollection, который полностью совместим с asyncio.

Стратегия динамических отношений позволяет конфигурировать relationship(), который при обращении к экземпляру возвращает наследуемый объект Query вместо коллекции. Затем Query может быть модифицирован таким образом, что коллекция базы данных может быть итерирована на основе критериев фильтрации. Возвращаемый объект Query является экземпляром AppenderQuery, который сочетает в себе поведение загрузки и итерации Query вместе с элементарными методами мутации коллекции, такими как AppenderQuery.append() и AppenderQuery.remove().

Стратегия «динамического» загрузчика может быть настроена с помощью декларативной формы с аннотацией типов, используя класс аннотаций DynamicMapped:

from sqlalchemy.orm import DynamicMapped


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    posts: DynamicMapped[Post] = relationship()

Выше, коллекция User.posts на отдельном объекте User вернет объект AppenderQuery, который является подклассом Query, который также поддерживает основные операции мутации коллекции:

jack = session.get(User, id)

# filter Jack's blog posts
posts = jack.posts.filter(Post.headline == "this is a post")

# apply array slices
posts = jack.posts[5:20]

Динамические отношения поддерживают ограниченные операции записи с помощью методов AppenderQuery.append() и AppenderQuery.remove():

oldpost = jack.posts.filter(Post.headline == "old post").one()
jack.posts.remove(oldpost)

jack.posts.append(Post("new post"))

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

Загрузчики динамических отношений - API

Object Name Description

AppenderQuery

Динамический запрос, поддерживающий основные операции хранения коллекции.

DynamicMapped

Представляет тип атрибута, сопоставленный ORM, для «динамического» отношения.

class sqlalchemy.orm.AppenderQuery

Динамический запрос, поддерживающий основные операции хранения коллекции.

Методы на AppenderQuery включают все методы Query, плюс дополнительные методы, используемые для сохранения коллекции.

Классная подпись

класс sqlalchemy.orm.AppenderQuery (sqlalchemy.orm.dynamic.AppenderMixin, sqlalchemy.orm.Query)

method sqlalchemy.orm.AppenderQuery.add(item: _T) None

наследуется от AppenderMixin.add() метода AppenderMixin

Добавьте элемент к этому AppenderQuery.

Данный элемент будет сохранен в базе данных в терминах коллекции родительского экземпляра при следующем flush.

Этот метод предоставляется для обеспечения совместимости с классом коллекции WriteOnlyCollection.

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

method sqlalchemy.orm.AppenderQuery.add_all(iterator: Iterable[_T]) None

наследуется от AppenderMixin.add_all() метода AppenderMixin

Добавляет итерабель элементов к данному AppenderQuery.

Данные элементы будут сохранены в базе данных в терминах коллекции родительского экземпляра при следующей вспышке.

Этот метод предоставляется для обеспечения совместимости с классом коллекции WriteOnlyCollection.

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

method sqlalchemy.orm.AppenderQuery.append(item: _T) None

наследуется от AppenderMixin.append() метода AppenderMixin

Добавьте элемент к этому AppenderQuery.

Данный элемент будет сохранен в базе данных в терминах коллекции родительского экземпляра при следующем flush.

method sqlalchemy.orm.AppenderQuery.count() int

наследуется от AppenderMixin.count() метода AppenderMixin

Возвращает количество строк, которые вернул бы SQL, сформированный этим Query.

В результате SQL для этого запроса будет выглядеть следующим образом:

SELECT count(1) AS count_1 FROM (
    SELECT <rest of query follows...>
) AS anon_1

Приведенный выше SQL возвращает один ряд, который является суммарным значением функции count; метод Query.count() затем возвращает это единственное целочисленное значение.

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

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

Для более тонкого контроля над конкретными столбцами для подсчета, чтобы пропустить использование подзапроса или иного контроля пункта FROM, или для использования других агрегатных функций, используйте выражения expression.func в сочетании с Session.query(), т.е.:

from sqlalchemy import func

# count User records, without
# using a subquery.
session.query(func.count(User.id))

# return count of user "id" grouped
# by "name"
session.query(func.count(User.id)).\
        group_by(User.name)

from sqlalchemy import distinct

# count distinct "name" values
session.query(func.count(distinct(User.name)))
method sqlalchemy.orm.AppenderQuery.extend(iterator: Iterable[_T]) None

наследуется от AppenderMixin.extend() метода AppenderMixin

Добавляет итерабель элементов к данному AppenderQuery.

Данные элементы будут сохранены в базе данных в терминах коллекции родительского экземпляра при следующей вспышке.

method sqlalchemy.orm.AppenderQuery.remove(item: _T) None

наследуется от AppenderMixin.remove() метода AppenderMixin

Удалить элемент из этого AppenderQuery.

Данный элемент будет удален из коллекции родительского экземпляра при следующей очистке.

class sqlalchemy.orm.DynamicMapped

Представляет тип атрибута, сопоставленный ORM, для «динамического» отношения.

Аннотация типа DynamicMapped может быть использована в отображении Annotated Declarative Table, чтобы указать, что стратегия загрузчика lazy="dynamic" должна быть использована для конкретного relationship().

Legacy Feature

Стратегия «динамического» ленивого загрузчика - это унаследованная форма того, что сейчас является стратегией «только запись», описанной в разделе Писать только отношения.

Например:

class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True)
    addresses: DynamicMapped[Address] = relationship(
        cascade="all,delete-orphan"
    )

Справочную информацию см. в разделе Динамические загрузчики отношений.

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

См.также

Динамические загрузчики отношений - полный фон

WriteOnlyMapped - полностью версия в стиле 2.0

Классная подпись

класс sqlalchemy.orm.DynamicMapped (sqlalchemy.orm.base._MappedAnnotationBase)

Установка RaiseLoad

Отношения с «raise»-загрузкой будут выдавать InvalidRequestError там, где атрибут обычно выдавал бы ленивую загрузку:

class MyClass(Base):
    __tablename__ = "some_table"

    # ...

    children: Mapped[List[MyRelatedClass]] = relationship(lazy="raise")

Выше, доступ к атрибутам коллекции children вызовет исключение, если она не была предварительно заполнена. Это относится к доступу на чтение, но для коллекций также влияет на доступ на запись, поскольку коллекции нельзя изменять без предварительной загрузки. Это необходимо для того, чтобы убедиться, что приложение не создает неожиданных ленивых загрузок в определенном контексте. Вместо того, чтобы читать журналы SQL, чтобы определить, что все необходимые атрибуты были загружены с нетерпением, стратегия «raise» заставит незагруженные атрибуты немедленно подниматься при обращении к ним. Стратегия «raise» также доступна на основе опции запроса с помощью опции загрузчика raiseload().

Использование пассивных удалений

Важным аспектом управления коллекциями в SQLAlchemy является то, что при удалении объекта, ссылающегося на коллекцию, SQLAlchemy необходимо учитывать объекты, находящиеся внутри этой коллекции. Эти объекты должны быть деассоциированы от родителя, что для коллекции типа «один ко многим» означает, что столбцы внешнего ключа устанавливаются в NULL, или, основываясь на настройках cascade, может потребоваться выдача DELETE для этих строк.

Процесс unit of work рассматривает объекты только по строкам, что означает, что операция DELETE подразумевает, что все строки коллекции должны быть полностью загружены в память в процессе flush. Это невыполнимо для больших коллекций, поэтому вместо этого мы стремимся полагаться на возможности базы данных автоматически обновлять или удалять строки с помощью правил внешних ключей ON DELETE, инструктируя блок работы отказаться от фактической загрузки этих строк для их обработки. Рабочий модуль может быть настроен на работу таким образом путем конфигурирования relationship.passive_deletes на конструкцию relationship(); используемые ограничения внешнего ключа также должны быть правильно настроены.

Более подробную информацию о полной конфигурации «пассивного удаления» см. в разделе Использование каскада внешних ключей ON DELETE с отношениями ORM.

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