Манипулирование данными с помощью ORM¶
Предыдущий раздел Работа с данными был посвящен языку SQL Expression Language с точки зрения Core, чтобы обеспечить преемственность основных конструкций операторов SQL. В этом разделе мы рассмотрим жизненный цикл Session
и то, как он взаимодействует с этими конструкциями.
Предварительные разделы - часть учебника, посвященная ORM, основывается на двух предыдущих разделах этого документа, ориентированных на ORM:
Выполнение с помощью сессии ORM - представляет, как создать объект ORM
Session
Определение метаданных таблицы с помощью ORM - где мы устанавливаем наши ORM отображения сущностей
User
иAddress
Выбор сущностей и столбцов ORM - несколько примеров выполнения операторов SELECT для сущностей типа
User
Вставка строк с помощью ORM¶
При использовании ORM объект Session
отвечает за создание конструкций Insert
и их передачу нам в транзакции. Мы инструктируем Session
делать это путем добавления записей объектов; Session
затем убеждается, что эти новые записи будут переданы в базу данных, когда они понадобятся, с помощью процесса, известного как flush.
Экземпляры классов представляют строки¶
Если в предыдущем примере мы выполняли INSERT, используя словари Python для обозначения данных, которые мы хотели добавить, то в ORM мы напрямую используем пользовательские классы Python, которые мы определили еще в Определение метаданных таблицы с помощью ORM. На уровне классов, классы User
и Address
служили местом для определения того, как должны выглядеть соответствующие таблицы базы данных. Эти классы также служат расширяемыми объектами данных, которые мы используем для создания и манипулирования строками в транзакции. Ниже мы создадим два объекта User
, каждый из которых будет представлять потенциальную строку базы данных, которая будет вставлена в транзакцию:
>>> 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 в базу данных, чтобы вытолкнуть текущий набор изменений, этот процесс называется промывкой.
Мы можем проиллюстрировать процесс промывки вручную, вызвав метод Session.flush()
:
>>> session.flush()
BEGIN (implicit)
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('squidward', 'Squidward Tentacles')
INSERT INTO user_account (name, fullname) VALUES (?, ?)
[...] ('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 (другие примеры включают PostgreSQL IDENTITY или SERIAL, использование последовательностей и т.д.), то функция 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 rows), прежде чем изучать другие модели поведения и возможности ORM:
>>> session.commit()
COMMIT
Приведенная выше операция зафиксирует транзакцию, которая находилась в процессе. Объекты, с которыми мы имели дело, по-прежнему attached находятся в состоянии Session
, в котором они пребывают до тех пор, пока Session
не будет закрыт (что вводится в Закрытие сеанса).
Совет
Важно отметить, что атрибуты объектов, с которыми мы только что работали, были expired, то есть при следующем обращении к любым их атрибутам Session
начнет новую транзакцию и перезагрузит их состояние. Этот вариант иногда является проблематичным как по причинам производительности, так и в случае, если вы хотите использовать объекты после закрытия Session
(что известно как состояние detached), поскольку у них не будет состояния и не будет Session
, с помощью которого можно загрузить это состояние, что приведет к ошибкам «detached instance». Это поведение можно контролировать с помощью параметра Session.expire_on_commit
. Подробнее об этом говорится в Закрытие сеанса.
Обновление объектов ORM¶
В предыдущем разделе Обновление и удаление строк с помощью Core мы представили конструкцию Update
, которая представляет собой оператор SQL UPDATE. При использовании ORM существует два способа применения этой конструкции. Основной способ заключается в том, что она выполняется автоматически как часть процесса unit of work, используемого Session
, где оператор UPDATE выполняется на основе каждого первичного ключа, соответствующего отдельным объектам, в которых произошли изменения. Вторая форма UPDATE называется «UPDATE с поддержкой ORM» и позволяет нам использовать конструкцию Update
с Session
в явном виде; это описано в следующем разделе.
Предположим, мы загрузили объект User
для имени пользователя sandy
в транзакцию (также демонстрируя метод Select.filter_by()
, а также метод Result.scalar_one()
):
sql>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
BEGIN (implicit)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('sandy',)
Объект Python sandy
, как уже упоминалось, действует как прокси для строки в базе данных, точнее, строки базы данных в терминах текущей транзакции, которая имеет первичный ключ 2
:
>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')
Если мы изменяем атрибуты этого объекта, Session
отслеживает это изменение:
>>> sandy.fullname = "Sandy Squirrel"
Объект появляется в коллекции под названием Session.dirty
, указывая на то, что объект «грязный»:
>>> sandy in session.dirty
True
Когда Session
в следующий раз выдает flush, будет выдан UPDATE, который обновит это значение в базе данных. Как упоминалось ранее, flush происходит автоматически до того, как мы выдадим какой-либо SELECT, используя поведение, известное как autoflush. Мы можем напрямую запросить столбец User.fullname
из этой строки и получим обратно обновленное значение:
>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
UPDATE user_account SET fullname=? WHERE user_account.id = ?
[...] ('Sandy Squirrel', 2)
SELECT user_account.fullname
FROM user_account
WHERE user_account.id = ?
[...] (2,)
>>> print(sandy_fullname)
Sandy Squirrel
Выше мы видим, что мы запросили, чтобы Session
выполнил один оператор select()
. Однако выданный SQL показывает, что также было выдано UPDATE, что является процессом промывки, выталкивающим отложенные изменения. Объект sandy
Python теперь больше не считается грязным:
>>> sandy in session.dirty
False
Однако обратите внимание, что мы все еще находимся в транзакции, и наши изменения не были перенесены в постоянное хранилище базы данных. Поскольку фамилия Сэнди на самом деле «Чикс», а не «Белка», мы исправим эту ошибку позже, когда откатим транзакцию. Но сначала мы сделаем еще несколько изменений данных.
См.также
Промывка- подробно описывает процесс промывки, а также информацию о настройке Session.autoflush
.
Операторы UPDATE с поддержкой ORM¶
Как уже упоминалось ранее, существует второй способ создания UPDATE-запросов с точки зрения ORM, который известен как ORM enabled UPDATE statement. Это позволяет использовать общий SQL-оператор UPDATE, который может влиять на многие строки одновременно. Например, чтобы создать UPDATE, который изменит столбец User.fullname
на основе значения в столбце User.name
:
>>> session.execute(
... update(User)
... .where(User.name == "sandy")
... .values(fullname="Sandy Squirrel Extraordinaire")
... )
UPDATE user_account SET fullname=? WHERE user_account.name = ?
[...] ('Sandy Squirrel Extraordinaire', 'sandy')
<sqlalchemy.engine.cursor.CursorResult object ...>
При вызове оператора UPDATE с поддержкой ORM используется специальная логика для поиска объектов в текущей сессии, соответствующих заданным критериям, чтобы они были обновлены новыми данными. Выше, идентификатор объекта sandy
был найден в памяти и обновлен:
>>> sandy.fullname
'Sandy Squirrel Extraordinaire'
Логика обновления известна как опция synchronize_session
и подробно описана в разделе UPDATE и DELETE с произвольным предложением WHERE.
См.также
UPDATE и DELETE с произвольным предложением WHERE - описывает использование ORM update()
и delete()
, а также опции синхронизации ORM.
Удаление объектов ORM¶
В завершение основных операций персистентности отдельный объект ORM может быть помечен для удаления с помощью метода Session.delete()
. Давайте загрузим patrick
из базы данных:
sql>>> patrick = session.get(User, 3)
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
для удаления, как и в случае с другими операциями, то пока не произойдет flush, фактически ничего не произойдет:
>>> session.delete(patrick)
Текущее поведение ORM заключается в том, что patrick
остается в Session
до тех пор, пока не произойдет flush, который, как уже упоминалось, происходит, если мы выдаем запрос:
>>> session.execute(select(User).where(User.name == "patrick")).first()
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',)
Выше, SELECT, который мы попросили выдать, предшествовал DELETE, что указывало на предстоящее удаление для patrick
. Также был выполнен запрос SELECT
к таблице address
, который был вызван тем, что ORM ищет в этой таблице строки, которые могут быть связаны с целевой строкой; это поведение является частью поведения, известного как cascade, и его можно настроить для более эффективной работы, позволив базе данных автоматически обрабатывать связанные строки в address
; в разделе удалить об этом подробно рассказывается.
См.также
удалить - описывает, как настроить поведение Session.delete()
в плане того, как должны обрабатываться связанные строки в других таблицах.
Кроме того, удаляемый экземпляр объекта patrick
больше не считается постоянным внутри Session
, как показано проверкой содержимого:
>>> patrick in session
False
Однако, как и в случае с UPDATE, которые мы сделали в объекте sandy
, каждое изменение, которое мы сделали здесь, является локальным для текущей транзакции, которая не станет постоянной, если мы ее не зафиксируем. Поскольку откат транзакции на данный момент более интересен, мы сделаем это в следующем разделе.
Заявления DELETE с поддержкой ORM¶
Подобно операциям UPDATE, существует также версия DELETE с поддержкой ORM, которую мы можем проиллюстрировать, используя конструкцию delete()
с Session.execute()
. Она также имеет функцию, благодаря которой непросроченные объекты (см. expired), соответствующие заданным критериям удаления, будут автоматически помечены как «deleted» в Session
:
>>> # refresh the target object for demonstration purposes
>>> # only, not needed for the DELETE
sql>>> squidward = session.get(User, 4)
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,)
>>> session.execute(delete(User).where(User.name == "squidward"))
DELETE FROM user_account WHERE user_account.name = ?
[...] ('squidward',)
<sqlalchemy.engine.cursor.CursorResult object at 0x...>
Идентификатор squidward
, как и идентификатор patrick
, теперь также находится в удаленном состоянии. Обратите внимание, что для демонстрации этого нам пришлось перезагрузить squidward
выше; если бы объект был просрочен, операция DELETE не стала бы тратить время на обновление просроченных объектов только для того, чтобы увидеть, что они были удалены:
>>> squidward in session
False
Откат¶
У Session
есть метод Session.rollback()
, который, как и ожидалось, выдает ROLLBACK на текущем SQL-соединении. Однако он также влияет на объекты, которые в настоящее время связаны с Session
, в нашем предыдущем примере объект Python sandy
. Хотя мы изменили .fullname
объекта sandy
на чтение "Sandy Squirrel"
, мы хотим откатить это изменение. Вызов Session.rollback()
не только откатит транзакцию, но и удалит все объекты, в настоящее время связанные с этим Session
, что приведет к тому, что они обновятся при следующем обращении к ним с помощью процесса, известного как lazy loading:
>>> session.rollback()
ROLLBACK
Чтобы рассмотреть процесс «истечения» более внимательно, мы можем заметить, что у объекта Python sandy
не осталось никакого состояния внутри его Python __dict__
, за исключением специального объекта внутреннего состояния SQLAlchemy:
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}
Это состояние «expired»; повторное обращение к атрибуту приведет к автозапуску новой транзакции и обновлению sandy
с текущей строкой базы данных:
>>> sandy.fullname
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,)
'Sandy Cheeks'
Теперь мы можем заметить, что полная строка базы данных также была заполнена в __dict__
объекта sandy
:
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}
Для удаленных объектов, когда мы ранее отметили, что patrick
больше не находится в сессии, идентичность этого объекта также восстанавливается:
>>> patrick in session
True
и, конечно, данные базы данных также присутствуют:
sql>>> session.execute(select(User).where(User.name == "patrick")).scalar_one() is patrick
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = ?
[...] ('patrick',)
True
Закрытие сеанса¶
В приведенных выше разделах мы использовали объект Session
вне контекстного менеджера Python, то есть не использовали оператор with
. Это нормально, однако, если мы будем действовать таким образом, лучше всего явно закрыть Session
, когда мы закончим с ним:
>>> session.close()
ROLLBACK
Закрытие Session
, что происходит, когда мы используем его и в менеджере контекста, приводит к следующим результатам:
Это releases все ресурсы соединения в пул соединений, отменяя (например, откатывая) все транзакции, которые находились в процессе выполнения.
Это означает, что когда мы используем сессию для выполнения некоторых задач только для чтения, а затем закрываем ее, нам не нужно явно вызывать
Session.rollback()
, чтобы убедиться, что транзакция откатилась назад; пул соединений справится с этим.Он удаляет все объекты из
Session
.Это означает, что все объекты Python, которые мы загрузили для этого
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
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,)'squidward'Совет
По возможности старайтесь не использовать объекты в их отсоединенном состоянии. Когда
Session
закрывается, очистите также ссылки на все ранее присоединенные объекты. Для случаев, когда отсоединенные объекты необходимы, например, для немедленного отображения только что зафиксированных объектов в веб-приложении, гдеSession
закрывается перед отрисовкой представления, установите флагSession.expire_on_commit
в значениеFalse
.