Манипулирование данными с помощью ORM

Предыдущий раздел Работа с данными был посвящен языку SQL Expression Language с точки зрения Core, чтобы обеспечить преемственность между основными конструкциями операторов SQL. Далее в этом разделе будет рассмотрен жизненный цикл Session и его взаимодействие с этими конструкциями.

Предварительные разделы - часть учебника, посвященная ORM, опирается на два предыдущих раздела этого документа, ориентированных на ORM:

Вставка строк с использованием шаблона ORM Unit of Work

При использовании ORM объект Session отвечает за построение конструкций Insert и выдачу их в виде операторов INSERT в рамках текущей транзакции. Мы указываем Session на это путем добавления записей объекта к нему; Session затем убеждается, что эти новые записи будут выданы в базу данных, когда они понадобятся, используя процесс, известный как flush. Общий процесс, используемый Session для сохранения объектов, известен как паттерн unit of work.

Экземпляры классов представляют строки

Если в предыдущем примере мы выполняли INSERT, используя словари Python для указания данных, которые мы хотели добавить, то в ORM мы напрямую используем пользовательские классы Python, которые мы определили еще в Использование декларативных форм ORM для определения метаданных таблицы. На уровне классов User и Address служили местом для определения того, как должны выглядеть соответствующие таблицы базы данных. Эти классы также служат расширяемыми объектами данных, которые мы используем для создания и манипулирования строками внутри транзакции. Ниже мы создадим два объекта User, каждый из которых будет представлять потенциальную строку базы данных, подлежащую INSERT:

>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

Мы можем построить эти объекты, используя имена отображаемых столбцов в качестве аргументов ключевого слова в конструкторе. Это возможно, поскольку класс User включает автоматически сгенерированный конструктор __init__(), который был предоставлен ORM-маппингом, чтобы мы могли создать каждый объект, используя имена столбцов в качестве ключей в конструкторе.

Аналогично тому, как это было сделано в примерах Core для Insert, мы не стали включать первичный ключ (т.е. запись для столбца id), поскольку хотели бы воспользоваться функцией автоинкрементации первичного ключа базы данных, в данном случае SQLite, с которой также интегрируется ORM. Значение атрибута id для указанных объектов, если его просмотреть, отображается как None:

>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

Значение None предоставляется SQLAlchemy для указания на то, что атрибут пока не имеет значения. Атрибуты, сопоставленные с SQLAlchemy, всегда возвращают значение в Python и не выдают ошибку AttributeError при его отсутствии, если речь идет о новом объекте, которому еще не было присвоено значение.

В настоящий момент два наших объекта находятся в состоянии transient - они не связаны ни с каким состоянием базы данных и еще не связаны с объектом Session, который может генерировать для них операторы INSERT.

Добавление объектов в сессию

Чтобы пошагово проиллюстрировать процесс добавления, создадим Session без использования менеджера контекста (и, следовательно, мы должны убедиться, что закроем его впоследствии!):

>>> session = Session(engine)

Затем объекты добавляются в Session с помощью метода Session.add(). Когда этот метод вызывается, объекты находятся в состоянии, известном как pending, и еще не были вставлены:

>>> session.add(squidward)
>>> session.add(krabs)

Когда у нас есть отложенные объекты, мы можем увидеть это состояние, посмотрев на коллекцию на Session под названием Session.new:

>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

В приведенном выше представлении используется коллекция IdentitySet, которая по сути является Python-набором, хэширующим по идентичности объектов во всех случаях (т.е. с использованием встроенной в Python функции id(), а не Python-функции hash()).

Промывка

В Session используется шаблон, известный как unit of work. В общем случае это означает, что он накапливает изменения по одному за раз, но не передает их в базу данных до тех пор, пока это не потребуется. Это позволяет принимать более точные решения о том, как SQL DML должен быть выдан в транзакции на основе заданного набора ожидающих изменений. Когда транзакция передает SQL в базу данных, чтобы вывести текущий набор изменений, этот процесс называется flush.

Мы можем проиллюстрировать процесс промывки вручную, вызвав метод Session.flush():

