2

SQL Server 2005 バックエンドを使用して、C# 4.0 で記述された Windows デスクトップ アプリケーションに取り組んでいます。アプリケーションは Timestamp データ型フィールドを使用して、データの同時実行性を処理します。監査を処理するためにデータテーブルにいくつかのトリガーを配置するまで、すべてが正常に機能していました。テスト スクリプトを実行すると、誤ったデータ同時実行エラーが発生するようになりました。同時実行を管理するために使用している 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());
}

更新トリガーの 1 つを次に示します。これは、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

@@DBTSrowversionデータベース全体からの最新の値を提供します。つまり、トリガーの一部が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) 0 行が返される - これは、他の何かがこの行を更新したことを意味します。または、b) 1 行が返されます (WHERE句の残りの部分が 1 行に正しく制限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;

これは 1 つではなく 3 つのステートメントrowversionになりましたが、データベース内の最新の値ではなく、関心のある行から値をキャプチャしていることを保証します。

于 2012-04-30T06:23:06.677 に答える