Настройка счетчика версий¶
Mapper
поддерживает управление version id column, который представляет собой один столбец таблицы, который увеличивает или иным образом обновляет свое значение каждый раз, когда происходит UPDATE
в сопоставленной таблице. Это значение проверяется каждый раз, когда ORM выдает UPDATE
или DELETE
на строку, чтобы убедиться, что значение, хранящееся в памяти, соответствует значению базы данных.
Предупреждение
Поскольку функция версионирования основана на сравнении записей объекта в памяти, она применима только к процессу Session.flush()
, когда ORM пересылает отдельные строки в памяти в базу данных. Она не действует при выполнении многорядного UPDATE или DELETE с помощью методов Query.update()
или Query.delete()
, поскольку эти методы только выпускают оператор UPDATE или DELETE, но в остальном не имеют прямого доступа к содержимому тех строк, на которые оказывается воздействие.
Цель этой функции - определить, когда две параллельные транзакции изменяют одну и ту же строку примерно в одно и то же время, или, в качестве альтернативы, обеспечить защиту от использования «несвежей» строки в системе, которая может повторно использовать данные из предыдущей транзакции без обновления (например, если установить expire_on_commit=False
с Session
, то возможно повторное использование данных из предыдущей транзакции).
Простой подсчет версий¶
Наиболее простым способом отслеживания версий является добавление целочисленного столбца в сопоставленную таблицу, затем установление его в качестве version_id_col
в опциях маппера:
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
version_id = mapped_column(Integer, nullable=False)
name = mapped_column(String(50), nullable=False)
__mapper_args__ = {"version_id_col": version_id}
Примечание
Настоятельно рекомендуется** сделать столбец version_id
NOT NULL. Функция версионирования не поддерживает значение NULL в столбце версионирования.
Выше, отображение User
отслеживает целочисленные версии с помощью столбца version_id
. При первой очистке объекта типа User
столбцу version_id
будет присвоено значение «1». Затем, при последующем UPDATE таблицы всегда будет выдаваться значение, подобное следующему:
UPDATE user SET version_id=:version_id, name=:name
WHERE user.id = :user_id AND user.version_id = :user_version_id
-- {"name": "new name", "version_id": 2, "user_id": 1, "user_version_id": 1}
Приведенный выше оператор UPDATE обновляет строку, которая не только соответствует user.id = 1
, но и требует, чтобы user.version_id = 1
, где «1» - это последний идентификатор версии, который мы использовали для этого объекта. Если транзакция в другом месте изменила строку независимо, этот идентификатор версии больше не будет соответствовать, и оператор UPDATE сообщит, что ни одна строка не совпала; это условие, которое проверяет SQLAlchemy, что именно одна строка совпала с нашим оператором UPDATE (или DELETE). Если совпадает ноль строк, это указывает на то, что наша версия данных устарела, и возникает ошибка StaleDataError
.
Пользовательские счетчики версий / типы¶
Для версионирования можно использовать и другие типы значений или счетчиков. К распространенным типам относятся даты и GUID. При использовании альтернативного типа или схемы счетчиков SQLAlchemy предоставляет крючок для этой схемы с помощью аргумента version_id_generator
, который принимает вызываемую переменную генерации версии. Этой вызываемой переменной передается значение текущей известной версии, и ожидается, что она вернет последующую версию.
Например, если мы хотим отслеживать версионность нашего класса User
, используя случайно сгенерированный GUID, мы можем сделать следующее (обратите внимание, что некоторые бэкенды поддерживают собственный тип GUID, но мы иллюстрируем здесь использование простой строки):
import uuid
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
version_uuid = mapped_column(String(32), nullable=False)
name = mapped_column(String(50), nullable=False)
__mapper_args__ = {
"version_id_col": version_uuid,
"version_id_generator": lambda version: uuid.uuid4().hex,
}
Механизм персистентности будет обращаться к uuid.uuid4()
каждый раз, когда объект User
будет подвержен INSERT или UPDATE. В этом случае наша функция генерации версий может игнорировать входящее значение version
, поскольку функция uuid4()
генерирует идентификаторы без какого-либо предварительного значения. Если бы мы использовали последовательную схему версионирования, например, числовую или систему специальных символов, мы могли бы использовать данное version
для того, чтобы помочь определить последующее значение.
См.также
Счетчики версий на стороне сервера¶
version_id_generator
также может быть настроен на использование значения, которое генерируется базой данных. В этом случае базе данных потребуются средства для генерации новых идентификаторов, когда строка подвергается INSERT, а также UPDATE. Для случая UPDATE обычно требуется триггер обновления, если только данная база данных не поддерживает какой-либо другой собственный идентификатор версии. База данных PostgreSQL, в частности, поддерживает системный столбец xmin, который обеспечивает версионность UPDATE. Мы можем использовать колонку PostgreSQL xmin
для версионирования нашего класса User
следующим образом:
from sqlalchemy import FetchedValue
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(50), nullable=False)
xmin = mapped_column("xmin", String, system=True, server_default=FetchedValue())
__mapper_args__ = {"version_id_col": xmin, "version_id_generator": False}
С приведенным выше отображением ORM будет полагаться на колонку xmin
для автоматического предоставления нового значения счетчика идентификатора версии.
ORM обычно не выполняет активную выборку значений генерируемых базой данных при выполнении INSERT или UPDATE, вместо этого оставляя эти столбцы как «истекшие» и подлежащие выборке при следующем обращении к ним, если только не установлен флаг eager_defaults
Mapper
. Однако, когда используется колонка версии на стороне сервера, ORM должен активно извлекать только что созданное значение. Это делается для того, чтобы счетчик версий был установлен до того, как какая-либо параллельная транзакция сможет обновить его снова. Эту выборку лучше всего выполнять одновременно в операторе INSERT или UPDATE с помощью RETURNING, иначе при последующем выполнении оператора SELECT все еще существует потенциальное состояние гонки, когда счетчик версий может измениться до того, как он будет извлечен.
Если целевая база данных поддерживает RETURNING, оператор INSERT для нашего класса User
будет выглядеть следующим образом:
INSERT INTO "user" (name) VALUES (%(name)s) RETURNING "user".id, "user".xmin
-- {'name': 'ed'}
Там, где описано выше, ORM может получить все новые значения первичного ключа вместе с идентификаторами версий, сгенерированными сервером, в одном операторе. Если бэкенд не поддерживает RETURNING, то для каждого INSERT и UPDATE необходимо выполнить дополнительный SELECT, что гораздо менее эффективно, а также влечет за собой возможность пропустить счетчики версий:
INSERT INTO "user" (name) VALUES (%(name)s)
-- {'name': 'ed'}
SELECT "user".version_id AS user_version_id FROM "user" where
"user".id = :param_1
-- {"param_1": 1}
Настоятельно рекомендуется* использовать счетчики версий на стороне сервера только в случае крайней необходимости и только на бэкендах, поддерживающих RETURNING, в настоящее время это PostgreSQL, Oracle, MariaDB 10.5, SQLite 3.35 и SQL Server.
Программные или условные счетчики версий¶
Когда version_id_generator
имеет значение False, мы также можем программно (и условно) установить идентификатор версии на нашем объекте таким же образом, как мы назначаем любой другой сопоставленный атрибут. Например, если мы используем наш пример UUID, но установим version_id_generator
в False
, мы можем установить идентификатор версии по своему усмотрению:
import uuid
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
version_uuid = mapped_column(String(32), nullable=False)
name = mapped_column(String(50), nullable=False)
__mapper_args__ = {"version_id_col": version_uuid, "version_id_generator": False}
u1 = User(name="u1", version_uuid=uuid.uuid4())
session.add(u1)
session.commit()
u1.name = "u2"
u1.version_uuid = uuid.uuid4()
session.commit()
Мы можем обновить наш объект User
без увеличения счетчика версий; значение счетчика останется неизменным, а оператор UPDATE будет по-прежнему сверяться с предыдущим значением. Это может быть полезно для схем, в которых только определенные классы UPDATE чувствительны к проблемам параллелизма:
# will leave version_uuid unchanged
u1.name = "u3"
session.commit()