TSQL 在事务中尝试/捕获,反之亦然?

Posted

技术标签:

【中文标题】TSQL 在事务中尝试/捕获,反之亦然?【英文标题】:TSQL Try / Catch within Transaction or vice versa? 【发布时间】:2014-05-28 05:29:30 【问题描述】:

我正在编写一个脚本,该脚本将从多个表中删除记录,但在删除之前它必须返回一个计数以供用户在提交前确认。

这是脚本的摘要。

BEGIN TRANSACTION SCHEDULEDELETE
    BEGIN TRY
        DELETE   -- delete commands full SQL cut out
        DELETE   -- delete commands full SQL cut out
        DELETE   -- delete commands full SQL cut out
        PRINT 'X rows deleted. Please commit or rollback.' --calculation cut out.
    END TRY
    BEGIN CATCH 
        SELECT
            ERROR_NUMBER() AS ErrorNumber,
            ERROR_SEVERITY() AS ErrorSeverity,
            ERROR_STATE() AS ErrorState,
            ERROR_PROCEDURE() AS ErrorProcedure,
            ERROR_LINE() AS ErrorLine,
            ERROR_MESSAGE() AS ErrorMessage

            ROLLBACK TRANSACTION SCHEDULEDELETE
            PRINT 'Error detected, all changes reversed.'
    END CATCH

--COMMIT TRANSACTION SCHEDULEDELETE --Run this if count correct.

--ROLLBACK TRANSACTION SCHEDULEDELETE --Run this if there is any doubt whatsoever.

这是我第一次编写事务,将 TRY/CATCH 块放在事务中是否正确/最佳做法,还是应该将事务放在 TRY 块中?

此脚本中的重要因素是用户必须手动提交事务。

【问题讨论】:

是的,在 try/catch 块之外。 永远不要等待最终用户提交事务,除非它是单用户模式数据库。 @dean 有问题吗?基本上,用户将被告知他们应该期望删除 X 条记录,如果数量匹配,则立即提交。事务运行时不会写入这些表(尽管其他表会写入)。 我赞同@dean 所说的话。除非您的用户在这类事情上有一定的经验,否则您将陷入困境。即便如此,用户错误迟早会发生。如果可能的话,最好只填补您拥有的任何信息空白,从而防止您首先创建可靠的脚本。如果绝对有必要在提交之前确认结果,那么我建议默认情况下将脚本置于回滚状态。但即使这样也不是很好的做法。 如果他们事先知道受影响的记录数应该是多少,那么最好在匹配时使用参数提交(并且没有发现错误),并在不匹配时使用原因和输出回滚。从长远来看,最好为用户错误和临时疏忽留出尽可能少的空间,您可以更好地自动化这些事情,它们往往越可靠。 【参考方案1】:

除了上述 M.Ali 和 Dean 的好建议之外,对于那些希望在 SQL SERVER 中使用新的(er)TRY CATCH THROW 范式的人提供一点帮助:

(我找不到完整的语法,所以在这里添加它)

要点:HERE

此处的示例存储过程代码(来自我的要点):

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
 
CREATE PROC [dbo].[pr_ins_test]
@CompanyID INT
AS
 
SET NOCOUNT ON
 
BEGIN
 
    DECLARE @PreviousConfigID INT
    
    BEGIN TRY
        BEGIN TRANSACTION MYTRAN; -- Give the transaction a name
        SELECT 1/0  -- Generates divide by zero error causing control to jump into catch
 
        PRINT '>> COMMITING'
        COMMIT TRANSACTION MYTRAN;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
        BEGIN 
            PRINT '>> ROLLING BACK'
            ROLLBACK TRANSACTION MYTRAN; -- The semi-colon is required (at least in SQL 2012)
            
            
        END; -- I had to put a semicolon to avoid error near THROW
        THROW
    END CATCH
END

【讨论】:

只是一个观察 - THROW 应该在 BEGIN/END 之外,因此无论事务是否打开,它都会抛出。 @MarkG 不确定我是否理解,如果 THROW 在 BEGIN/END Catch 之外,则执行此存储过程总是会引发异常,除非您在 commit tran 之后使用 return 语句。就像现在的摘录一样,只是将球移到外面是没有用的 我怀疑@MarkG 是说 THROW 应该在 END 之后但在 END CATCH 之前。这将确保无论@@TRANCOUNT 是什么都抛出异常。 我尝试在 Visual Studio 的数据库项目中添加 THROW 语句,但出现错误。 我处理与THROW 相关的分号的方式是我只写;THROW;——不管怎样。我只是喜欢这样做,所以我不会忘记添加它。【参考方案2】:

