多表复杂实体的乐观并发

时间:2014-02-05 19:58:53

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

我有一个复杂的实体(我们称之为Thing),它在SQL Server中表示为多个表:一个父表dbo.Thing包含多个子表dbo.ThingBodyPart,{{1}我们使用dbo.ThingThoughtrowversion上使用单个dbo.Thing列实现了乐观并发。这一直很有效,直到我们为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

这非常精彩,直到我们向INSTEAD OF UPDATE添加dbo.Thing触发器。现在,存储过程不再返回新的@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的错误吗?

1 个答案:

答案 0 :(得分:1)

我尊敬的一位同事Jonathan MacCollum向我指出了bit of documentation

  

INSERTED

     

是列前缀,指定插入或更新操作添加的值。   以INSERTED为前缀的列反映UPDATE,INSERT或MERGE语句完成后的值,但在执行触发器之前

由此,我假设我需要修改我的存储过程,将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),我就知道{{1}仍然完全像我离开时那样。至少,我认为......