>>> session.flush()
{execsql}BEGIN (implicit)
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[... (insertmanyvalues) 1/2 (ordered; batch not supported)] ('squidward', 'Squidward Tentacles')
INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id
[insertmanyvalues 2/2 (ordered; batch not supported)] ('ehkrabs', 'Eugene H. Krabs')

Выше мы видим, что Session сначала был вызван для выдачи SQL, поэтому он создал новую транзакцию и выдал соответствующие операторы INSERT для двух объектов. Теперь транзакция остается открытой до тех пор, пока мы не вызовем любой из методов Session.commit(), Session.rollback() или Session.close() из Session.

Хотя Session.flush() может использоваться для ручного вытеснения ожидающих изменений в текущую транзакцию, обычно в этом нет необходимости, поскольку Session обладает поведением, известным как autoflush, которое мы проиллюстрируем позже. Он также вытесняет изменения при каждом вызове Session.commit().

Автогенерируемые атрибуты первичного ключа

После вставки строк два созданных нами объекта Python находятся в состоянии, известном как persistent, где они связаны с объектом Session, в который они были добавлены или загружены, и обладают множеством других поведений, которые будут рассмотрены позже.

Другим следствием произошедшего INSERT стало получение ORM новых идентификаторов первичных ключей для каждого нового объекта; для этого обычно используется тот же самый аксессор CursorResult.inserted_primary_key, который мы представили ранее. Теперь объекты squidward и krabs имеют эти новые идентификаторы первичных ключей, и мы можем просмотреть их, используя атрибут id:

>>> squidward.id
4
>>> krabs.id
5

Совет

Почему ORM выдает два отдельных оператора INSERT, когда можно было использовать executemany? Как мы увидим в следующем разделе, Session при очистке объектов всегда должен знать первичный ключ вновь вставляемых объектов. Если используется такая функция, как автоинкремент SQLite (другие примеры - IDENTITY или SERIAL PostgreSQL, использование последовательностей и т.д.), то функция CursorResult.inserted_primary_key обычно требует, чтобы каждый INSERT выдавался по одной строке за раз. Если бы мы заранее указали значения первичных ключей, то ORM смог бы лучше оптимизировать эту операцию. Некоторые бэкенды баз данных, такие как psycopg2, также могут одновременно выполнять INSERT многих строк, сохраняя при этом возможность получения значений первичных ключей.

Получение объектов по первичному ключу из карты идентификации

Идентификатор первичного ключа объектов является значимым для Session, поскольку теперь объекты связаны с этим идентификатором в памяти с помощью функции, известной как identity map. Карта идентичности - это хранилище в памяти, которое связывает все объекты, загруженные в память в данный момент, с их идентичностью первичного ключа. Мы можем наблюдать это, извлекая один из указанных выше объектов с помощью метода Session.get(), который возвращает запись из карты идентификации, если она локально присутствует, в противном случае выдается сообщение SELECT:

>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

Важно отметить, что карта идентификации хранит уникальный экземпляр конкретного объекта Python для конкретного идентификатора базы данных в области видимости конкретного объекта Session. Можно заметить, что some_squidward ссылается на тот же объект, что и squidward ранее:

>>> some_squidward is squidward
True

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

Совершение

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

>>> session.commit()
COMMIT

Приведенная выше операция зафиксирует транзакцию, которая находилась в процессе выполнения. Объекты, с которыми мы имели дело, все еще находятся в состоянии attached к Session, в котором они пребывают до тех пор, пока Session не будет закрыта (что вводится в Закрытие сессии).

Совет

Важно отметить, что атрибуты объектов, с которыми мы только что работали, были expired, то есть при следующем обращении к любым атрибутам объектов Session будет начата новая транзакция и заново загружено их состояние. Этот вариант иногда является проблематичным как по причинам производительности, так и в случае, если необходимо использовать объекты после закрытия Session (что известно как состояние detached), поскольку они не будут иметь никакого состояния и не будут иметь Session, с помощью которого можно было бы загрузить это состояние, что приведет к ошибкам «detached instance». Это поведение можно контролировать с помощью параметра Session.expire_on_commit. Подробнее об этом говорится в Закрытие сессии.

