Transacción en un procedimiento almacenado

12

Necesito realizar una ACTUALIZACIÓN e INSERTAR en una sola transacción. Ese código funciona bien por sí solo, pero me gustaría poder llamarlo fácilmente y pasar los parámetros requeridos. Cuando intento anidar esta transacción en un procedimiento almacenado, me encuentro con muchos errores de sintaxis.

¿Cómo puedo encapsular el siguiente código para que pueda llamarse fácilmente?

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
fuente
1
Probablemente ayudaría si agregara detalles en su pregunta sobre cuáles son exactamente los "errores" (use el enlace de edición debajo de su pregunta). Además, por favor haga el recorrido . ¡Gracias!
Max Vernon

Respuestas:

15

Le gusta la necesidad de ajustar ese código en CREATE PROCEDURE ...sintaxis y eliminar las GOdeclaraciones después BEGIN TRANSACTIONy antes 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

También tenga en cuenta que he agregado un TRY...CATCHbloque de declaración para permitir realizar una ROLLBACK TRANSACTIONdeclaración en caso de que ocurra algún error. Probablemente necesite un mejor manejo de errores que eso, pero sin conocer sus requisitos, eso es difícil en el mejor de los casos.

Algunas buenas lecturas:

  1. Especifique siempre el esquema

  2. Procedimiento almacenado Mejores prácticas

  3. Malos hábitos para evitar

Max Vernon
fuente
1
Todavía desea tener una transacción guardada. Si coloca una transacción en un SP y el SP se envuelve en otra transacción, las cosas fallarán. sqlstudies.com/2014/01/06/…
Kenneth Fisher
gracias por señalarme eso Sé que no hay transacciones anidadas en SQL Server, sin embargo, no estaba al tanto de las SAVE TRANSimplicaciones del comando.
Max Vernon
8

Si desea manejar adecuadamente los procedimientos almacenados anidados que pueden manejar transacciones (ya sea que se inicie desde T-SQL o el código de la aplicación), debe seguir la plantilla que describí en la siguiente respuesta:

¿Estamos obligados a manejar la transacción en código C # así como en el procedimiento almacenado

Notarás dos diferencias de lo que estás intentando aquí:

  1. El uso de RAISERRORdentro del CATCHbloque. Esto burbujea el error hasta el nivel de llamada (ya sea en la base de datos o en la capa de aplicación), por lo que se puede tomar una decisión con respecto al hecho de que ocurrió un error.

  2. No se SAVE TRANSACTION. Nunca he encontrado un caso para usar esto. Sé que algunas personas lo prefieren, pero en todo lo que he hecho en cualquier lugar en el que he trabajado, la noción de que ocurra un error dentro de cualquiera de los niveles anidados implica que cualquier trabajo que ya se haya realizado no es válido. Al usarlo SAVE TRANSACTION, solo está volviendo al estado justo antes de que se llame a este Procedimiento almacenado, dejando el proceso existente como válido.

    Si desea obtener más detalles SAVE TRANSACTION, eche un vistazo a la información en esta respuesta:

    Cómo deshacer cuando se inician 3 procedimientos almacenados desde un procedimiento almacenado

    Otro problema con SAVE TRANSACTIONes un matiz de su comportamiento, como se señala en la página de MSDN para GUARDAR TRANSACCIÓN (énfasis agregado):

    Se permiten nombres duplicados de puntos de guardado en una transacción, pero una instrucción ROLLBACK TRANSACTION que especifica el nombre del punto de guardado solo revertirá la transacción a la SAVE TRANSACTION más reciente usando ese nombre.

    Es decir, debe tener mucho cuidado de dar a cada Punto de guardado en cada Procedimiento almacenado un nombre único en todos los Puntos de guardado en todos los Procedimientos almacenados. Los siguientes ejemplos ilustran este punto.

    Este primer ejemplo muestra lo que sucede cuando reutiliza el nombre del punto de guardado; solo se revierte el punto de guardado del nivel más bajo.

    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 ejemplo muestra lo que sucede cuando usa nombres únicos de puntos de guardado; el punto de guardado del nivel deseado se revierte.

    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
fuente
Es por eso que uso @@ NESTLEVEL para fabricar mi nombre de punto de guardado SAVE TRANSACTION.
Vincent Vancalbergh