【问题标题】:How to THROW exception in inner transaction without ending SQL Server stored procedure execution?如何在不结束 SQL Server 存储过程执行的情况下在内部事务中抛出异常?
【发布时间】:2019-07-06 13:19:33
【问题描述】:

我的目标是向调用者抛出异常,但继续执行SQL Server 存储过程。所以,本质上,我想要完成的是try..catch..finally block,尽管据我所知,SQL Server 没有 try..catch..finally 块的概念。

我有一个示例stored procedure 来说明。这只是我想出的一个例子,所以请不要过多关注表模式。希望您了解我在这里尝试执行的操作的要点。无论如何,存储过程包含一个explicit transaction,它在catch block 中抛出一个exception。在 try..catch 块之后还有进一步的执行,但如果 THROW 被执行,它永远不会被执行。据我了解,至少在 SQL Server 中,THROW 无法区分内部事务和外部事务或嵌套事务。

在这个存储过程中,我有两个表:Tbl1Tbl2Tbl1Tbl1.ID 上有一个primary keyTbl2EmpFK 上有一个 foreign key,它映射到 Tbl1.IDEmpID 有一个唯一的约束。 Tbl1 中不能插入重复记录。 Tbl1Tbl2ID 上都有主键,并使用身份增量进行自动插入。存储过程有三个输入参数,其中一个是employeeID

在内部事务中,Tbl1 中插入了一条记录——添加了一个新的员工 ID。如果它失败了,这个想法是事务应该优雅地出错,但存储的过程应该仍然继续运行直到完成。无论表格插入成功与否,后面都会使用EmpID来填写EmpFk

在 try..catch 块之后,我通过传递到存储过程的 employeeID 参数执行 Tbl1.ID 查找。然后,我在 TBl2 中插入一条记录; Tbl1.IDTbl2.EmpFK 的值。

(您可能会问“为什么要使用这样的模式?为什么不将这么小的数据集合并到一个表中?”同样,这只是一个示例。它不一定是员工。你可以选择任何东西. 它只是一个小部件。想象一下 Tbl1 可能包含一个非常非常大的数据集。确定的是有两个表具有主键/外键关系。)

这是示例数据集:

Tbl1
ID EmpID
1  AAA123
2  AAB123
3  AAC123

Tbl2
ID Role        Location EmpFK
1  Junior      NW       1
2  Senior      NW       2
3  Manager     NE       2
4  Sr Manager  SE       3
5  Director    SW       3

这是示例存储过程:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE PROCEDURE [dbo].[usp_TestProc]

    @employeeID VARCHAR(10)
    ,@role VARCHAR(50)
    ,@location VARCHAR(50)

AS
BEGIN

    SET NOCOUNT ON;

    DECLARE @employeeFK INT;

    BEGIN TRY
        BEGIN TRANSACTION MYTRAN;

            INSERT [Tbl1] (
                [EmpID]
            )
            VALUES (
                @employeeID
            );

        COMMIT TRANSACTION MYTRAN;
    END TRY

    BEGIN CATCH

        IF @@TRANCOUNT > 0
        BEGIN

            ROLLBACK TRANSACTION MYTRAN;

        END;

        THROW; -- Raises exception, exiting stored procedure

    END CATCH;

    SELECT
        @employeeFK = [ID]
    FROM
        [Tbl1]
    WHERE
        [EmpID] = @employeeID;

    INSERT [Tbl2] (
        [Role]
        ,[Location]
        ,[EmpFK]
    )
    VALUES (
        @role
        ,@location
        ,@employeeFK
    );

END;

所以,我仍然想将错误返回给调用者,即记录错误,但我不希望它停止冷执行存储过程。它应该继续非常类似于 try..catch..finally 块。这可以通过THROW 完成吗?还是我必须使用其他方法?

也许我弄错了,但THROW 不是RAISERROR 的升级版,而且我们应该使用前者来处理异常?

我过去曾在这些情况下使用过RAISERROR,它非常适合我。但是THROW 是一个更简单、更优雅的解决方案,imo,并且可能是更好的实践。我不太确定。

提前感谢您的帮助。

【问题讨论】:

  • RAISERROR 没有被弃用,不,因此您可以使用它来实现此行为。 (DB<>Fiddle 实际上无法在此处复制行为,但是,如果您在自己的实例上使用此 SQL,您将看到返回 2 个结果集,一个没有行,一个有)
  • 这只是在尖叫成为xy problem
  • @Larnu。谢谢你。在我的 SQL Server 实例上,抛出异常并且第二次插入从未完成。据我了解,SQL Server 不会在事务之间解析抛出的异常。因此,THROW 退出存储过程。如果不是这样,我将回到绘图板,看看我是否可以在我的实例中复制 DB Fiddle。再次感谢您的帮助。
  • @SeanLange 我不太清楚你为什么这么说。这不是面试问题,但我经常看到这种时尚的面试问题。如果我在面试中被问到这个确切的问题,我会尽我所知回答这个问题。我无法回答这是一个 XY 问题。我必须解决它。如果问题被定义为如何在不停止执行的情况下抛出,那就是手头的任务,那么它就不是 XY 问题。
  • 您认为您可以向调用者抛出异常继续执行吗? try/catch/finally 通常不是这样定义的。

标签: sql-server exception stored-procedures transactions throw


【解决方案1】:

一成不变的是有两个表有一个主键 / 外键关系。

在内部事务中使用 THROW 不是做你想做的事的方法。从您的代码来看,您想要插入一个新员工,除非该员工已经存在,然后,无论该员工是否已经存在,您都希望在第二次插入子表中使用该员工的 PK/id。

一种方法是拆分逻辑。这是我的意思的伪代码:

IF NOT EXISTS(Select employee with @employeeId)
  INSERT the new employee

SELECT @employeeFK like you are doing.

INSERT into Table2 like you are doing.

如果在传递已经存在的@employeeId 时仍然需要引发错误,则可以在 IF 之后放置一个 ELSE,并填充一个字符串变量,并在 proc 结束时,如果填充了该变量,然后抛出/引发错误。

【讨论】:

  • 啊。好点子。谢谢你。而且我仍然可以记录异常,这是最理想的。我可以使用THROW 还是仅在它位于 catch 块内时使用?否则,我会坚持使用RAISERROR 来处理异常。再次感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-03-28
  • 1970-01-01
  • 2017-02-07
  • 2019-08-01
  • 2012-02-26
  • 2023-04-08
  • 2022-11-02
相关资源
最近更新 更多