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

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

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

Вставка строк с помощью 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()

Объект 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)

Если мы помечаем 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)

>>> 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
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.

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