Вложенные хранимые процедуры, содержащие шаблон TRY CATCH ROLLBACK? - PullRequest
53 голосов
/ 15 января 2010

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

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 будет откат, и наконец, что ошибка будет зарегистрирована? Или что мне нужно изменить в моем шаблоне, чтобы гарантировать, что вся операция является атомарной, при этом позволяя дочерним деталям использоваться индивидуально (то есть подпроцедуры должны иметь такую ​​же атомную защиту)

Ответы [ 5 ]

105 голосов
/ 15 января 2010

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

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

Пояснения:

  • все начало 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)

Использование:

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

Резюме

  • Так что не создавайте больше 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 имеет подобную оболочку , которая использует точки сохранения. Я предпочитаю атомарный вызов БД и не использую частичные обновления как их статья

10 голосов
/ 15 января 2010

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

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

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

1 голос
/ 29 апреля 2013

Чтобы решить проблему с возвратом номера ошибки и номера строки, упомянутых @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)
0 голосов
/ 23 февраля 2017

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

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

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

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

0 голосов
/ 03 октября 2013

- Приведенный выше метод @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
...