web-dev-qa-db-ja.com

ストアドプロシージャのトランザクションでエラーを処理する方法

ストアドプロシージャのトランザクションでエラーをキャッチし、catchブロックのテーブルに記録する必要があります。

トランザクションの開始後、ループはテーブルにいくつかの値を挿入しようとします。各挿入ステートメントをtry/catchブロックで囲んでいるため、いずれかの挿入で主キー違反が発生した場合は、ログテーブルにレコードを挿入してエラーを処理します。次に、ループが完了し、いずれかが失敗した場合でも、すべての挿入を試みたら、トランザクションをコミットします。

エラーをキャッチして処理している場合でも、プロシージャの最初でSET XACT_ABORT ONを呼び出すと、PKエラーが発生したときにトランザクションが中止されるのではないかと心配しています。それは本当ですか、それともtry/catchがエラーをインターセプトし、トランザクションが中止されないようにエラーを抑制しますか?

Try/catchがエラーによるトランザクションの中止を止めない場合、代わりにSET XACT_ABORT OFFを呼び出して、必要なエラートラップ動作を取得できますか?

これが動作をテストするためのコードです。

まず、ターゲットテーブルとログテーブルを作成します。

CREATE TABLE ErrorTestTable (a int primary key clustered)
CREATE TABLE ErrorLogTable (m nvarchar(500), d datetime2(7))

次に、プロシージャを作成します。

CREATE PROCEDURE [dbo].[ErrorTest] 
AS
BEGIN
    SET XACT_ABORT ON;
    SET NOCOUNT ON;

    DECLARE @Try int = 0;
    DECLARE @MaxTries int = 5;

    BEGIN TRAN;

    WHILE @Try < @MaxTries
    BEGIN

        BEGIN TRY
            print 'begin try - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
            INSERT INTO ErrorTestTable (a) VALUES (1)
            INSERT INTO ErrorLogTable(m,d) VALUES ('successfully inserted record!', sysutcdatetime());
        END TRY
        BEGIN CATCH
            PRINT 'begin catch - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
            INSERT INTO ErrorLogTable(m,d) VALUES ('pk violation!', sysutcdatetime());
        END CATCH
        SET @Try += 1;
    END

    COMMIT TRAN;

 END

最後に、これらのステートメントを実行して、動作を観察します。

DELETE FROM ErrorLogTable
DELETE FROM ErrorTestTable
SELECT * FROM ErrorLogTable
SELECT * FROM ErrorTestTable
BEGIN TRY
   EXEC dbo.ErrorTest;
END TRY
BEGIN CATCH
   PRINT 'Error thrown by procedure.';
END CATCH
SELECT * FROM ErrorLogTable
SELECT * FROM ErrorTestTable

簡単な手順をテストしたところ、いくつかのことがわかりました。 SET XACT_ABORT ONでは、最初の主キー違反が発生すると、実行はキャッチハンドラにジャンプします。その時点で、XACT_STATEによるトランザクションはコミットできません(-1)。 catchブロック内で、ログテーブルに挿入しようとすると、別のエラーがスローされ、ストアドプロシージャが終了します。私はsprocの外でそのエラーをキャッチし、2つのテーブルから選択します。そこで、コミットされていないデータを確認できます。

その時点でトランザクションはまだロールバックされておらず、バッチ全体が完了するまで(sproc呼び出し後の「select」呼び出しを含む)、最終的に次のエラーメッセージが表示されます

コミットできないトランザクションがバッチの最後に検出されました。トランザクションはロールバックされます。

そして最後に操作をロールバックします

コメントへの応答

最初に情報を検証するだけでなく、プロシージャを呼び出すアプリケーションでエラー処理を行うのはなぜですか?

すでに両方を行っています。検証するだけでなく、挿入される値のカスタムアプリケーションロックを取得するため、ロックの競合は事実上存在せず、PK違反はbasically不可能です。エラーが発生した場合に処理できるかどうかを知りたいだけです。これは非常に高い同時実行性(100以上のスレッド)による「未使用」のランダムな識別子の迅速な挿入であるため、Xのランダムな値がすでに使用されているかどうかを確認するだけでなく、挿入するものに決定する必要があります試行中値のロックを解除します。ロックの取得に成功した後、まだが使用されていない場合は、挿入します。