只有在您位于 TRY 块内且就在实际语句之前,才打开事务,并立即提交。不要等待您的控件进入批处理的末尾来提交您的事务。

如果您在TRY 块中出现问题并且您已经打开了事务,则控件将跳转到CATCH 块。只需在此处回滚您的事务并根据需要进行其他错误处理。

在实际回滚事务之前,我使用@@TRANCOUNT 函数对任何打开的事务添加了一点检查。在这种情况下,它真的没有多大意义。当您在打开事务之前在 TRY 块中进行一些验证检查时,它会更有用,例如检查参数值和其他内容,如果任何验证检查失败,则会在 TRY 块中引发错误。在这种情况下,控件将跳转到CATCH 块,甚至无需打开事务。在那里,您可以检查任何打开的事务,如果有任何打开的事务则回滚。在您的情况下,您确实不需要检查任何未结交易,因为除非您的交易出现问题,否则您不会进入CATCH 块。

在你执行完DELETE操作后不要询问它是否需要提交或回滚;在打开交易之前进行所有这些验证。事务打开后,立即提交,如果出现任何错误,请进行错误处理(通过使用几乎所有错误函数获取详细信息,您做得很好)。

BEGIN TRY

  BEGIN TRANSACTION SCHEDULEDELETE
    DELETE   -- delete commands full SQL cut out
    DELETE   -- delete commands full SQL cut out
    DELETE   -- delete commands full SQL cut out
 COMMIT TRANSACTION SCHEDULEDELETE
    PRINT 'X rows deleted. Operation Successful Tara.' --calculation cut out.
END TRY

BEGIN CATCH 
  IF (@@TRANCOUNT > 0)
   BEGIN
      ROLLBACK TRANSACTION SCHEDULEDELETE
      PRINT 'Error detected, all changes reversed'
   END 
    SELECT
        ERROR_NUMBER() AS ErrorNumber,
        ERROR_SEVERITY() AS ErrorSeverity,
        ERROR_STATE() AS ErrorState,
        ERROR_PROCEDURE() AS ErrorProcedure,
        ERROR_LINE() AS ErrorLine,
        ERROR_MESSAGE() AS ErrorMessage
END CATCH

【讨论】:

我知道 SQL Server 支持事务已经有几十年了,但 TRY/CATCH 是最近才添加的 - 那么在将 TRY/CATCH 添加到 SQL Server 之前应该如何实现 BEGIN TRANSACTION + COMMIT + ROLLBACK 呢? 完全没有理由在TRY 块中打开交易。事实上,在TRY 之前立即打开它是正确的结构。 @Dai TRY/CATCH 自 2005 年以来一直在 SQL Server 中,而在 SQL Server 2005(即 SQL Sever 2000 和更早版本)之前,有很多我们不能做的事情,或者非常复杂做最简单的事情的方法。所以我希望你不在 SQL Server 2000 上:)。让您知道,在 2005 年之前,我们在每个语句后使用 @@Error 函数来检查语句是否出错。 @Ant_222 你想扩展你的要求吗?我已经详细说明了我提出的主张的原因。您刚刚发表了声明,但没有提供任何理由说明您建议的方法是正确的方法。 BEGIN TRAN 保留在外部 TRY 块的原因很简单:如果BEGIN TRAN 失败,则不需要ROLLBACK。现在,我想听听您将BEGIN TRAN inside 放在TRY 块中的原因(我在您的回答中没有看到)。 BEGIN TRAN 外面可能有什么问题?【参考方案3】:

永远不要等待最终用户提交事务,除非它是单用户模式数据库。

简而言之,它是关于阻塞的。您的事务将对正在更新的资源采取一些排他锁,并将持有这些锁直到事务结束(提交或回滚)。没有人能够触摸这些行。如果将快照隔离与版本存储清理一起使用,则会出现一些不同的问题。

最好先发出一个选择查询来确定一些符合条件的行,将其呈现给最终用户,然后在他确认后执行实际删除。

【讨论】:

这是正确的解决方案。但请务必记住符合删除条件的行并尽可能删除它们,而不是在用户确认操作时发生任何事情。

以上是关于TSQL 在事务中尝试/捕获,反之亦然?的主要内容,如果未能解决你的问题,请参考以下文章

尝试 开始事务,保存更改 捕获 回滚,开始另一个事务,保存更改 EF 核心

TSQL / SQL-SERVER:如何在具有主键的快照复制中查找所有表

如何在 NestJs 中捕获 Typeorm 事务错误

Postgresql 捕获事务错误和回滚

TSQL:触发/回滚事务错误

未捕获的 Promise 错误:TypeError:成员不是函数