Dan*_*ing 5 t-sql sql-server triggers stored-procedures optimistic-locking
我有一个复杂的实体(我们称之为Thing),这是在SQL Server中表示为很多表:一个父表dbo.Thing与多个子表dbo.ThingBodyPart,dbo.ThingThought等等.我们已经使用一个单一的实现乐观并发rowversion上柱dbo.Thing,使用UPDATE OUTPUT INTO的技术.这一直很有效,直到我们添加了一个触发器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
Run Code Online (Sandbox Code Playgroud)
这非常美妙,直到我们添加了一个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. :('
Run Code Online (Sandbox Code Playgroud)
当触发器不存在时,此代码正确输出:
@oldRowVersion = 0x0000000000016CDC
(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x000000000004E9D1
@expected = 0x000000000004E9D1
Pass!
Run Code Online (Sandbox Code Playgroud)
当触发器出现时,我们没有收到预期值:
@oldRowVersion = 0x0000000000016CDC
(1 row(s) affected)
(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x0000000000016CDC
@expected = 0x000000000004E9D1
Fail. :(
Run Code Online (Sandbox Code Playgroud)
对于不同方法的任何想法?
我假设一个UPDATE是一个原子操作,它是,除非有触发器,显然它不是.我错了吗?在我看来,这似乎非常糟糕,潜在的并发错误隐藏在每个语句背后.如果触发器真的是 INSTEAD OF,我不应该回到正确的时间戳,就好像触发器UPDATE是我实际执行的那个?这是一个SQL Server错误吗?
我的一位尊敬的同事Jonathan MacCollum向我指出了这段文档:
已插入
是列前缀,指定插入或更新操作添加的值。以 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;
Run Code Online (Sandbox Code Playgroud)
我想我仍然可以放心,我的存储过程不会意外地覆盖其他任何人的更改,但我不应该再假设存储过程的调用者保存的数据仍然是最新的。存储过程返回的新值有可能@rowVersion实际上是其他人更新的结果,而不是我的。@rowVersion所以实际上,返回根本没有意义。执行此存储过程后,调用者应重新获取Thing其所有子记录,以确保其数据图片一致。
...这进一步使我得出结论,rowversion列并不是实现乐观锁定的最佳选择,遗憾的是,这正是它们的唯一目的。我最好使用手动递增的int列,查询如下:
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;
Run Code Online (Sandbox Code Playgroud)
该Version列在单个原子操作中进行检查和递增,因此其他语句没有机会插入其中。我不必询问数据库新值是什么,因为我告诉它新值是什么。只要该Version列包含我期望的值(并且假设更新此行的所有其他人也都遵守规则 - 正确递增Version),我就可以知道它Thing仍然与我离开时完全相同。至少,我认为...
| 归档时间: |
|
| 查看次数: |
1100 次 |
| 最近记录: |