web-dev-qa-db-ja.com

SQLサーバーのトリガーが失敗したときにトランザクションをコミットできますか?

テーブルの挿入トリガーが失敗したときのデータ損失を回避しようとしています。次のシナリオでこれを試していますが、コードが失敗します。

Customersテーブルで挿入が発生した場合、同じデータをArchiveテーブルに挿入します。トリガーが失敗した場合、トランザクション全体をロールバックしたくありません。これはCustomersテーブルのデータ損失につながります。

トリガーが失敗した場合でも、データをCustomersテーブルに挿入し、ストアドプロシージャは通常どおりCustomer_IDを返す必要があります。

ALTER TRIGGER [dbo].[Customer_Insert_Trigger_Test] 
   ON  [dbo].[Customers]
   AFTER INSERT
AS 
BEGIN

BEGIN TRY
    begin transaction;
    set nocount  on;
    SAVE TRANSACTION InsertSaveHere;
    --Simulating error situation
    RAISERROR (N'This is message %s %d.', -- Message text.
       11, -- Severity,
       1, -- State,
       N'number', -- First argument.
       5); -- Second argument.

   Insert into Archive select * from Inserted;
    commit transaction;

  END TRY

  BEGIN CATCH
    ROLLBACK TRANSACTION InsertSaveHere;
  END CATCH
END

私の質問は主に、トリガーが失敗した場合に、customersテーブルに実際に挿入してロールバックしないようにする方法に関するものです。そのために私のコードを変更するには?

7
PushCode

質問は「コードが失敗している」と述べていますが、エラーメッセージや具体的に何が失敗しているのかを示すものはありません。これらの情報の両方ではなくても、少なくとも1つを含めることは、常により良い答えを得るのに役立ちます。

とりあえず、トリガーとトランザクションについて誤った仮定のように見えるものがあります。@@TRANCOUNTを呼び出してBEGIN TRAN;をインクリメントしますが、@@TRANCOUNT行がTRYブロック内で実行され、エラーがない場合にのみCOMMIT TRAN;をデクリメントします。エラーの場合、COMMITはスキップされ、セーブポイントのROLLBACKが発生します。ただし、セーブポイントをロールバックしても、@@TRANCOUNTは減りません。この場合、INSERT操作は終了し、トランザクションはまだアクティブです。

トリガーは、トリガーを起動したDML操作にバインドする、内部で開始されたトランザクション内に存在します。これは、トリガー内でROLLBACKを呼び出して、そのDML操作をキャンセルする方法です。

オプション1A(エラーがトランザクションをキャンセルしないようにする-トリガーに複数のDMLステートメントがある場合は優先)

これを念頭に置いて、これを機能させるために、BEGIN TRAN;およびCOMMIT TRAN;行を削除できるはずです。正味の効果は、エラーがない場合、INSERTからArchiveテーブルへの期待どおりのコミットですが、エラーがある場合は、ROLLBACKを保存ポイントまで実行して続行します。

ただし、これら2つの部分を削除しても、次のエラーが発生するという厄介な状況が残ります。

メッセージ3931、レベル16、状態1、手順Customer_Insert_Trigger_Test、行78
現在のトランザクションはコミットできず、セーブポイントにロールバックできません。トランザクション全体をロールバックします。

この動作の理由は、トリガーを呼び出すときにシステムがXACT_ABORT ONを暗黙的に設定しているためです。 XACT_ABORT ONの効果は、anyエラーが発生した場合にトランザクションをキャンセルすることです。治療法は?トリガーの先頭にXACT_ABORT OFFを設定するだけです。

たとえば、以下は私にとってはうまくいきます:

CREATE
--ALTER
TRIGGER [dbo].[Customer_Insert_Trigger_Test] 
   ON  [dbo].[Inserts]
   AFTER INSERT
AS 
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;

BEGIN TRY
    PRINT '@@TRANCOUNT = ' + CONVERT(VARCHAR(10), @@TRANCOUNT); -- for debug only
    SAVE TRANSACTION InsertSaveHere;

    --Simulating error situation
        RAISERROR (N'This is message %s %d.', -- Message text.
          11, -- Severity,
          1, -- State,
          N'number', -- First argument.
          5); -- Second argument.

   --Insert into Archive select * from Inserted;
END TRY
BEGIN CATCH
    PRINT 'Entering CATCH block...'; -- for debug only
    ROLLBACK TRANSACTION InsertSaveHere;
END CATCH;

END;

このメソッドはnotを実行することに注意してください==このテーブルでのトリガーの予想される動作を変更します:1)最も外側のレイヤーで発生する実際のCOMMIT(最初のDMLステートメントまたはそのステートメントの前に明示的なトランザクションが開始されていた場合はそれ以上)、2)このテーブルの他の潜在的なトリガーがROLLBACKを発行して操作をキャンセルできないこと、および3)DMLの前に開始された明示的なトランザクションの機能このテーブルのステートメント。ROLLBACKを発行して、このテーブルのDML操作を含むすべての変更をキャンセルします。

オプション1B(エラーによるトランザクションのキャンセルの防止-トリガーに単一のDMLステートメントがある場合は優先)

もちろん、ここでエラーが発生する可能性があるのがINSERTテーブルへのArchiveだけである場合は、SAVE TRANROLLBACK TRANSACTION InsertSaveHere;を削除し、CATCHブロックで何かを実行して、DECLARE @Test INT;のように空にならないようにすることもできます。うまくいくかもしれません。ここでの推論は、エラーが実際に発生したことのない単一のDMLステートメントであるため、ロールバックするものは何もないということです;-)。


オプション2(非推奨)

タイトルに記載されている質問に答えるには、トリガー内でCOMMITを使用できるようにする必要がありますが、私はeXtreeemely予想されるものを変更するので、そのようなことをするのに慎重ですトランザクションがコミットまたはロールバックするときの動作。これにより、このテーブルの他のトリガーの適切な操作が妨げられる可能性があります(このトリガーが最初に実行されると、ROLLBACKを発行して操作をキャンセルできなくなります)。このテーブルでのDML操作の前に明示的なトランザクションが開始されました。

これを行うには(注:この段落を読み続ける前に、すぐ上の段落を読む必要があります)、COMMIT TRAN;を発行します(トリガーがトランザクション内に既に存在するため)。 BEGIN TRAN;COMMIT TRAN;は最初のDML操作をコミットし、BEGIN TRAN;@@TRANCOUNTを1に戻して、トリガーの実行が終了したときに、トリガーが開始時とは異なる@@TRANCOUNTで終了したことを示すエラーが発生しないようにします。

7
Solomon Rutzky

トリガーはすでに暗黙のトランザクション内で常に動作しています。関連する質問を参照してください:

SQL Serverトリガーが実行されることを確認する方法はありますか?

エラーをキャッチして中止/ロールバックを防ぐことができますが、トリガー内からトランザクションを「コミット」することはできません。