我有一个复杂的实体(我们称之为Thing
),它在SQL Server中表示为多个表:一个父表dbo.Thing
包含多个子表dbo.ThingBodyPart
,{{1}我们使用dbo.ThingThought
在rowversion
上使用单个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的错误吗?
答案 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}仍然完全像我离开时那样。至少,我认为......