当sql server触发器失败时,我们可以提交事务吗?

Pus*_*ode 11 trigger sql-server transaction

当表上的插入触发器失败时,我试图避免数据丢失。我正在使用以下场景尝试此操作,但我的代码失败了。

当在客户表上发生插入时,我想将相同的数据插入到存档表中。当触发器失败时,我不想回滚整个事务,这会导致客户表上的数据丢失。

即使触发器失败,我也希望将数据插入到Customers表中,并且存储过程应该像往常一样返回Customer_ID

ALTER TRIGGER [dbo].[Customer_Insert_Trigger_Test] 
   ON  [dbo].[Customers]
   AFTER INSERT
AS 
BEGIN

BEGIN TRY
    begin transaction;
    set nocount  on;
    SAVE TRANSACTION InsertSaveHere;
    --Simulating error situation
    RAISERROR (N'This is message %s %d.', -- Message text.
       11, -- Severity,
       1, -- State,
       N'number', -- First argument.
       5); -- Second argument.

   Insert into Archive select * from Inserted;
    commit transaction;

  END TRY

  BEGIN CATCH
    ROLLBACK TRANSACTION InsertSaveHere;
  END CATCH
END
Run Code Online (Sandbox Code Playgroud)

我的问题主要是关于如何避免在客户表上的实际插入以在触发器失败时回滚。如何更改我的代码?

Sol*_*zky 10

问题提到“代码失败”,但没有任何错误消息或具体失败的迹象。包括至少一个,如果不是两个,这些信息总是有助于获得更好的答案。

目前,我看到一些关于触发器和事务的假设似乎是不正确的:您@@TRANCOUNT通过调用增加 ,BEGIN TRAN;但只有@@TRANCOUNT在没有错误并且该COMMIT TRAN;行在TRY块内执行时才减少。在出错的情况下,将COMMIT跳过 并ROLLBACK出现保存点的a 。但是回滚保存点不会减少@@TRANCOUNT在这种情况下INSERT操作结束并且事务仍然处于活动状态。

触发器存在于内部启动的事务中,该事务将其绑定到触发触发器的 DML 操作。这是您能够ROLLBACK在触发器内调用以取消该 DML 操作的方式。

选项 1(首先防止错误 - 如果可以可靠地测试错误条件,则首选)

(2020 年 10 月 21 日添加:不知道我在发布此答案时如何或为什么没有想到此选项)

如果可能的话,如果可以测试导致错误的条件,那么最好在尝试有时会失败的操作之前测试该条件。如果错误从未真正发生,则您无需更改默认行为和/或添加自定义事务处理。在您的情况下(即将新记录添加到存档表),您可以执行以下操作之一:

SET NOCOUNT ON;

BEGIN TRY

    INSERT INTO dbo.Archive
        SELECT *
        FROM   inserted ins
        WHERE  NOT EXISTS (SELECT *
                           FROM   dbo.Archive arc
                           WHERE  arc.[Login] = ins.[Login])
                 -- assuming [Login] field exists and should be unique
END TRY
BEGIN CATCH
    DECLARE @DoNothing INT;
END CATCH;
Run Code Online (Sandbox Code Playgroud)

有些时候,这会不会赶上一切,还有可能是偶尔的违规行为,在这种情况下,你仍然可能需要即使使用选项2使用选项2.但是,它始终工作,最好还是试图阻止错误,因为必须完成工作才能达到导致错误的条件,而这只是浪费时间、IO、争用等。

选项 2A(防止错误取消交易 - 如果触发器中有多个 DML 语句,则首选)

考虑到这一点,您应该能够删除BEGIN TRAN;COMMIT TRAN;行以使其正常工作。最终效果是,如果没有错误,INSERT进入Archive表将按预期提交,但如果有错误,它将执行ROLLBACK保存点并继续。

但是,在删除这两个部分后,您仍然会遇到以下错误的棘手情况:

消息 3931,级别 16,状态 1,过程 Customer_Insert_Trigger_Test,行 XXXXX
当前事务无法提交且无法回滚到保存点。回滚整个事务。

这种行为的原因似乎是XACT_ABORT ON系统调用触发器时的隐式设置。的效果XACT_ABORT ON是取消大多数错误(编译错误或 from 除外)的事务(和查询批处理RAISERROR)。补救措施?只需XACT_ABORT OFF在触发器的开头设置即可。

例如,以下对我有用:

CREATE
--ALTER
TRIGGER [dbo].[Customer_Insert_Trigger_Test] 
   ON  [dbo].[Inserts]
   AFTER INSERT
AS 
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;

BEGIN TRY
    PRINT '@@TRANCOUNT = ' + CONVERT(VARCHAR(10), @@TRANCOUNT); -- for debug only
    SAVE TRANSACTION InsertSaveHere;

    -- Simulating error situation
    DECLARE @Error INT = 1 / 0; -- runtime error

   -- Insert into Archive select * from Inserted;
END TRY
BEGIN CATCH
    PRINT 'Entering CATCH block...'; -- for debug only
    ROLLBACK TRANSACTION InsertSaveHere;
END CATCH;

END;
Run Code Online (Sandbox Code Playgroud)

请注意,这种方法并不会相对于改变触发器在这个表中预期的行为:1)实际COMMIT在最外层发生(无论是最初的DML语句或超越,如果一个明确的交易已在这之前启动语句),2)此表上的其他潜在触发器发出 ROLLBACK 以取消操作的能力,以及 3)在此表上的 DML 语句之前启动的显式事务的能力,发出 ROLLBACK 以取消所有更改包括对此表的 DML 操作。

选项 2B(防止错误取消交易 - 如果触发器中有单个 DML 语句,则首选)

当然,如果能够在此错误的唯一的事情就是INSERTArchive表中,那么你很可能也摆脱了SAVE TRANROLLBACK TRANSACTION InsertSaveHere;,只是做在事CATCH块,以便它不是空的,像DECLARE @Test INT;可能会奏效。这里的推理是,错误从未真正发生过的单个 DML 语句,因此没有什么可以回滚;-)。


选项 3(不推荐

要回答这个问题在标题中说:你应该能够COMMIT触发内,但我会eXtreeemely,因为它改变了当交易将提交或回滚预期的行为,这可能会影响正常操作谨慎做这样的事此表上的其他触发器(ROLLBACK如果此触发器首先运行,它们将无法发出取消操作的命令),并且会阻止在此表上的 DML 操作之前启动的显式事务的预期操作。

为此(注意:在继续阅读本段之前,您需要直接阅读上面的段落),您将发出 a COMMIT TRAN;(因为触发器已存在于事务中),然后执行 a BEGIN TRAN;。The COMMIT TRAN;will commit the initial DML operation, the BEGIN TRAN;will put @@TRANCOUNTback to 1 so that当触发器执行结束时,你不会收到错误,指出触发器以与@@TRANCOUNT开始时不同的方式结束。