I have a complex entity (let's call it Thing
) which is represented in SQL Server as many tables: one parent table dbo.Thing
with several child tables dbo.ThingBodyPart
, dbo.ThingThought
, etc. We've implemented optimistic concurrency using a single rowversion
column on dbo.Thing
, using the UPDATE OUTPUT INTO
technique. This has been working great, until we added a trigger to dbo.Thing
. I'm looking for advice in choosing a different approach, because I'm fairly convinced that my current approach cannot be fixed.
Here is our current code:
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
This worked absolutely beautifully, until we added an INSTEAD OF UPDATE
trigger to dbo.Thing
. Now the stored procedure no longer returns the new @rowVersion
value, but returns the old unmodified value. I'm at a loss. Are there other ways to approach optimistic concurrency that would be as effective and easy as the one above, but would also work with triggers?
To illustrate what exactly goes wrong with this code, consider this test code:
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. :('
When the trigger is not present, this code correctly outputs:
@oldRowVersion = 0x0000000000016CDC
(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x000000000004E9D1
@expected = 0x000000000004E9D1
Pass!
When the trigger is present, we do not receive the expected value:
@oldRowVersion = 0x0000000000016CDC
(1 row(s) affected)
(1 row(s) affected)
@@ROWCOUNT = 1
@newRowVersion = 0x0000000000016CDC
@expected = 0x000000000004E9D1
Fail. :(
Any ideas for a different approach?
I was assuming that an UPDATE
was an atomic operation, which it is, except when there are triggers, when apparently it's not. Am I wrong? This seems really bad, in my opinion, with potential concurrency bugs lurking behind every statement. If the trigger really is INSTEAD OF
, shouldn't I get back the correct timestamp, as though the trigger's UPDATE
was the one I actually executed? Is this a SQL Server bug?