sql-server - 多表复杂实体的乐观并发

标签 sql-server tsql stored-procedures triggers optimistic-locking

我有一个复杂的实体(我们称之为 Thing),它在 SQL Server 中表示为多个表:一个父表 dbo.Thing 和多个子表 dbo.ThingBodyPartdbo.ThingThought 等。我们在 dbo.Thing 上使用单个 rowversion 列实现了乐观并发>,使用UPDATE OUTPUT INTO technique 。在我们向 dbo.Thing 添加触发器之前,这一直很有效。我正在寻求选择不同方法的建议,因为我相当确信我当前的方法无法修复。

这是我们当前的代码:

CREATE PROCEDURE dbo.UpdateThing
    @id uniqueidentifier,
    -- ...
    -- ... other parameters describing what to update...
    -- ...
    @rowVersion binary(8) OUTPUT
AS
BEGIN TRANSACTION;
BEGIN TRY

    -- ...
    -- ... update lots of Thing's child rows...
    -- ...

    DECLARE @t TABLE (
        [RowVersion] binary(8) NOT NULL
    );

    UPDATE dbo.Thing
    SET ModifiedUtc = sysutcdatetime()
    OUTPUT INSERTED.[RowVersion] INTO @t
    WHERE
        Id = @id
        AND [RowVersion] = @rowVersion;

    IF @@ROWCOUNT = 0 RAISERROR('Thing has been updated by another user.', 16, 1);

    COMMIT;

    SELECT @rowVersion = [RowVersion] FROM @t;

END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK;
    EXEC usp_Rethrow_Error;
END CATCH

这工作得非常完美,直到我们向 dbo.Thing 添加了 INSTEAD OF UPDATE 触发器。现在,存储过程不再返回新的 @rowVersion 值,而是返回旧的未修改值。我不知所措。是否有其他方法可以实现乐观并发,与上述方法一样有效且简单,但也可以与触发器一起使用?

<小时/>

为了说明此代码到底出了什么问题,请考虑以下测试代码:

DECLARE
    @id uniqueidentifier = 'b0442c71-dbcb-4e0c-a178-1a01b9efaf0f',
    @oldRowVersion binary(8),
    @newRowVersion binary(8),
    @expected binary(8);

SELECT @oldRowVersion = [RowVersion]
FROM dbo.Thing
WHERE Id = @id;

PRINT '@oldRowVersion = ' + convert(char(18), @oldRowVersion, 1);

DECLARE @t TABLE (
    [RowVersion] binary(8) NOT NULL
);

UPDATE dbo.Thing
SET ModifiedUtc = sysutcdatetime()
OUTPUT INSERTED.[RowVersion] INTO @t
WHERE
    Id = @id
    AND [RowVersion] = @oldRowVersion;

PRINT '@@ROWCOUNT = ' + convert(varchar(10), @@ROWCOUNT);

SELECT @newRowVersion = [RowVersion] FROM @t;

PRINT '@newRowVersion = ' + convert(char(18), @newRowVersion, 1);

SELECT @expected = [RowVersion]
FROM dbo.Thing
WHERE Id = @id;

PRINT '@expected = ' + convert(char(18), @expected, 1);

IF @newRowVersion = @expected PRINT 'Pass!'
ELSE PRINT 'Fail.  :('

当触发器不存在时,此代码正确输出:

@oldRowVersion = 0x0000000000016CDC

(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x000000000004E9D1
@expected = 0x000000000004E9D1
Pass!

当触发器存在时,我们没有收到预期值:

@oldRowVersion = 0x0000000000016CDC

(1 row(s) affected)

(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x0000000000016CDC
@expected = 0x000000000004E9D1
Fail.  :(
<小时/>

有什么不同方法的想法吗?

我假设UPDATE是一个原子操作,确实如此,除非有触发器,而显然它不是。我错了吗?在我看来,这看起来真的很糟糕,因为每个语句背后都潜藏着潜在的并发错误。如果触发器确实是INSTEAD OF,我不应该返回正确的时间戳吗,就好像触发器的UPDATE是我实际执行的一样?这是 SQL Server 错误吗?

最佳答案

我尊敬的同事之一,Jonathan MacCollum ,向我指出了这个bit of documentation :

INSERTED

Is a column prefix that specifies the value added by the insert or update operation. Columns prefixed with INSERTED reflect the value after the UPDATE, INSERT, or MERGE statement is completed but before triggers are executed.

据此,我认为我需要修改我的存储过程,将一个 UPDATE 拆分为 UPDATE ,然后是 SELECT [RowVersion] .. ..

UPDATE dbo.Thing
SET ModifiedUtc = sysutcdatetime()
WHERE
    Id = @id
    AND [RowVersion] = @rowVersion;

IF @@ROWCOUNT = 0 RAISERROR('Thing has been updated by another user.', 16, 1);

COMMIT;

SELECT @rowVersion = [RowVersion]
FROM dbo.Thing
WHERE Id = @id;

我想我仍然可以放心,我的存储过程不会意外覆盖其他任何人的更改,但我不应该再假设存储过程的调用者保存的数据仍然是最新的。存储过程返回的新 @rowVersion 值有可能实际上是其他人更新的结果,而不是我的。所以实际上,返回 @rowVersion 根本没有意义。执行此存储过程后,调用者应重新获取 Thing 及其所有子记录,以确保其数据图片一致。

...这进一步让我得出结论,rowversion 列不是实现乐观锁定的最佳选择,遗憾的是这是它们的唯一目的。我最好使用手动递增的 int 列,并使用如下查询:

UPDATE dbo.Thing
SET Version = @version + 1
WHERE
    Id = @id
    AND Version = @version;

Version 列在单个原子操作中进行检查和递增,因此其他语句不会插入其中。我不必询问数据库新值是什么,因为我告诉它新值是什么。只要 Version 列包含我期望的值(并假设更新此行的所有其他人也遵守规则 - 正确递增 Version),我就可以知道东西仍然和我离开时一模一样。至少,我认为...

关于sql-server - 多表复杂实体的乐观并发,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21587223/

相关文章:

sql - 从一个表插入另一个表并激活触发器

sql - 使列长度成为另一个数据库中列的最大长度

sql - 修复 XML 解析的技术 : illegal qualified name character

mysql - sql 存储过程 "Default Parameter Values"

c# - 查询添加两列的值

sql-server - ROW_NUMBER 在 CTE 中不起作用

sql-server - 无法在 sql server 中将 varchar 转换为日期

sql-server - SQL 级联删除多列

mysql存储过程循环几天

stored-procedures - 如何在plsql中查找给定父记录的所有子记录?