TSQL:防止触发器抑制错误但回滚事务

时间:2016-02-17 16:48:15

标签: sql sql-server hibernate tsql

我遇到一个问题,其中一个触发器的一部分失败了。此故障导致包装触发器的事务回滚。问题在于它没有向命令的调用点引发错误。它的行为就像交易中没有错误一样,它被捕获的唯一方法是应该更改的数据不是。 在我的例子中,发生的事情是交易中有很多变化。触发器中发生错误。然而,一切都被回滚,命令的调用者没有看到SQL异常。呼叫者永远不会被告知存在问题。

有没有办法指示TSQL以一种向命令的调用点报告错误的方式抛出异常?

    CREATE TABLE Archive (
        aColumn INT NOT NULL
    )


    CREATE TABLE Source (
        aColumn INT NULL
    )
   --
    CREATE TRIGGER Archive_Trigger ON Source
    AFTER UPDATE
    AS
    BEGIN
        INSERT INTO Archive
        SELECT DELETED.aColumn
        FROM DELETED
    END

   -- Other attempts
    CREATE TRIGGER Archive_Trigger ON Source
    AFTER UPDATE
    AS
    BEGIN
        BEGIN TRY
            INSERT INTO Archive
            SELECT DELETED.aColumn
            FROM DELETED
       END TRY
       BEGIN CATCH
            SET XACT_ABORT ON;
            DECLARE @ErrorMessage NVARCHAR(4000) ;
            SET @ErrorMessage = ERROR_MESSAGE() ;
            RAISERROR('Error %s occurred in Archive Trigger', 16, 1, @ErrorMessage) ;
       END CATCH
    END

这是触发器正在做的非常天真的版本。问题不会发生在生产系统上。当它发生在开发环境中时。当/ Source /表更改时,/ Archive / table也必须更改。有时会忘记存档,也就是错误发生的时间。例如,如果aColumn在归档中不为null而在Source中为null。这个实例会导致问题。

如果我执行以下代码,在表中有一行null aColumn。我期望的结果是得到一个例外。我没有。

DataSource source;
Connection connection = source.getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE Source SET aColumn = NULL");
statement.executeUpdate();

3 个答案:

答案 0 :(得分:1)

有些例外severity不足以打破当前批次的执行。它只是继续并执行后续声明。如果我没有弄错,例如,约束违规不是批处理错误。

为了确保明显错误的整批中断,您可以将XACT_ABORT选项设置为ON,如果已建立,它还将回滚整个事务

管理异常的最佳方法是使用TRY-CATCH块supposed包围您的代码,以响应严重性高于信息消息且低于错误断开连接的所有错误。落入CATCH块后(除非你故意使异常静音,否则无法避免)你可能会抛出另一个用户定义的异常,回滚等等,正如你自己所知道的那样。 TRY-CATCH保证您的代码会对异常做出反应(如果有的话)。

建议不要在XACT_ABORT内启用TRY-CATCH,因为它有点无意义:如果发生异常,您正试图控制代码的行为(使用try-catch)并同时告诉服务器“abort'em all!”(使用xact_abort)。

答案 1 :(得分:0)

如果我理解正确,你的开发人员正在创建触发器,但因为忘记更改脚本而插入错误的存档表?

我们所做的是使用动态sql创建所有审计触发器。我们的设计为每个审计表都有相同的列(基本上是一个rowid,它是表的id列,列名列和旧值列以及新值列。所以我们动态发送的是名称我们正在创建的表,它基于将您想要触发的表名的变量放入动态sql中使用的变量。如果要使审计表与表的列匹配,那么它会更复杂一点,那么你需要从系统表中获取列名并放入临时表并加入动态sql中的列名。

当需要创建审计表和触发器时,我们运行一个执行这两个操作的脚本,我们所要做的就是输入我们放置审计表的数据库和表的名称。

需要一段时间才能解决这个问题,但是一旦你做到了,创建新的审计就很容易,你就不会像引用错误的审计表那样出错。

答案 2 :(得分:0)

RAISERROR语句不是停止处理的严重错误。事实上,MSDN says

  

THROW声明表彰SET XACT_ABORT。 RAISERROR没有。新   应用程序应使用THROW而不是RAISERROR。

您可以通过运行以下SQL来验证此行为:

set xact_abort on
begin tran
raiserror('error 1', 16, 1, null)
raiserror('error 2', 16, 1, null)

输出将是:

Msg 50000, Level 16, State 1, Line 3
error 1
Msg 50000, Level 16, State 1, Line 4
error 2

除非您是系统管理员,否则任何引发严重错误的尝试都将失败:

  

消息2754,级别16,状态1,行8错误严重性级别大于   18只能由sysadmin角色的成员指定,使用   WITH LOG选项。

我认为你应该使用SET XACT_ABORT ON,但是:

1)在实际SET XACT_ABORT ON期间执行INSERT。这将导致原始错误终止连接。

例如:

CREATE TRIGGER Archive_Trigger ON Source
AFTER UPDATE
AS
BEGIN
    SET XACT_ABORT ON
    INSERT INTO Archive
    SELECT DELETED.aColumn
    FROM DELETED
END

2)或者,您可能会在BEGIN CATCH语句中导致更严重的错误。例如,使用SELECT 1/0导致除以0错误。客户端必须忽略除零错误才能得到您提出的错误消息。

请注意the original SET XACT_ABORT setting will be restored after leaving the trigger(搜索"触发")。

当触发器确实导致取消触发器的错误时,SQL Server将使用以下文本将错误3609引发到客户端:

  

交易在触发器中结束。批次已中止。

我们的应用程序中发生了这个特定问题。事务在触发器内被杀死。客户端应用程序未收到错误,并在事务之外继续。此时发生了非常糟糕的事情。

我们通过让数据访问对象(DAO)检测当前数据库连接何时关闭,中断或事务为空来解决这个问题。在这种情况下,如果在具有3609的SqlException.Errors中返回SQL错误,则DAO会抛出特定的"该事务在触发器中结束"异常。