Работа с транзакциями и DBAPI

Когда объект Engine готов к работе, мы можем приступить к изучению основных операций Engine и его основных интерактивных конечных точек, Connection и Result. Кроме того, мы познакомимся с функцией facade для этих объектов, известной как Session.

Примечание для читателей ORM

При использовании ORM, Engine управляется другим объектом, называемым Session. Session в современной SQLAlchemy подчеркивает транзакционную и SQL модель выполнения, которая в значительной степени идентична модели выполнения Connection, обсуждаемой ниже, поэтому, хотя этот подраздел ориентирован на Core, все концепции здесь по существу относятся и к использованию ORM и рекомендуются для всех изучающих ORM. Шаблон выполнения, используемый Connection, будет противопоставлен шаблону выполнения Session в конце этого раздела.

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

Получение соединения

Единственная цель объекта Engine с точки зрения пользователя - обеспечить единицу связи с базой данных, называемую Connection. При непосредственной работе с ядром объект Connection является способом взаимодействия с базой данных. Поскольку Connection представляет собой открытый ресурс базы данных, мы хотим всегда ограничивать область использования этого объекта определенным контекстом, и лучший способ сделать это - использовать форму менеджера контекста Python, также известную как the with statement. Ниже мы проиллюстрируем «Hello World», используя текстовый SQL-запрос. Текстовый SQL выражается с помощью конструкции text(), которая будет рассмотрена более подробно позже:

>>> from sqlalchemy import text

>>> with engine.connect() as conn:
...     result = conn.execute(text("select 'hello world'"))
...     print(result.all())
BEGIN (implicit) select 'hello world' [...] ()
[('hello world',)]
ROLLBACK

В приведенном выше примере менеджер контекста обеспечил подключение к базе данных, а также обрамил операцию внутри транзакции. Поведение Python DBAPI по умолчанию включает в себя то, что транзакция всегда находится в процессе выполнения; когда область видимости соединения становится released, выдается ROLLBACK для завершения транзакции. Транзакция не фиксируется автоматически; когда мы хотим зафиксировать данные, нам обычно нужно вызвать Connection.commit(), как мы увидим в следующем разделе.

Совет

Режим «autocommit» доступен для особых случаев. Об этом говорится в разделе Установка уровней изоляции транзакций, включая DBAPI Autocommit.

Результат нашего SELECT также был возвращен в объекте Result, который будет рассмотрен позже, однако сейчас мы добавим, что лучше всего убедиться, что этот объект потребляется внутри блока «connect» и не передается за пределы нашего соединения.

Принятие изменений

Мы только что узнали, что соединение DBAPI является неавтофиксируемым. Что если мы захотим зафиксировать некоторые данные? Мы можем изменить наш вышеприведенный пример, чтобы создать таблицу и вставить некоторые данные, после чего транзакция будет зафиксирована с помощью метода Connection.commit(), вызванного внутри блока, в котором мы получили объект Connection:

# "commit as you go"
>>> with engine.connect() as conn:
...     conn.execute(text("CREATE TABLE some_table (x int, y int)"))
...     conn.execute(
...         text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
...         [{"x": 1, "y": 1}, {"x": 2, "y": 4}],
...     )
...     conn.commit()
BEGIN (implicit) CREATE TABLE some_table (x int, y int) [...] () <sqlalchemy.engine.cursor.CursorResult object at 0x...> INSERT INTO some_table (x, y) VALUES (?, ?) [...] ((1, 1), (2, 4)) <sqlalchemy.engine.cursor.CursorResult object at 0x...> COMMIT

Выше мы выполнили два оператора SQL, которые обычно являются транзакционными: оператор «CREATE TABLE» [1] и параметризованный оператор «INSERT» (синтаксис параметризации рассматривается несколькими разделами ниже в Отправка нескольких параметров). Поскольку мы хотим, чтобы выполненная нами работа была зафиксирована внутри нашего блока, мы вызываем метод Connection.commit(), который фиксирует транзакцию. После вызова этого метода внутри блока мы можем продолжать выполнять другие SQL-запросы и, если захотим, можем снова вызвать Connection.commit() для последующих запросов. SQLAlchemy называет этот стиль фиксировать по мере выполнения.

Существует и другой способ фиксации данных, который заключается в том, что мы можем заранее объявить наш блок «connect» блоком транзакции. Для этого режима работы мы используем метод Engine.begin() для получения соединения, а не метод Engine.connect(). Этот метод будет управлять областью действия Connection, а также заключать все внутри транзакции с COMMIT в конце, при условии успешного блока, или ROLLBACK в случае возникновения исключения. Этот стиль можно назвать begin once:

# "begin once"
>>> with engine.begin() as conn:
...     conn.execute(
...         text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
...         [{"x": 6, "y": 8}, {"x": 9, "y": 10}],
...     )
BEGIN (implicit) INSERT INTO some_table (x, y) VALUES (?, ?) [...] ((6, 8), (9, 10)) <sqlalchemy.engine.cursor.CursorResult object at 0x...> COMMIT

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

