web-dev-qa-db-ja.com

ストアドプロシージャでのトランザクション

単一のトランザクションでUPDATEとINSERTを実行する必要があります。そのコードはそれ自体で問題なく機能しますが、簡単に呼び出して必要なパラメーターを渡すことができるようにしたいと思います。このトランザクションをストアドプロシージャにネストしようとすると、多くの構文エラーが発生します。

次のコードをカプセル化して簡単に呼び出せるようにするにはどうすればよいですか?

BEGIN TRANSACTION AssignUserToTicket
GO

DECLARE @updateAuthor varchar(100)
DECLARE @assignedUser varchar(100)
DECLARE @ticketID bigint

SET @updateAuthor = 'user1'
SET @assignedUser = 'user2'
SET @ticketID = 123456

    UPDATE tblTicket SET ticketAssignedUserSamAccountName = @assignedUser WHERE (ticketID = @ticketID);
    INSERT INTO [dbo].[tblTicketUpdate]
           ([ticketID]
           ,[updateDetail]
           ,[updateDateTime]
           ,[userSamAccountName]
           ,[activity])
     VALUES
           (@ticketID,
           'Assigned ticket to ' + @assignedUser,
           GetDate(),
           @updateAuthor,
           'Assign');
GO
COMMIT TRANSACTION AssignUserToTicket
12
Charlie K

そのコードをCREATE PROCEDURE ...構文でラップし、BEGIN TRANSACTIONの後とCOMMIT TRANSACTIONの前のGOステートメントを削除する必要があります。

GO
CREATE PROCEDURE dbo.AssignUserToTicket
(
     @updateAuthor varchar(100)
    , @assignedUser varchar(100)
    , @ticketID bigint
)
AS
BEGIN
    BEGIN TRANSACTION;
    SAVE TRANSACTION MySavePoint;
    SET @updateAuthor = 'user1';
    SET @assignedUser = 'user2';
    SET @ticketID = 123456;

    BEGIN TRY
        UPDATE dbo.tblTicket 
        SET ticketAssignedUserSamAccountName = @assignedUser 
        WHERE (ticketID = @ticketID);

        INSERT INTO [dbo].[tblTicketUpdate]
            (
            [ticketID]
            ,[updateDetail]
            ,[updateDateTime]
            ,[userSamAccountName]
            ,[activity]
            )
        VALUES (
            @ticketID
            , 'Assigned ticket to ' + @assignedUser
            , GetDate()
            , @updateAuthor
            , 'Assign'
            );
        COMMIT TRANSACTION 
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
        BEGIN
            ROLLBACK TRANSACTION MySavePoint; -- rollback to MySavePoint
        END
    END CATCH
END;
GO

また、エラーが発生した場合にTRY...CATCHステートメントを実行できるように、ROLLBACK TRANSACTIONステートメントブロックを追加しました。おそらくそれよりも優れたエラー処理が必要ですが、要件の知識がないと、それは最高の状態で困難です。

いくつかの良い読書:

  1. 常にスキーマを指定

  2. ストアドプロシージャのベストプラクティス

  3. 避けるべき悪い習慣

15
Max Vernon

トランザクションを処理できるネストされたストアドプロシージャを適切に処理する場合(T-SQLから開始したか、アプリコードから開始したかに関係なく)、次の回答で説明したテンプレートに従う必要があります。

C#コードおよびストアドプロシージャでトランザクションを処理する必要があります

ここで試みているものとは2つの違いがあることに気づくでしょう。

  1. RAISERRORブロック内でのCATCHの使用。これにより、エラーが呼び出しレベル(DBまたはアプリレイヤーのいずれか)までバブリングされるため、エラーが発生したという事実に関して決定を下すことができます。

  2. いいえSAVE TRANSACTION。私はこれを使用するためのケースを見つけたことがありません。私は一部の人々がそれを好むことを知っていますが、私が働いたことのある場所でこれまでに行ったことすべてにおいて、ネストされたレベルのいずれかでエラーが発生するという概念は、すでに行われた作業が無効であることを意味しました。 SAVE TRANSACTIONを使用すると、このストアドプロシージャが呼び出される直前の状態に戻るだけで、既存のプロセスは他の方法で有効なままになります。

    SAVE TRANSACTIONの詳細が必要な場合は、この回答の情報をご覧ください。

    1つのストアドプロシージャから3つのストアドプロシージャが開始されたときにロールバックする方法

    SAVE TRANSACTIONのもう1つの問題は、MSDNページの SAVE TRANSACTION (強調を追加)に記載されているように、その動作のニュアンスです。

    トランザクションでは重複するセーブポイント名を使用できますが、セーブポイント名を指定するROLLBACK TRANSACTIONステートメントは、トランザクションを最新SAVE TRANSACTIONにロールバックするだけですその名前。

    つまり、各ストアドプロシージャの各セーブポイントに、すべてのストアドプロシージャのすべてのセーブポイントで一意の名前を付けるように注意する必要があります。次の例は、この点を示しています。

    この最初の例は、セーブポイント名を再利用するとどうなるかを示しています。最下位レベルのセーブポイントのみがロールバックされます。

    IF (OBJECT_ID(N'tempdb..#SaveTranTestA') IS NOT NULL)
    BEGIN
        DROP TABLE #SaveTranTestA;
    END;
    CREATE TABLE #SaveTranTestA (SomeVal INT NOT NULL);
    
    BEGIN TRAN; -- start level 1
    SAVE TRANSACTION MySavePoint;
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 1
    
    INSERT INTO #SaveTranTestA (SomeVal) VALUES (100);
    
    BEGIN TRAN; -- start level 2
    SAVE TRANSACTION MySavePoint;
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 2
    
    INSERT INTO #SaveTranTestA (SomeVal) VALUES (200);
    
    COMMIT; -- exit level 2
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 1
    SELECT * FROM #SaveTranTestA;
    -- 100
    -- 200
    
    ROLLBACK TRANSACTION MySavePoint; -- error occurred; undo actions up to this point
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 1
    SELECT * FROM #SaveTranTestA;
    -- 100
    
    COMMIT; -- exit level 1
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 0
    SELECT * FROM #SaveTranTestA;
    -- 100
    

    この2番目の例は、一意のセーブポイント名を使用するとどうなるかを示しています。目的のレベルのセーブポイントがロールバックされます。

    IF (OBJECT_ID(N'tempdb..#SaveTranTestB') IS NOT NULL)
    BEGIN
        DROP TABLE #SaveTranTestB;
    END;
    CREATE TABLE #SaveTranTestB (SomeVal INT NOT NULL);
    
    BEGIN TRAN; -- start level 1
    SAVE TRANSACTION MySavePointUno;
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 1
    
    INSERT INTO #SaveTranTestB (SomeVal) VALUES (100);
    
    BEGIN TRAN; -- start level 2
    SAVE TRANSACTION MySavePointDos;
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 2
    
    INSERT INTO #SaveTranTestB (SomeVal) VALUES (200);
    
    COMMIT; -- exit level 2
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 1
    SELECT * FROM #SaveTranTestB;
    -- 100
    -- 200
    
    ROLLBACK TRANSACTION MySavePointUno; --error occurred; undo actions up to this point
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 1
    SELECT * FROM #SaveTranTestB;
    -- <no rows>
    
    COMMIT; -- exit level 1
    
    SELECT @@TRANCOUNT AS [TranCount]; -- 0
    SELECT * FROM #SaveTranTestB;
    -- <no rows>
    
8
Solomon Rutzky