Настройка счетчика версий

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 = Column(Integer, primary_key=True)
    version_id = Column(Integer, nullable=False)
    name = 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 = Column(Integer, primary_key=True)
    version_uuid = Column(String(32), nullable=False)
    name = 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 = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    xmin = 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, SQL Server (хотя SQL Server имеет major caveats при использовании триггеров), Firebird.

Добавлено в версии 0.9.0: Поддержка отслеживания идентификатора версии на стороне сервера.

Программные или условные счетчики версий

Когда version_id_generator имеет значение False, мы также можем программно (и условно) установить идентификатор версии на нашем объекте таким же образом, как мы назначаем любой другой сопоставленный атрибут. Например, если мы используем наш пример UUID, но установим version_id_generator в False, мы можем установить идентификатор версии по своему усмотрению:

import uuid


class User(Base):
    __tablename__ = "user"

    id = Column(Integer, primary_key=True)
    version_uuid = Column(String(32), nullable=False)
    name = 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()

Добавлено в версии 0.9.0: Поддержка программного и условного отслеживания идентификатора версии.

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