Основы выполнения заявлений

Мы рассмотрели несколько примеров выполнения SQL-запросов к базе данных, используя метод Connection.execute() в сочетании с объектом text() и возвращая объект Result. В этом разделе мы более подробно проиллюстрируем механику и взаимодействие этих компонентов.

Большая часть содержания этого раздела одинаково хорошо применима к современному использованию ORM при использовании метода Session.execute(), который работает очень похоже на метод Connection.execute(), включая то, что строки результатов ORM доставляются с помощью того же интерфейса Result, который используется в Core.

Получение строк

Сначала мы проиллюстрируем объект Result более подробно, используя строки, которые мы вставили ранее, выполнив текстовый оператор SELECT на созданной нами таблице:

>>> with engine.connect() as conn:
...     result = conn.execute(text("SELECT x, y FROM some_table"))
...     for row in result:
...         print(f"x: {row.x}  y: {row.y}")
BEGIN (implicit) SELECT x, y FROM some_table [...] ()
x: 1 y: 1 x: 2 y: 4 x: 6 y: 8 x: 9 y: 10
ROLLBACK

Выше, строка «SELECT», которую мы выполнили, выбрала все строки из нашей таблицы. Возвращаемый объект называется Result и представляет собой итерируемый объект строк результата.

Result имеет множество методов для получения и преобразования строк, например, метод Result.all(), показанный ранее, который возвращает список всех объектов Row. Он также реализует интерфейс итератора Python, так что мы можем выполнять итерацию над коллекцией объектов Row напрямую.

Сами объекты Row предназначены для работы как Python named tuples. Ниже мы проиллюстрируем различные способы доступа к строкам.

  • Присвоение кортежа - Это наиболее Python-идиоматический стиль, который заключается в позиционном присвоении переменных каждой строке по мере их получения:

    result = conn.execute(text("select x, y from some_table"))
    
    for x, y in result:
        # ...
  • Интегральный индекс - Кортежи - это последовательности Python, поэтому доступ к обычным целым числам тоже возможен:

    result = conn.execute(text("select x, y from some_table"))
    
      for row in result:
          x = row[0]
  • Имя атрибута - Поскольку это именованные кортежи Python, кортежи имеют динамические имена атрибутов, соответствующие именам каждого столбца. Эти имена обычно являются именами, которые оператор SQL присваивает столбцам в каждой строке. Хотя обычно они достаточно предсказуемы и могут управляться метками, в менее определенных случаях они могут быть подвержены специфическому для базы данных поведению:

    result = conn.execute(text("select x, y from some_table"))
    
    for row in result:
        y = row.y
    
        # illustrate use with Python f-strings
        print(f"Row: {row.x} {y}")
  • Доступ к отображению - Чтобы получать строки как объекты Python отображения, что по сути является версией интерфейса Python к общему объекту dict только для чтения, Result может быть преобразован в объект MappingResult с помощью модификатора Result.mappings(); это объект результата, который выдает словареподобные объекты RowMapping, а не объекты Row:

    result = conn.execute(text("select x, y from some_table"))
    
    for dict_row in result.mappings():
        x = dict_row["x"]
        y = dict_row["y"]

Параметры отправки

Операторы SQL обычно сопровождаются данными, которые должны быть переданы вместе с самим оператором, как мы видели ранее в примере INSERT. Поэтому метод Connection.execute() также принимает параметры, которые обозначаются как bound parameters. В качестве элементарного примера можно привести случай, когда мы хотим ограничить наш оператор SELECT только теми строками, которые соответствуют определенному критерию, например, строками, в которых значение «y» больше определенного значения, передаваемого в функцию.

Чтобы добиться этого, чтобы SQL-оператор оставался фиксированным, а драйвер мог правильно санировать значение, мы добавляем критерий WHERE к нашему оператору, который называет новый параметр «y»; конструкция text() принимает их, используя формат двоеточия «:y». Фактическое значение для «:y» затем передается в качестве второго аргумента в Connection.execute() в виде словаря:

>>> with engine.connect() as conn:
...     result = conn.execute(text("SELECT x, y FROM some_table WHERE y > :y"), {"y": 2})
...     for row in result:
...         print(f"x: {row.x}  y: {row.y}")
BEGIN (implicit) SELECT x, y FROM some_table WHERE y > ? [...] (2,)
x: 2 y: 4 x: 6 y: 8 x: 9 y: 10
ROLLBACK

В протоколируемом выводе SQL видно, что связанный параметр :y был преобразован в знак вопроса при отправке в базу данных SQLite. Это происходит потому, что драйвер базы данных SQLite использует формат под названием «qmark parameter style», который является одним из шести различных форматов, разрешенных спецификацией DBAPI. SQLAlchemy абстрагирует эти форматы только в один, который является «именованным» форматом с использованием двоеточия.

