Вложенные хранимые процедуры, содержащие шаблон TRY CATCH ROLLBACK?

Меня интересуют побочные эффекты и потенциальные проблемы следующего шаблона:

CREATE PROCEDURE [Name]
AS
BEGIN
    BEGIN TRANSACTION
    BEGIN TRY
        [...Perform work, call nested procedures...]
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION
        RAISERROR [rethrow caught error using @ErrorNumber, @ErrorMessage, etc]
    END CATCH
END

Насколько я понимаю, этот шаблон звучит при использовании с одной процедурой - процедура либо выполнит все свои инструкции без ошибок, либо откатит все действия и сообщит об ошибке.

Однако, когда одна хранимая процедура вызывает другую хранимую процедуру для выполнения некоторой части работы (при том понимании, что меньшая процедура иногда называется сама по себе), я вижу проблему, связанную с откатами - информационное сообщение ( Уровень 16) выдается с сообщением The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.. Это я предполагаю, потому что откат в подпроцедуре всегда откатывает внешнюю транзакцию, а не только транзакцию, запущенную в подпроцедуре.

Я хочу, чтобы все было отброшено назад и прервано, если возникла какая-либо ошибка (и сообщение об ошибке было сообщено клиенту как ошибка SQL), я просто не уверен в всех побочных эффектах, которые возникают из внешних слоев, пытающихся откат транзакции, которая уже откатна. Возможно, проверка @@TRANCOUNT перед выполнением отката на каждом уровне TRY CATCH?

Наконец, есть клиентский конец (Linq2SQL), у которого есть собственный уровень транзакции:

try
{
    var context = new MyDataContext();
    using (var transaction = new TransactionScope())
    {       
            // Some Linq stuff
        context.SubmitChanges();
        context.MyStoredProcedure();
        transactionComplete();
    }
}
catch
{
    // An error occured!
}

В случае, если хранимая процедура, "MySubProcedure", вызванная внутри MyStoredProcedure, вызывает ошибку, могу ли я быть уверен, что все, что было сделано ранее в MyStoredProcedure, будет отменено, все операции Linq, сделанные SubmitChanges, будут отброшены назад и наконец, что ошибка будет зарегистрирована? Или что мне нужно изменить в моем шаблоне, чтобы гарантировать, что вся операция является атомарной, при этом все еще позволяя использовать дочерние части отдельно (т.е. Подпроцедуры должны иметь одинаковую атомную защиту).

Ответ 1

Это наш шаблон (удаление ошибок)

Это предназначено для обработки

Пояснения:

  • все начало TXN и фиксация/откаты должны быть спарены так, что @@TRANCOUNT будет одинаковым при входе и выходе

  • Несоответствия @@TRANCOUNT вызывают ошибку 266, потому что

    • BEGIN TRAN увеличивает @@TRANCOUNT

    • COMMIT декременты @@TRANCOUNT

    • ROLLBACK возвращает @@TRANCOUNT в ноль

  • Вы не можете уменьшить @@TRANCOUNT для текущей области
    Это то, что вы считаете "внутренней транзакцией"

  • SET XACT_ABORT ON подавляет ошибку 266, вызванную несоответствием @@TRANCOUNT
    А также касается таких вопросов, как "Тайм-аут транзакций SQL Server" на dba.se

  • Это позволяет использовать TXN на стороне клиента (например, LINQ) Одна хранимая процедура может быть частью распределенной или XA-транзакции или просто инициирована в клиентском коде (например .net TransactionScope)

Применение:

  • Каждый сохраненный proc должен соответствовать одному и тому же шаблону

Резюме

  • Поэтому не создавайте больше TXN, чем вам нужно

Код

CREATE PROCEDURE [Name]
AS
SET XACT_ABORT, NOCOUNT ON

DECLARE @starttrancount int

BEGIN TRY
    SELECT @starttrancount = @@TRANCOUNT

    IF @starttrancount = 0
        BEGIN TRANSACTION

       [...Perform work, call nested procedures...]

    IF @starttrancount = 0 
        COMMIT TRANSACTION
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0 AND @starttrancount = 0 
        ROLLBACK TRANSACTION;
    THROW;
    --before SQL Server 2012 use 
    --RAISERROR [rethrow caught error using @ErrorNumber, @ErrorMessage, etc]
END CATCH
GO

Примечания:

  • Проверка отката на самом деле избыточна из-за SET XACT_ABORT ON. Тем не менее, это заставляет меня чувствовать себя лучше, выглядит странно без, и позволяет ситуации, в которых вы не хотите этого.

  • Remus Rusanu имеет похожую оболочку, которая использует точки сохранения, Я предпочитаю атомный вызов БД и не использую частичные обновления, такие как их статья

Ответ 2

Я не парень Linq (и ни один из них не Erland), но он написал абсолютные библии при обработке ошибок. Вне осложнений Linq может добавить к вашей проблеме, на все ваши другие вопросы следует ответить здесь:

http://www.sommarskog.se/error_handling/Part1.html

(Старая ссылка: http://www.sommarskog.se/error_handling_2005.html)

Ответ 3

Чтобы решить вопрос о возврате номера ошибки и номера строки, упомянутого @AlexKuznetsov, можно поднять ошибку как таковую:

DECLARE @ErrorMessage NVARCHAR(4000)
DECLARE @ErrorSeverity INT
DECLARE @ErrorState INT
DECLARE @ErrorLine INT
DECLARE @ErrorNumber INT

SELECT @ErrorMessage = ERROR_MESSAGE(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE(),
@ErrorNumber = ERROR_NUMBER(),
@ErrorLine = ERROR_LINE()

RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber, @ErrorLine)

Ответ 4

- метод @Amanda выше не возвращает правильный номер ошибки

DECLARE  
  @ErrorMessage   nvarchar(4000),  
  @ErrorSeverity   int,  
  @ErrorState int,  
  @ErrorLine  int,  
  @ErrorNumber   int  

BEGIN TRY  
 SELECT 1/0; -- CATCH me  
END TRY  

BEGIN CATCH  

  DECLARE @err int = @@ERROR  

  PRINT @err           -- 8134, divide by zero  
  PRINT ERROR_NUMBER() -- 8134  

  SELECT  
    @ErrorMessage  = ERROR_MESSAGE(),  
    @ErrorSeverity = ERROR_SEVERITY(),  
    @ErrorState    = ERROR_STATE(),  
    @ErrorNumber   = ERROR_NUMBER(),  
    @ErrorLine     = ERROR_LINE()  

  -- error number = 50000 :(  
  RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber, @ErrorLine)  

END CATCH  

-- error number = 8134  
SELECT 1/0

Ответ 5

В случае отсутствия специальной обработки ошибок в CATCH, кроме повторной и сохраненной цепочек вызовов procs, не слишком долго, может быть целесообразно использовать такой простой шаблон:

create procedure someNestedSP
as
SET XACT_ABORT ON
begin transaction
-- do some work or call some other similar SP
commit transaction

Он также будет откатывать корневую транзакцию со всеми "вложенными" в случае какой-либо ошибки, но код короче и проще, чем решение @gbn. Тем не менее XACT_ABORT заботится о большинстве проблем, упомянутых там.

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