Обновление объектов ORM с помощью шаблона Unit of Work

В предыдущем разделе Использование операторов UPDATE и DELETE была представлена конструкция Update, представляющая собой оператор SQL UPDATE. При использовании ORM эта конструкция используется двумя способами. Основной способ заключается в том, что она выдается автоматически как часть процесса unit of work, используемого в Session, где оператор UPDATE выдается на основе первичного ключа, соответствующего отдельным объектам, в которых произошли изменения.

Предположим, что мы загрузили в транзакцию объект User для имени пользователя sandy (также демонстрируя метод Select.filter_by(), а также метод Result.scalar_one()):

>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
{execsql}BEGIN (implicit)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('sandy',)

Объект Python sandy, как 2 уже упоминалось, выступает в качестве прокси для строки в базе данных, точнее, строки базы данных с точки зрения текущей транзакции, которая имеет первичный ключ ide

>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

:class:`_orm.Session`I

>>> sandy.fullname = "Sandy Squirrel"

:attr:`_orm.Session.dirty`Об

>>> sandy in session.dirty
True

fullname` а

>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
{execsql}UPDATE user_account SET fullname=? WHERE user_account.id = ?
[...] ('Sandy Squirrel', 2)
SELECT user_account.fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,){stop}
>>> print(sandy_fullname)
Sandy Squirrel

select sandy c

>>> sandy in session.dirty
False

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

См.также

<<<0>> Session.autoflush >

Удаление объектов ORM с помощью шаблона Unit of Work

unit of work`Н :meth:`_orm.Session.delete patrick а

>>> patrick = session.get(User, 3)
{execsql}SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (3,)

``patrick``I

>>> session.delete(patrick)

patrick Session C

>>> session.execute(select(User).where(User.name == "patrick")).first()
{execsql}SELECT address.id AS address_id, address.email_address AS address_email_address,
address.user_id AS address_user_id
FROM address
WHERE ? = address.user_id
[...] (3,)
DELETE FROM user_account WHERE user_account.id = ?
[...] (3,)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)

patrick``A ``SELECT address cascade address удалить b

См.также

удалить Session.delete() -

patrick Session -

>>> patrick in session
False

sandy -

Массовый / многорядный INSERT, upsert, UPDATE и DELETE

В качестве источника данных используется unit of work dml Session Session.add() t

В качестве источника данных используется Session t

insert update() delete() Использование операторов INSERT Использование операторов UPDATE и DELETE Session Connection т

orm_expression_update_delete`F :ref:`queryguide_toplevel o

Откат

В качестве параметра Session Session.rollback() Session sandy .fullname sandy "Sandy Squirrel" Session.rollback() Session lazy loading h

>>> session.rollback()
ROLLBACK

sandy``Н ``__dict__ а

>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

expired`Эт ``sandy` о

>>> sandy.fullname
{execsql}BEGIN (implicit)
SELECT user_account.id AS user_account_id, user_account.name AS user_account_name,
user_account.fullname AS user_account_fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,){stop}
'Sandy Cheeks'

__dict__``М ``sandy ы

>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

``patrick``Fo

>>> patrick in session
True

и, конечно, данные базы данных также присутствуют:

>>> session.execute(select(User).where(User.name == "patrick")).scalar_one() is patrick
{execsql}SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',){stop}
True

Закрытие сессии

Session with Session W

>>> session.close()
{execsql}ROLLBACK

:class:`_orm.Session`C

  • :term:`releases`Это

    :meth:`_orm.Session.rollback`Это

  • :class:`_orm.Session`Это

    Session`Эт ``sandy` patrick squidward detached expired Session.commit() о

    >>> squidward.name
    Traceback (most recent call last):
      ...
    sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed

    Session Session.add() Д

    >>> session.add(squidward)
    >>> squidward.name
    {execsql}BEGIN (implicit)
    SELECT user_account.id AS user_account_id, user_account.name AS user_account_name, user_account.fullname AS user_account_fullname
    FROM user_account
    WHERE user_account.id = ?
    [...] (4,){stop}
    'squidward'
Вернуться на верх