Отправка нескольких параметров

В примере Принятие изменений мы выполнили оператор INSERT, и оказалось, что мы смогли одновременно ввести в базу данных несколько строк. Для операторов, которые работают с данными, но не возвращают наборы результатов, а именно DML операторов, таких как «INSERT», которые не включают фразу типа «RETURNING», мы можем послать много параметров в метод Connection.execute(), передав список словарей вместо одного словаря, что позволит вызвать один SQL-оператор для каждого набора параметров в отдельности:

>>> with engine.connect() as conn:
...     conn.execute(
...         text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
...         [{"x": 11, "y": 12}, {"x": 13, "y": 14}],
...     )
...     conn.commit()
BEGIN (implicit) INSERT INTO some_table (x, y) VALUES (?, ?) [...] ((11, 12), (13, 14)) <sqlalchemy.engine.cursor.CursorResult object at 0x...> COMMIT

За кулисами объекты Connection используют функцию DBAPI, известную как cursor.executemany(). Этот метод выполняет эквивалентную операцию вызова заданного SQL-оператора для каждого набора параметров в отдельности. DBAPI может оптимизировать эту операцию различными способами, используя подготовленные операторы или в некоторых случаях объединяя наборы параметров в один SQL-оператор. Некоторые диалекты SQLAlchemy могут также использовать альтернативные API для этого случая, например psycopg2 dialect for PostgreSQL, который использует более производительные API для этого случая.

Совет

вы могли заметить, что этот раздел не помечен как концепция ORM. Это потому, что случай использования «нескольких параметров» обычно используется для операторов INSERT, которые при использовании ORM вызываются другим способом. Множественные параметры также могут использоваться с операторами UPDATE и DELETE для выполнения отдельных операций UPDATE/DELETE на основе каждой строки, однако, опять же, при использовании ORM существует другая техника, обычно используемая для обновления или удаления многих отдельных строк по отдельности.

Выполнение с помощью сессии ORM

Как уже упоминалось ранее, большинство паттернов и примеров выше применимы и к использованию ORM, поэтому здесь мы представим это использование, чтобы по мере изучения учебника мы могли проиллюстрировать каждый паттерн с точки зрения использования Core и ORM вместе.

Фундаментальный транзакционный / интерактивный объект базы данных при использовании ORM называется Session. В современной SQLAlchemy этот объект используется так же, как Connection, и фактически, когда используется Session, он внутренне ссылается на Connection, который он использует для эмиссии SQL.

Когда Session используется с не-ORM конструкциями, он проходит через операторы SQL, которые мы ему даем, и обычно не делает ничего существенно отличного от того, что делает Connection напрямую, поэтому мы можем проиллюстрировать его здесь в терминах простых текстовых операций SQL, которые мы уже изучили.

Session имеет несколько различных шаблонов создания, но здесь мы проиллюстрируем самый основной, который точно соответствует тому, как используется Connection, а именно - построение его в менеджере контекста:

>>> from sqlalchemy.orm import Session

>>> stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y")
>>> with Session(engine) as session:
...     result = session.execute(stmt, {"y": 6})
...     for row in result:
...         print(f"x: {row.x}  y: {row.y}")
BEGIN (implicit) SELECT x, y FROM some_table WHERE y > ? ORDER BY x, y [...] (6,)
x: 6 y: 8 x: 9 y: 10 x: 11 y: 12 x: 13 y: 14
ROLLBACK

Приведенный выше пример можно сравнить с примером в предыдущем разделе Параметры отправки - мы непосредственно заменяем вызов with engine.connect() as conn на with Session(engine) as session, а затем используем метод Session.execute() так же, как и метод Connection.execute().

Также, как и Connection, Session поддерживает поведение «фиксации по мере выполнения» с помощью метода Session.commit(), который показан ниже на примере текстового оператора UPDATE для изменения некоторых данных:

>>> with Session(engine) as session:
...     result = session.execute(
...         text("UPDATE some_table SET y=:y WHERE x=:x"),
...         [{"x": 9, "y": 11}, {"x": 13, "y": 15}],
...     )
...     session.commit()
BEGIN (implicit) UPDATE some_table SET y=? WHERE x=? [...] ((11, 9), (15, 13)) COMMIT

Выше мы вызвали оператор UPDATE, используя связанный параметр, стиль выполнения «executemany», представленный в Отправка нескольких параметров, завершив блок фиксацией «commit as you go».

Совет

На самом деле Session не удерживает объект Connection после завершения транзакции. Он получает новый Connection из Engine при следующей необходимости выполнения SQL к базе данных.

Очевидно, что Session имеет гораздо больше трюков в рукаве, чем это, однако понимание того, что у него есть метод Session.execute(), который используется так же, как и Connection.execute(), поможет нам начать работу с последующими примерами.

См.также

Основы использования сеанса - представляет основные модели создания и использования объекта Session.

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