I’m working on a Windows desktop application written in C# 4.0 with a SQL Server 2005 backend. The application uses a Timestamp datatype field to handle data concurrency. Everything was working fine until I put some Triggers on the data tables to handle auditting. Now I am getting false data concurrency errors when I run my test scripts. It’s as if the triggers are updating the Timestamp field that I am using to manage concurrency.
Does that sound right? And if so, is there anything I can do about it?
In case you need more information here is a brief description of how the concurrency checking works. When a record is loaded, it reads the Timestamp datatype value and stores it in the class along with all the other data.
When the user attempts to save the data, the class begins a transaction, reads the record from the database and compares the Timestamp fields.
If they match it goes ahead with the save in the same transaction, and grabs the new Timestamp with and T-SQL statement that lookds like “UPDATE … ; SELECT @@DBTS”.
If the Timestamps don’t match it throws a data concurrency exception.
It was working as planned before I added the audit triggers, but now it always throws a data concurrency exception if a record is updated and then updated again. My guess is that it is getting the new timestamp value after the update, but the trigger causes it to change again after that.
Here is the code that performs the update:
// Begin Transaction
SqlConnection conn = new SqlConnection(DataGateway.ConnStr);
SqlTransaction trans;
conn.Open();
trans = conn.BeginTransaction();
// Read current record
DataTable dt = base.Select(conn, trans);
// Timestamps match?
DataRow row = dt.Rows[0];
if (RowversionsEqual(Rowversion, (byte[])row["Rowversion"])) // Rowversion is a class property that holds the Timestamp obtained when data is initially read, Rowversions equal is a function that compares two Timestamp values
{
// Timestamps match, update record
SqlCommand cmd = new SqlCommand("UPDATE WrdImp SET Imp = @Imp, Note = @Note, EditDate = @EditTimestamp, EditBy = @EditBy WHERE BID = @BID AND WID = @WID; SELECT @@DBTS", conn, trans);
// Code to insert parameter values
Rowversion = (byte[])cmd.ExecuteScalar();
trans.Commit();
}
else
{
// Another user has made an interim change, notify user
trans.Rollback();
conn.Close();
throw new ImpDataConcurrencyException(dt.Rows[0]["EditBy"].ToString(), (DateTime)dt.Rows[0]["EditDate"],MsgComponent.Title, dt.Rows[0]["Imp"].ToString(), dt.Rows[0]["Note"].ToString());
}
Here is one of the update triggers. It was auto-generated by a third party product called APEX SQL Audit.
ALTER TRIGGER [dbo].[tr_u_AUDIT_WrdImp]
ON [dbo].[WrdImp]
FOR UPDATE
NOT FOR REPLICATION
As
BEGIN
DECLARE
@IDENTITY_SAVE varchar(50),
@AUDIT_LOG_TRANSACTION_ID Int,
@PRIM_KEY nvarchar(4000),
@Inserted bit,
--@TABLE_NAME nvarchar(4000),
@ROWS_COUNT int
SET NOCOUNT ON
--Set @TABLE_NAME = '[dbo].[WrdImp]'
Select @ROWS_COUNT=count(*) from inserted
SET @IDENTITY_SAVE = CAST(IsNull(@@IDENTITY,1) AS varchar(50))
INSERT
INTO [PLIMS].dbo.AUDIT_LOG_TRANSACTIONS
(
TABLE_NAME,
TABLE_SCHEMA,
AUDIT_ACTION_ID,
HOST_NAME,
APP_NAME,
MODIFIED_BY,
MODIFIED_DATE,
AFFECTED_ROWS,
[DATABASE]
)
values(
'WrdImp',
'dbo',
1, -- ACTION ID For UPDATE
CASE
WHEN LEN(HOST_NAME()) < 1 THEN ' '
ELSE HOST_NAME()
END,
CASE
WHEN LEN(APP_NAME()) < 1 THEN ' '
ELSE APP_NAME()
END,
SUSER_SNAME(),
GETDATE(),
@ROWS_COUNT,
'PLIMS'
)
Set @AUDIT_LOG_TRANSACTION_ID = SCOPE_IDENTITY()
SET @Inserted = 0
If UPDATE([Imp])
BEGIN
INSERT
INTO [PLIMS].dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
OLD_VALUE_LONG,
NEW_VALUE_LONG,
DATA_TYPE
, KEY1, KEY2
)
SELECT
@AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[WID]='+CONVERT(nvarchar(4000), IsNull(OLD.[WID], NEW.[WID]), 0), '[WID] Is Null')+' AND '+IsNull('[BID]='+CONVERT(nvarchar(4000), IsNull(OLD.[BID], NEW.[BID]), 0), '[BID] Is Null')),
'Imp',
CONVERT(nvarchar(4000), OLD.[Imp], 0),
CONVERT(nvarchar(4000), NEW.[Imp], 0),
'A'
, IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[WID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[WID], 0))), IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[BID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[BID], 0)))
FROM deleted OLD Inner Join inserted NEW On
(CONVERT(nvarchar(4000), NEW.[WID], 0)=CONVERT(nvarchar(4000), OLD.[WID], 0) or (NEW.[WID] Is Null and OLD.[WID] Is Null)) AND (CONVERT(nvarchar(4000), NEW.[BID], 0)=CONVERT(nvarchar(4000), OLD.[BID], 0) or (NEW.[BID] Is Null and OLD.[BID] Is Null))
where (
(
NEW.[Imp] <>
OLD.[Imp]
) Or
(
NEW.[Imp] Is Null And
OLD.[Imp] Is Not Null
) Or
(
NEW.[Imp] Is Not Null And
OLD.[Imp] Is Null
)
)
SET @Inserted = CASE WHEN @@ROWCOUNT > 0 Then 1 Else @Inserted End
END
If UPDATE([Note])
BEGIN
INSERT
INTO [PLIMS].dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
OLD_VALUE_LONG,
NEW_VALUE_LONG,
DATA_TYPE
, KEY1, KEY2
)
SELECT
@AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[WID]='+CONVERT(nvarchar(4000), IsNull(OLD.[WID], NEW.[WID]), 0), '[WID] Is Null')+' AND '+IsNull('[BID]='+CONVERT(nvarchar(4000), IsNull(OLD.[BID], NEW.[BID]), 0), '[BID] Is Null')),
'Note',
CONVERT(nvarchar(4000), OLD.[Note], 0),
CONVERT(nvarchar(4000), NEW.[Note], 0),
'A'
, IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[WID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[WID], 0))), IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[BID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[BID], 0)))
FROM deleted OLD Inner Join inserted NEW On
(CONVERT(nvarchar(4000), NEW.[WID], 0)=CONVERT(nvarchar(4000), OLD.[WID], 0) or (NEW.[WID] Is Null and OLD.[WID] Is Null)) AND (CONVERT(nvarchar(4000), NEW.[BID], 0)=CONVERT(nvarchar(4000), OLD.[BID], 0) or (NEW.[BID] Is Null and OLD.[BID] Is Null))
where (
(
NEW.[Note] <>
OLD.[Note]
) Or
(
NEW.[Note] Is Null And
OLD.[Note] Is Not Null
) Or
(
NEW.[Note] Is Not Null And
OLD.[Note] Is Null
)
)
SET @Inserted = CASE WHEN @@ROWCOUNT > 0 Then 1 Else @Inserted End
END
If UPDATE([EditDate])
BEGIN
INSERT
INTO [PLIMS].dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
OLD_VALUE_LONG,
NEW_VALUE_LONG,
DATA_TYPE
, KEY1, KEY2
)
SELECT
@AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[WID]='+CONVERT(nvarchar(4000), IsNull(OLD.[WID], NEW.[WID]), 0), '[WID] Is Null')+' AND '+IsNull('[BID]='+CONVERT(nvarchar(4000), IsNull(OLD.[BID], NEW.[BID]), 0), '[BID] Is Null')),
'EditDate',
CONVERT(nvarchar(4000), OLD.[EditDate], 121),
CONVERT(nvarchar(4000), NEW.[EditDate], 121),
'A'
, IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[WID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[WID], 0))), IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[BID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[BID], 0)))
FROM deleted OLD Inner Join inserted NEW On
(CONVERT(nvarchar(4000), NEW.[WID], 0)=CONVERT(nvarchar(4000), OLD.[WID], 0) or (NEW.[WID] Is Null and OLD.[WID] Is Null)) AND (CONVERT(nvarchar(4000), NEW.[BID], 0)=CONVERT(nvarchar(4000), OLD.[BID], 0) or (NEW.[BID] Is Null and OLD.[BID] Is Null))
where (
(
NEW.[EditDate] <>
OLD.[EditDate]
) Or
(
NEW.[EditDate] Is Null And
OLD.[EditDate] Is Not Null
) Or
(
NEW.[EditDate] Is Not Null And
OLD.[EditDate] Is Null
)
)
SET @Inserted = CASE WHEN @@ROWCOUNT > 0 Then 1 Else @Inserted End
END
If UPDATE([EditBy])
BEGIN
INSERT
INTO [PLIMS].dbo.AUDIT_LOG_DATA
(
AUDIT_LOG_TRANSACTION_ID,
PRIMARY_KEY_DATA,
COL_NAME,
OLD_VALUE_LONG,
NEW_VALUE_LONG,
DATA_TYPE
, KEY1, KEY2
)
SELECT
@AUDIT_LOG_TRANSACTION_ID,
convert(nvarchar(1500), IsNull('[WID]='+CONVERT(nvarchar(4000), IsNull(OLD.[WID], NEW.[WID]), 0), '[WID] Is Null')+' AND '+IsNull('[BID]='+CONVERT(nvarchar(4000), IsNull(OLD.[BID], NEW.[BID]), 0), '[BID] Is Null')),
'EditBy',
CONVERT(nvarchar(4000), OLD.[EditBy], 0),
CONVERT(nvarchar(4000), NEW.[EditBy], 0),
'A'
, IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[WID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[WID], 0))), IsNULL( CONVERT(nvarchar(500), CONVERT(nvarchar(4000), OLD.[BID], 0)), CONVERT(nvarchar(500), CONVERT(nvarchar(4000), NEW.[BID], 0)))
FROM deleted OLD Inner Join inserted NEW On
(CONVERT(nvarchar(4000), NEW.[WID], 0)=CONVERT(nvarchar(4000), OLD.[WID], 0) or (NEW.[WID] Is Null and OLD.[WID] Is Null)) AND (CONVERT(nvarchar(4000), NEW.[BID], 0)=CONVERT(nvarchar(4000), OLD.[BID], 0) or (NEW.[BID] Is Null and OLD.[BID] Is Null))
where (
(
NEW.[EditBy] <>
OLD.[EditBy]
) Or
(
NEW.[EditBy] Is Null And
OLD.[EditBy] Is Not Null
) Or
(
NEW.[EditBy] Is Not Null And
OLD.[EditBy] Is Null
)
)
SET @Inserted = CASE WHEN @@ROWCOUNT > 0 Then 1 Else @Inserted End
END
-- Watch
-- Lookup
IF @Inserted = 0
BEGIN
DELETE FROM [PLIMS].dbo.AUDIT_LOG_TRANSACTIONS WHERE AUDIT_LOG_TRANSACTION_ID = @AUDIT_LOG_TRANSACTION_ID
END
-- Restore @@IDENTITY Value
DECLARE @maxprec AS varchar(2)
SET @maxprec=CAST(@@MAX_PRECISION as varchar(2))
EXEC('SELECT IDENTITY(decimal('+@maxprec+',0),'+@IDENTITY_SAVE+',1) id INTO #tmp')
End
GO
EXEC sp_settriggerorder @triggername=N'[dbo].[tr_u_AUDIT_WrdImp]', @order=N'Last', @stmttype=N'UPDATE'
@@DBTSgives you the most recentrowversionvalue from the entire database – so yes, if any part of the trigger touches another table that also has arowversioncolumn, then you’ll get a different answer.Could you change your
UPDATEto use anOUTPUTclause?A single statement like:
Where I’ve added the old
rowversionvalue (so SQL can do the check and we don’t need to open an explicit transaction, nor needRowversionsEqual), and am returning the newrowversionvalue.So you execute the above statement, and either: a) zero rows are returned – this means that something else updated this row, or b) one row is returned (assuming the rest of the
WHEREclause is correctly limiting theUPDATEto one row), and it’s guaranteed to contain the value of therowversioncolumn in that row, as it was when theUPDATEcompleted.I’d forgotten the restriction re: output clause and triggers. I don’t have a 2005 instance handy at the moment, but something like:
Which is now 3 statements rather than 1, but still ensures you’re capturing the
rowversionvalue from the row of interest, rather than the most recent value anywhere in the database.