配置版本计数器


Mapper 支持对版本 ID 列的管理,该列是单个表列,每次对映射表执行 UPDATE 时,该列都会递增或以其他方式更新其值。每次 ORM 对行发出 UPDATEDELETE 时,都会检查此值,以确保内存中保存的值与数据库值匹配。


警告


因为版本控制功能依赖于内存中 record 的 Package,该功能仅适用于 Session.flush() 进程,其中 ORM 将各个内存中的行刷新到数据库中。 使用 Query.update() Query.delete() 执行多行 UPDATE 或 DELETE 时不生效 方法,因为这些方法只发出 UPDATE 或 DELETE 语句,否则 无权直接访问受影响的行的内容。


此功能的目的是检测两个并发事务何时大致同时修改同一行,或者防止在系统中使用“过时”行,该行可能会重用前一个事务中的数据而不刷新(例如,如果将 expire_on_commit=False 使用 Session 时,可以重用先前事务中的数据)。


简单版本计数


跟踪版本最直接的方法是向映射表中添加一个整数列,然后将其建立为 mapper 选项中的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” 是我们已知在此对象上使用的最后一个版本标识符。如果其他地方的事务独立修改了该行,则此版本 ID 将不再匹配,并且 UPDATE 语句将报告没有匹配的行;这是 SQLAlchemy 测试的条件,恰好有一行与我们的 UPDATE(或 DELETE)语句匹配。如果没有行匹配,则表示我们的数据版本已过时,并引发 StaleDataError


自定义版本计数器/类型¶


其他类型的值或计数器可用于版本控制。常见类型包括 dates 和 GUID。当使用备用类型或计数器方案时,SQLAlchemy 使用 version_id_generator 参数为此方案提供一个钩子,该参数接受版本生成可调用对象。此 callable 将传递当前已知版本的值,并预期返回后续版本。


例如,如果我们想使用随机生成的 GUID 跟踪 User 类的版本控制,我们可以这样做(请注意,某些后端支持本机 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,
    }


持久化引擎将在每次 用户对象受 INSERT 或 UPDATE 的约束。在这种情况下,我们的版本生成函数可以忽略 version 的传入值,因为 uuid4() 函数生成标识符而没有任何先决条件值。如果我们使用顺序版本控制方案,例如数字或特殊字符系统,则可以使用给定的版本来帮助确定后续值。


服务器端版本计数器


还可以将 version_id_generator 配置为依赖于数据库生成的值。在这种情况下,当一行受 INSERT 和 UPDATE 约束时,数据库将需要一些方法来生成新标识符。对于 UPDATE 情况,通常需要更新触发器,除非相关数据库支持其他一些本机版本标识符。PostgreSQL 数据库特别支持名为 xmin 的系统列 它提供 UPDATE 版本控制。 我们可利用 的 PostgreSQL xmin 列来对我们的用户进行版本控制 类,如下所示:

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 列自动提供版本 id 计数器的新值。


ORM 在发出 INSERT 或 UPDATE 时通常不会主动获取数据库生成的值,而是将这些列保留为“过期”,并在下次访问它们时获取,除非设置了 eager_defaultsMapper 标志。但是,当使用服务器端版本列时,ORM 需要主动获取新生成的值。这样,版本计数器就可以在之前设置 任何并发事务都可以再次更新它。 此获取也是 最好使用 RETURNING 在 INSERT 或 UPDATE 语句中同时完成,否则,如果之后发出 SELECT 语句,仍然存在潜在的争用条件,即版本计数器可能会在获取之前发生变化。


当目标数据库支持 RETURNING 时,我们的 User 类的 INSERT 语句将如下所示:

INSERT INTO "user" (name) VALUES (%(name)s) RETURNING "user".id, "user".xmin
-- {'name': 'ed'}


在上面,ORM 可以在一个语句中获取任何新生成的主键值以及服务器生成的版本标识符。当后端不支持 RETURNING 时,必须为每个 INSERT 和 UPDATE,这效率要低得多,并且还引入了 错过的版本计数器:

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 Database、MariaDB 10.5、SQLite 3.35 和 SQL Server。


编程或条件版本计数器


version_id_generator 设置为 False 时,我们还可以以编程方式(和有条件地)设置对象上的版本标识符,就像我们分配任何其他 mapped 属性一样。例如,如果我们使用了 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 对象;counter 的值将保持不变,并且 UPDATE 语句仍将检查以前的值。这对于只有某些 UPDATE 类对并发问题敏感的方案可能很有用:

# will leave version_uuid unchanged
u1.name = "u3"
session.commit()