Манипулирование данными с помощью ORM¶
Предыдущий раздел Работа с данными был посвящен языку SQL Expression Language с точки зрения Core, чтобы обеспечить преемственность между основными конструкциями операторов SQL. Далее в этом разделе будет рассмотрен жизненный цикл Session
и его взаимодействие с этими конструкциями.
Предварительные разделы - часть учебника, посвященная ORM, опирается на два предыдущих раздела этого документа, ориентированных на ORM:
Выполнение с помощью сеанса ORM - представляет, как сделать объект ORM
Session
Использование декларативных форм ORM для определения метаданных таблицы - здесь мы устанавливаем наши ORM отображения сущностей
User
иAddress
Выбор сущностей и столбцов ORM - несколько примеров выполнения операторов SELECT для сущностей типа
User
Вставка строк с использованием шаблона 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
См.также
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.close()
{execsql}ROLLBACK
:class:`_orm.Session`C
:term:`releases`Это
:meth:`_orm.Session.rollback`Это
:class:`_orm.Session`Это
Session`Эт ``sandy`
patrick
squidward
detached expiredSession.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.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'
Совет
Session
Session
Session.expire_on_commit
False
T