SQL Server - вложенные транзакции в хранимой процедуре

Допустим, такова ситуация:

  [Stored Proc 1]
  BEGIN
     BEGIN TRANSACTION
       ...
            exec sp 2   
     COMMIT
  END

Теперь, если SP 2 - откатывается по какой-либо причине, SP 1 - фиксирует или откатывает или выдает исключение?

Благодарю.

7 ответов

Решение

В SQL Server нет автономных транзакций. Вы можете увидеть @@TRANCOUNT больше, чем 1, но откат влияет на все это.

РЕДАКТИРОВАТЬ попросил указать на документацию. Не знаю, в какой теме это задокументировано, но я могу показать это вам в действии.

USE tempdb;
GO

Внутренний процесс:

CREATE PROCEDURE dbo.sp2
    @trip BIT
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;

    PRINT @@TRANCOUNT;

    IF @trip = 1
    BEGIN
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
    END
    ELSE
    BEGIN   
        IF @@TRANCOUNT > 0
            COMMIT TRANSACTION;
    END

    PRINT @@TRANCOUNT;
END
GO

Внешний процесс:

CREATE PROCEDURE dbo.sp1
    @trip BIT
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;

    PRINT @@TRANCOUNT;

    BEGIN TRY
        EXEC dbo.sp2 @trip = @trip;
    END TRY
    BEGIN CATCH
        PRINT ERROR_MESSAGE();
    END CATCH

    PRINT @@TRANCOUNT;

    IF @@TRANCOUNT > 0
        COMMIT TRANSACTION;

    PRINT @@TRANCOUNT;
END
GO

Итак, теперь давайте назовем это и позволим всему зафиксировать:

EXEC dbo.sp1 @trip = 0;

Результаты:

1
2
1
1
0

Теперь давайте его назовем и откатим внутреннюю процедуру:

EXEC dbo.sp1 @trip = 1;

Результаты:

1
2
0 <- обратите внимание, что откат здесь откатил оба
Число транзакций после EXECUTE указывает на несовпадающее количество операторов BEGIN и COMMIT. Предыдущий счетчик = 1, текущий счетчик = 0.
0
0

Можно выполнить откат SP2 и не потерять работу, выполненную SP1. Но чтобы это произошло, вы должны написать свои хранимые процедуры, используя очень специфический шаблон, как описано в разделе Обработка исключений и вложенные транзакции:

create procedure [usp_my_procedure_name]
as
begin
    set nocount on;
    declare @trancount int;
    set @trancount = @@trancount;
    begin try
        if @trancount = 0
            begin transaction
        else
            save transaction usp_my_procedure_name;

        -- Do the actual work here

lbexit:
        if @trancount = 0   
            commit;
    end try
    begin catch
        declare @error int, @message varchar(4000), @xstate int;
        select @error = ERROR_NUMBER(), @message = ERROR_MESSAGE(), @xstate = XACT_STATE();
        if @xstate = -1
            rollback;
        if @xstate = 1 and @trancount = 0
            rollback
        if @xstate = 1 and @trancount > 0
            rollback transaction usp_my_procedure_name;

        raiserror ('usp_my_procedure_name: %d: %s', 16, 1, @error, @message) ;
    end catch   
end

Не все ошибки могут быть исправлены, существует ряд состояний ошибок, из которых транзакция не может быть восстановлена, наиболее очевидным примером является взаимоблокировка (вы получаете уведомление об исключении взаимоблокировки после того, как транзакция уже откатилась). И SP1, и SP@ должны быть написаны с использованием этого шаблона. Если у вас мошеннический SP, или вы хотите просто использовать существующие хранимые процедуры, которые невольно выдают ROLLBACK Заявления тогда ваше дело потеряно.

Если SP2 откатит транзакцию, SP1 также откатится.

См. http://msdn.microsoft.com/en-US/library/ms187844(v=sql.105).aspx для получения подробной информации.

Во вложенных транзакциях, если откатывается какой-либо из внутренних переходов, выполняется откат всех его внешних транзакций.

Каждая хранимая процедура должна заканчиваться тем же счетчиком транзакций, с которым она была введена. Если количество не совпадает, SQL Server выдает ошибку 266: «Счетчик транзакций после EXECUTE указывает на отсутствие инструкции COMMIT или ROLLBACK TRANSACTION».
Если хранимая процедура не инициирует самую внешнюю транзакцию, она не должна выполнять ROLLBACK.