3
Triynko

問題は、トランザクションがコミットできなくなると(つまり、エラーが発生すると)、ループが失敗を受け入れずに、テーブルにデータを挿入し続けるという事実から生じます。 DBEが後続の変更をコミットしようとすると、トランザクションが無効になるため、コミットできません。 TRY/CATCHWHILEループの外に移動すると、問題の一部が解決します。

CREATE PROCEDURE [dbo].[ErrorTest] 
AS
BEGIN
SET NOCOUNT ON;

DECLARE @error TABLE  (m nvarchar(500), d datetime2(7));
DECLARE @Try int = 0;
DECLARE @MaxTries int = 5;

BEGIN TRAN;
BEGIN TRY
    WHILE @Try < @MaxTries
    BEGIN

        print 'begin try - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
        INSERT INTO ErrorTestTable (a) 
        VALUES (1);
        INSERT INTO ErrorLogTable(m,d) 
        VALUES ('successfully inserted record!', sysutcdatetime());

        SET @Try += 1;
    END
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    PRINT 'begin catch - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
    INSERT INTO @error(m,d) 
    VALUES ('pk violation!', sysutcdatetime());
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH

INSERT ErrorLogTable(m,d) 
SELECT * 
  FROM @error;
END
GO

ただし、トランザクションがロールバックされると、トランザクション中に入力された番号は失われます。

また、経験から、INSERTブロック内からCATCHステートメントを発行するときに問題が発生することがわかりました。場合によっては、このINSERTをトランザクションでロールバックすることもできます。ただし、私の答えからわかるように、テーブル変数を宣言し、それにカスタム状態を挿入し、トランザクションを正しく処理した後でログを実行するという回避策があります。テーブル変数に挿入されたデータは、トランザクションがコミット/ロールバックされた後も引き続き使用できます。

これを使用して、入力されている数値をキャプチャし、トランザクションが主キー違反に遭遇したときに、次のように、追跡されたテーブルから失敗した数値までの数値を再挿入できます。

DECLARE @numbers TABLE (a INT);
DECLARE @hasError BIT = 0;
BEGIN TRAN;
BEGIN TRY
    WHILE @value < @max
    BEGIN
        INSERT ErrorTestTable VALUES (@value);
        INSERT @numbers VALUES (@value); -- this will not be reached if an error occurs...
        SET @value += 1;
    END
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
    SET @hasError = 1;
END CATCH

-- re-insert the numbers that were successful 
IF @hasError = 1
BEGIN
    INSERT ErrorTestTable
    SELECT a
      FROM @numbers;
END

後続の番号が入力されていることを確認する場合は、1つの番号が失敗した場合でも、トランザクションの外部にロジックを追加してそれを実行する必要があります。個人的には、手動ループの代わりにセットベースの操作を使用するようにストアドプロシージャを書き換え、次のように主キー違反を完全に回避しようとするロジックも含めます。

ALTER PROCEDURE [dbo].[ErrorTest] (
    @value INT OUTPUT,
    @max INT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @error TABLE  (m nvarchar(500), d datetime2(7));

BEGIN TRAN;
BEGIN TRY
WITH numbers
AS
(
    SELECT ROW_NUMBER() OVER (ORDER BY o.object_id) AS RN
      FROM sys.objects AS o
CROSS JOIN sys.objects AS p
)
INSERT ErrorTestTable
SELECT RN
  FROM numbers
 WHERE rn between @value and @max
   AND NOT EXISTS (SELECT * FROM ErrorTestTable WHERE a = RN);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
INSERT INTO @error(m,d) 
VALUES ('pk violation!', sysutcdatetime());
IF @@TRANCOUNT > 0
    ROLLBACK TRANSACTION;
THROW;
END CATCH

INSERT ErrorLogTable(m,d) 
SELECT * 
  FROM @error;
END
GO
3
Mr.Brownstone