2

我正在使用带有 SQL Server 2005 后端的 C# 4.0 编写的 Windows 桌面应用程序。应用程序使用 Timestamp 数据类型字段来处理数据并发。一切正常,直到我在数据表上放置了一些触发器来处理审计。现在,当我运行我的测试脚本时,我得到了错误的数据并发错误。就好像触发器正在更新我用来管理并发性的时间戳字段。
听起来对吗?如果是这样,我能做些什么吗?

如果您需要更多信息,这里简要描述了并发检查的工作原理。加载记录时,它会读取 Timestamp 数据类型值并将其与所有其他数据一起存储在类中。
当用户尝试保存数据时,该类开始一个事务,从数据库中读取记录并比较 Timestamp 字段。
如果它们匹配,则在同一个事务中继续保存,并使用看起来像“UPDATE ... ; SELECT @@DBTS”的 T-SQL 语句获取新的时间戳。
如果时间戳不匹配,则会引发数据并发异常。

在我添加审计触发器之前它按计划工作,但现在如果记录更新然后再次更新,它总是会引发数据并发异常。我的猜测是它在更新后获取了新的时间戳值,但触发器导致它在此之后再次更改。

以下是执行更新的代码:

// 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());
}

这是更新触发器之一。它是由名为 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'
4

1 回答 1

1

@@DBTS为您提供整个数据库中的最新rowversion值 - 所以是的,如果触发器的任何部分触及另一个也有rowversion列的表,那么您将得到不同的答案。

你能改变你UPDATE的使用OUTPUT条款吗?

一个单一的声明,如:

UPDATE WrdImp
SET
    Imp = @Imp,
    Note = @Note,
    EditDate = @EditTimestamp,
    EditBy = @EditBy
OUTPUT
    inserted.rowversion
WHERE
    BID = @BID AND
    WID = @WID AND
    rowversion = @OldRowVersion;

我在其中添加了旧rowversion值(因此 SQL 可以进行检查,我们不需要打开显式事务,也不需要RowversionsEqual),并返回新rowversion值。

因此,您执行上述语句,并且:a)返回零行 - 这意味着其他内容更新了该行,或 b)返回一行(假设WHERE子句的其余部分正确地将 限制UPDATE为一行),并且它保证包含该rowversion行中列的值,就像UPDATE完成时一样。


我忘记了限制 re: output 子句和触发器。我目前手头没有 2005 实例,但类似于:

DECLARE @RV table (RV binary(8));
UPDATE WrdImp
SET
    Imp = @Imp,
    Note = @Note,
    EditDate = @EditTimestamp,
    EditBy = @EditBy
OUTPUT
    inserted.rowversion INTO @RV(RV)
WHERE
    BID = @BID AND
    WID = @WID AND
    rowversion = @OldRowVersion;
SELECT RV from @RV;

现在是 3 个语句而不是 1 个,但仍确保您rowversion从感兴趣的行捕获值,而不是数据库中任何位置的最新值。

于 2012-04-30T06:23:06.677 回答