Если вложенная процедура начинает новую транзакцию; но если он обнаруживает необходимость отката, а значение @@TRANSACTION больше 1, он вызывает ошибку, возвращает сообщение об ошибке вызывающей стороне через выходной параметр или возвращаемое значение и выдает COMMIT вместо ROLLBACK.

      CREATE PROCEDURE [dbo].[Pinner]
    -- Add the parameters for the stored procedure here
    @ErrorMessage   varchar(max) out
    AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    -- Insert statements for procedure here

    begin tran
    begin try
        throw 51000, 'error occured', 1

        commit tran
        set @ErrorMessage = ''
    end try
    begin catch
            set @ErrorMessage = ERROR_MESSAGE();
            if @@TRANCOUNT = 1 
                rollback tran
            if @@TRANCOUNT > 1 
                commit tran
    end catch
END

      create PROCEDURE [dbo].[Pouter] 
    -- Add the parameters for the stored procedure here
    @ErrorMessage   varchar(max) out
    AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    -- Insert statements for procedure here
    begin tran
    begin try


        EXECUTE [dbo].[Pinner] 
           @ErrorMessage OUTPUT
        
        if @ErrorMessage <> '' begin 
            throw 51000, @ErrorMessage, 1
        end

        commit tran
        set @ErrorMessage = ''
    end try
    begin catch
            set @ErrorMessage = ERROR_MESSAGE();

            if @@TRANCOUNT = 1 
                rollback tran
            if @@TRANCOUNT > 1 
                commit tran
    end catch
END

      DECLARE @ErrorMessage varchar(max)

EXEC    [dbo].[Pouter]
        @ErrorMessage = @ErrorMessage OUTPUT

SELECT  @ErrorMessage as N'@ErrorMessage'

https://www.codemag.com/article/0305111/handling-sql-server-errors-in-nested-procedures

Вот быстрый и грязный способ вложения транзакций в хранимые процедуры (используя код из ответа Аарона), который иногда может быть полезен. Он использует параметр по умолчанию, чтобы указать внутренней процедуре, является ли он вложенным вызовом, и возвращает результат успеха / неудачи внешней процедуре.

CREATE PROCEDURE dbo.sp2
    @trip BIT,
    @nested BIT = 0
AS
BEGIN
    SET NOCOUNT, XACT_ABORT ON

    IF @nested = 0 BEGIN TRAN

    PRINT @@TRANCOUNT

    IF @trip = 1
    BEGIN
        IF @nested = 0 ROLLBACK
        RETURN 1
    END
    ELSE
    BEGIN   
        IF @nested = 0 COMMIT
    END

    PRINT @@TRANCOUNT
    RETURN 0
END
GO

Внешняя процедура проверяет успех / неудачу, а при необходимости откатывает транзакцию.

CREATE PROCEDURE dbo.sp1
    @trip BIT
AS
BEGIN
    DECLARE @result INT
    SET NOCOUNT, XACT_ABORT ON

    BEGIN TRAN

    PRINT @@TRANCOUNT

    BEGIN TRY
        EXEC @result = dbo.sp2 @trip = @trip, @nested = 1
        IF @result <> 0
        BEGIN
            ROLLBACK
            RETURN 1
        END
    END TRY
    BEGIN CATCH
        PRINT ERROR_MESSAGE()
    END CATCH

    PRINT @@TRANCOUNT

    COMMIT

    PRINT @@TRANCOUNT
    RETURN 0
END
GO
USE [DemoProject]
GO

/****** Object:  StoredProcedure [dbo].[Customers_CRUD]    Script Date: 11-Jan-17 2:57:38 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[Customers_CRUD]
      @Action VARCHAR(10)
      ,@BId INT = NULL
      ,@Username VARCHAR(50) = NULL
      ,@Provincename VARCHAR(50) = NULL
      ,@Cityname VARCHAR(50) = NULL
      ,@Number VARCHAR(50) = NULL
      ,@Name VARCHAR(50) = NULL
      ,@ContentType VARCHAR(50) = NULL
      ,@Data VARBINARY(MAX) = NULL

AS
BEGIN
      SET NOCOUNT ON;

      --SELECT
    IF @Action = 'SELECT'
      BEGIN
            SELECT BId , Username,Provincename,Cityname,Number,Name,ContentType, Data
            FROM tblbooking
      END

      --INSERT
    IF @Action = 'INSERT'
      BEGIN
            INSERT INTO tblbooking(Username,Provincename,Cityname,Number,Name,ContentType, Data)
            VALUES (@Username ,@Provincename ,@Cityname ,@Number ,@Name ,@ContentType ,@Data)
      END

      --UPDATE
    IF @Action = 'UPDATE'
      BEGIN
            UPDATE tblbooking
            SET Username = @Username,Provincename = @Provincename,Cityname = @Cityname,Number = @Number,Name = @Name,ContentType = @ContentType,Data = @Data
            WHERE BId = @BId
      END

      --DELETE
    IF @Action = 'DELETE'
      BEGIN
            DELETE FROM tblbooking
            WHERE BId = @BId
      END
END

GO
Другие вопросы по тегам