Transação em um procedimento armazenado

12

Preciso executar um UPDATE e um INSERT em uma única transação. Esse código funciona bem por si só, mas eu gostaria de poder chamá-lo facilmente e passar os parâmetros necessários. Quando tento aninhar esta transação em um procedimento armazenado, encontro muitos erros de sintaxe.

Como posso encapsular o código a seguir para que ele possa ser chamado facilmente?

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
Charlie K
fonte
1
Provavelmente, ajudaria se você adicionasse detalhes à sua pergunta sobre quais são exatamente os "erros" (use o link de edição abaixo da sua pergunta). Além disso, faça o passeio . Obrigado!
Max Vernon

Respostas:

15

Você precisa envolver esse código na CREATE PROCEDURE ...sintaxe e remover as GOinstruções BEGIN TRANSACTIONantes e depois COMMIT TRANSACTION.

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

Observe também que adicionei um TRY...CATCHbloco de instrução para permitir a execução de uma ROLLBACK TRANSACTIONinstrução caso ocorra algum erro. Você provavelmente precisa de um tratamento de erros melhor que isso, mas sem o conhecimento de seus requisitos, isso é difícil na melhor das hipóteses.

Boa leitura:

  1. Sempre especifique o esquema

  2. Práticas recomendadas para procedimentos armazenados

  3. Maus hábitos a evitar

Max Vernon
fonte
1
Você ainda deseja ter uma transação salva. Se você colocar uma transação em um SP e o SP for envolvido em outra transação, as coisas falharão. sqlstudies.com/2014/01/06/…
Kenneth Fisher
obrigado por me apontar isso. Eu sei que não há transações aninhadas no SQL Server, no entanto, eu não estava ciente das SAVE TRANSimplicações do comando.
Max Vernon
8

Se você deseja manipular adequadamente os procedimentos armazenados aninhados que podem manipular transações (iniciadas no T-SQL ou no código do aplicativo), siga o modelo que descrevi na resposta a seguir:

Somos obrigados a manipular transações no código C #, bem como no procedimento armazenado

Você notará duas diferenças no que está tentando aqui:

  1. O uso de RAISERRORdentro do CATCHbloco. Isso corrige o erro até o nível de chamada (seja na camada de banco de dados ou de aplicativo), para que uma decisão possa ser tomada com relação ao fato de que ocorreu um erro.

  2. Não SAVE TRANSACTION. Eu nunca encontrei um caso para usar isso. Sei que algumas pessoas preferem isso, mas em tudo o que já fiz em qualquer lugar em que trabalhei, a noção de um erro ocorrendo em qualquer um dos níveis aninhados implicava que qualquer trabalho já realizado era inválido. Ao usar, SAVE TRANSACTIONvocê está apenas voltando ao estado imediatamente antes da chamada deste Procedimento Armazenado, deixando o processo existente como válido.

    Se você quiser obter mais detalhes SAVE TRANSACTION, consulte as informações nesta resposta:

    Como reverter quando três procedimentos armazenados são iniciados em um procedimento armazenado

    Outro problema SAVE TRANSACTIONé a nuance de seu comportamento, conforme observado na página do MSDN para SAVE TRANSACTION (ênfase adicionada):

    Nomes de ponto de salvamento duplicados são permitidos em uma transação, mas uma instrução ROLLBACK TRANSACTION que especifica o nome do ponto de salvamento reverterá a transação apenas de volta para a SAVE TRANSACTION mais recente usando esse nome.

    Ou seja, você precisa ter muito cuidado para atribuir a cada ponto de salvamento em cada procedimento armazenado um nome exclusivo em todos os pontos de salvamento em todos os procedimentos armazenados. Os exemplos a seguir ilustram esse ponto.

    Este primeiro exemplo mostra o que acontece quando você reutiliza o nome do Save Point; somente o Save Point de nível mais baixo é revertido.

    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

    Este segundo exemplo mostra o que acontece quando você usa nomes exclusivos de Save Point; o ponto de salvamento do nível desejado é revertido.

    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>
Solomon Rutzky
fonte
É por isso que eu uso @@ NESTLEVEL para fabricar meu nome de ponto de salvamento SAVE TRANSACTION.
Vincent Vancalbergh