Использование операторов UPDATE и DELETE
До сих пор мы рассматривали INSERT — чтобы добавить данные в базу, и затем долго изучали SELECT, который отвечает за все основные сценарии получения данных из базы. В этом разделе мы разберём конструкции UPDATE и DELETE, которые используются для изменения уже существующих строк и для их удаления. Раздел написан с точки зрения Core-подхода SQLAlchemy.
Для читателей, использующих ORM
Как уже упоминалось в разделе про INSERT, операции UPDATE и DELETE при работе с ORM обычно вызываются автоматически объектом Session в рамках процесса unit of work.
Однако, в отличие от INSERT, конструкции UPDATE и DELETE можно использовать и напрямую с ORM — через механизм, называемый «ORM-enabled update и delete». Поэтому знание этих конструкций полезно и при работе с ORM. Оба подхода рассматриваются в разделах «Обновление объектов ORM через паттерн Unit of Work» и «Удаление объектов ORM через паттерн Unit of Work».
Конструкция update() — SQL-выражение
Функция update() создаёт новый экземпляр объекта Update, который представляет SQL-оператор UPDATE и служит для изменения существующих данных в таблице.
Как и в случае с insert(), существует «традиционная» форма update(), которая генерирует UPDATE только для одной таблицы за раз и не возвращает строк. Однако некоторые СУБД поддерживают UPDATE, изменяющий сразу несколько таблиц, а также поддерживают клаузу RETURNING, благодаря которой можно вернуть значения столбцов из изменённых строк.
Простейший UPDATE выглядит так:
>>> from sqlalchemy import update
>>> stmt = (
... update(user_table)
... .where(user_table.c.name == "patrick")
... .values(fullname="Patrick the Star")
... )
>>> print(stmt)
UPDATE user_account SET fullname=:fullname WHERE user_account.name = :name_1
Метод .values() управляет содержимым части SET оператора UPDATE. Это тот же самый метод, что используется в конструкции Insert. Параметры обычно передаются в виде именованных аргументов — ключей, соответствующих именам столбцов.
UPDATE поддерживает все основные формы SQL-оператора UPDATE, включая обновление на основе выражений, где можно использовать выражения над столбцами:
>>> stmt = update(user_table).values(fullname="Username: " + user_table.c.name)
>>> print(stmt)
UPDATE user_account SET fullname=(:name_1 || user_table.name)
Для поддержки «executemany» (когда один и тот же запрос выполняется с множеством наборов параметров) можно использовать конструкцию bindparam(), которая создаёт именованные параметры вместо конкретных значений:
>>> from sqlalchemy import bindparam
>>> stmt = (
... update(user_table)
... .where(user_table.c.name == bindparam("oldname"))
... .values(name=bindparam("newname"))
... )
>>> with engine.begin() as conn:
... conn.execute(
... stmt,
... [
... {"oldname": "jack", "newname": "ed"},
... {"oldname": "wendy", "newname": "mary"},
... {"oldname": "jim", "newname": "jake"},
... ],
... )
BEGIN (implicit)
UPDATE user_account SET name=? WHERE user_account.name = ?
[...] [('ed', 'jack'), ('mary', 'wendy'), ('jake', 'jim')]
<sqlalchemy.engine.cursor.CursorResult object at 0x...>
COMMIT
К UPDATE можно применять и другие приёмы:
Коррелированные обновления (Correlated Updates)
Оператор UPDATE может использовать строки из других таблиц через коррелированный подзапрос. Подзапрос можно разместить в любом месте, где допустимо выражение-столбец:
>>> scalar_subq = (
... select(address_table.c.email_address)
... .where(address_table.c.user_id == user_table.c.id)
... .order_by(address_table.c.id)
... .limit(1)
... .scalar_subquery()
... )
>>> update_stmt = update(user_table).values(fullname=scalar_subq)
>>> print(update_stmt)
UPDATE user_account SET fullname=(
SELECT address.email_address
FROM address
WHERE address.user_id = user_account.id ORDER BY address.id
LIMIT :param_1
)
UPDATE … FROM
Некоторые СУБД (PostgreSQL, MSSQL, MySQL) поддерживают синтаксис UPDATE … FROM, где дополнительные таблицы указываются прямо в специальной клаузе FROM. SQLAlchemy генерирует этот синтаксис автоматически, если в WHERE присутствуют другие таблицы:
>>> update_stmt = (
... update(user_table)
... .where(user_table.c.id == address_table.c.user_id)
... .where(address_table.c.email_address == "patrick@aol.com")
... .values(fullname="Pat")
... )
>>> print(update_stmt)
UPDATE user_account SET fullname=:fullname FROM address
WHERE user_account.id = address.user_id AND address.email_address = :email_address_1
Существует также специфичный для MySQL синтаксис, позволяющий обновлять несколько таблиц одновременно. Для этого в .values() нужно явно указывать объекты Table:
>>> update_stmt = (
... update(user_table)
... .where(user_table.c.id == address_table.c.user_id)
... .where(address_table.c.email_address == "patrick@aol.com")
... .values(
... {
... user_table.c.fullname: "Pat",
... address_table.c.email_address: "pat@aol.com",
... }
... )
... )
>>> from sqlalchemy.dialects import mysql
>>> print(update_stmt.compile(dialect=mysql.dialect()))
UPDATE user_account, address
SET address.email_address=%s, user_account.fullname=%s
WHERE user_account.id = address.user_id AND address.email_address = %s
Синтаксис UPDATE … FROM можно комбинировать с конструкцией Values (на PostgreSQL и др.) для массового обновления множества строк одним запросом:
>>> from sqlalchemy import Values
>>> values = Values(
... user_table.c.id,
... user_table.c.name,
... name="my_values",
... ).data([(1, "new_name"), (2, "another_name"), ("3", "name_name")])
>>> update_stmt = (
... user_table.update().values(name=values.c.name).where(user_table.c.id == values.c.id)
... )
>>> from sqlalchemy.dialects import postgresql
>>> print(update_stmt.compile(dialect=postgresql.dialect()))
UPDATE user_account
SET name=my_values.name
FROM (VALUES (%(param_1)s, %(param_2)s), (%(param_3)s, %(param_4)s), (%(param_5)s, %(param_6)s)) AS my_values (id, name)
WHERE user_account.id = my_values.id
Порядок параметров в UPDATE (только MySQL)
В MySQL порядок выражений в SET действительно влияет на результат вычисления. Для точного контроля порядка можно использовать метод Update.ordered_values(), передавая последовательность кортежей:
>>> update_stmt = update(some_table).ordered_values(
... (some_table.c.y, 20), (some_table.c.x, some_table.c.y + 10)
... )
>>> print(update_stmt)
UPDATE some_table SET y=:y, x=(some_table.y + :y_1)
Хотя словари в Python начиная с версии 3.7 сохраняют порядок вставки, метод
ordered_values()всё равно даёт дополнительную ясность намерений, когда критически важен порядок обработки SET-клауз в MySQL.
Конструкция delete() — SQL-выражение
Функция delete() создаёт новый экземпляр объекта Delete, представляющий SQL-оператор DELETE, который удаляет строки из таблицы.
С точки зрения API delete() очень похож на update(): традиционно он не возвращает строк, но на некоторых СУБД поддерживает вариант с RETURNING.
>>> from sqlalchemy import delete
>>> stmt = delete(user_table).where(user_table.c.name == "patrick")
>>> print(stmt)
DELETE FROM user_account WHERE user_account.name = :name_1
Удаление из нескольких таблиц
Как и Update, Delete поддерживает коррелированные подзапросы в WHERE и специфичные для СУБД синтаксисы, например DELETE … USING в MySQL:
>>> delete_stmt = (
... delete(user_table)
... .where(user_table.c.id == address_table.c.user_id)
... .where(address_table.c.email_address == "patrick@aol.com")
... )
>>> from sqlalchemy.dialects import mysql
>>> print(delete_stmt.compile(dialect=mysql.dialect()))
DELETE FROM user_account USING user_account, address
WHERE user_account.id = address.user_id AND address.email_address = %s
Получение количества затронутых строк для UPDATE и DELETE
Оба конструкта Update и Delete позволяют узнать количество строк, удовлетворяющих условию WHERE, после выполнения запроса (при использовании Core-интерфейса Connection). Это значение доступно через атрибут CursorResult.rowcount:
>>> with engine.begin() as conn:
... result = conn.execute(
... update(user_table)
... .values(fullname="Patrick McStar")
... .where(user_table.c.name == "patrick")
... )
... print(result.rowcount)
BEGIN (implicit)
UPDATE user_account SET fullname=? WHERE user_account.name = ?
[...] ('Patrick McStar', 'patrick')
1
COMMIT
Важные замечания о rowcount:
- Возвращается количество строк, подходящих под WHERE, независимо от того, были ли они действительно изменены.
rowcountможет быть недоступен для запросов с RETURNING или при использованииexecutemany— зависит от DBAPI.- Если драйвер не может определить количество строк, возвращается
-1. - SQLAlchemy заранее сохраняет значение
cursor.rowcount, потому что некоторые драйверы запрещают обращаться к нему после закрытия курсора. - Некоторые сторонние драйверы (особенно для NoSQL) могут вообще не поддерживать
rowcount. Это проверяется черезCursorResult.supports_sane_rowcount.
rowcount активно используется ORM в процессе unit of work для проверки корректности UPDATE/DELETE и для работы механизма версионирования (version counter).
Использование RETURNING с UPDATE и DELETE
Как и Insert, конструкции Update и Delete поддерживают клаузу RETURNING через методы .returning(). При использовании на СУБД, поддерживающих RETURNING, в результате будут возвращены выбранные столбцы из всех строк, удовлетворяющих условию WHERE:
>>> update_stmt = (
... update(user_table)
... .where(user_table.c.name == "patrick")
... .values(fullname="Patrick the Star")
... .returning(user_table.c.id, user_table.c.name)
... )
>>> print(update_stmt)
UPDATE user_account SET fullname=:fullname
WHERE user_account.name = :name_1
RETURNING user_account.id, user_account.name
>>> delete_stmt = (
... delete(user_table)
... .where(user_table.c.name == "patrick")
... .returning(user_table.c.id, user_table.c.name)
... )
>>> print(delete_stmt)
DELETE FROM user_account
WHERE user_account.name = :name_1
RETURNING user_account.id, user_account.name
