【问题标题】:Flyway fail on error in Transact-SQL migrationFlyway 在 Transact-SQL 迁移中因错误而失败
【发布时间】:2021-02-11 08:48:47
【问题描述】:

将 Flyway 与 Microsoft SQL Server 结合使用时,我们观察到this question 中描述的问题。

基本上,像这样的迁移脚本不会在另一部分失败时回滚成功的GO-delimited 批次:

BEGIN TRANSACTION

-- Create a table with two nullable columns
CREATE TABLE [dbo].[t1](
    [id] [nvarchar](36) NULL,
    [name] [nvarchar](36) NULL
)

-- add one row having one NULL column
INSERT INTO [dbo].[t1] VALUES(NEWID(), NULL)

-- set one column as NOT NULLABLE
-- this fails because of the previous insert
ALTER TABLE [dbo].[t1] ALTER COLUMN [name] [nvarchar](36) NOT NULL
GO

-- create a table as next action, so that we can test whether the rollback happened properly
CREATE TABLE [dbo].[t2](
    [id] [nvarchar](36) NOT NULL
)
GO

COMMIT TRANSACTION

在上面的示例中,尽管前面的 ALTER TABLE 语句失败,但正在创建表 t2

在链接的问题上,提出了以下方法(在飞行路线上下文之外):

  1. 一个多批处理脚本应该有一个错误处理程序范围,它在出错时回滚事务,并在最后提交。在 TSQL 中,您可以使用动态 sql 来做到这一点

    • 动态 SQL 使脚本难以阅读,非常不方便
  2. 使用 SQLCMD,您可以使用 -b 选项在出错时中止脚本

    • 这可以在 flyway 中使用吗?
  3. 或者滚动你自己的脚本运行器

    • flyway 可能是这种情况吗?是否有特定于 flyway 的配置来启用正确的错误失败?

编辑:替代示例

给定:简单的数据库

BEGIN TRANSACTION

CREATE TABLE [a] (
    [a_id] [nvarchar](36) NOT NULL,
    [a_name] [nvarchar](100) NOT NULL
);

CREATE TABLE [b] (
    [b_id] [nvarchar](36) NOT NULL,
    [a_name] [nvarchar](100) NOT NULL
);

INSERT INTO [a] VALUES (NEWID(), 'name-1');
INSERT INTO [b] VALUES (NEWID(), 'name-1'), (NEWID(), 'name-2');

COMMIT TRANSACTION

迁移脚本 1(失败,没有 GO)

BEGIN TRANSACTION

ALTER TABLE [b] ADD [a_id] [nvarchar](36) NULL;

UPDATE [b] SET [a_id] = [a].[a_id] FROM [a] WHERE [a].[a_name] = [b].[a_name];

ALTER TABLE [b] ALTER COLUMN [a_id] [nvarchar](36) NOT NULL;

ALTER TABLE [b] DROP COLUMN [a_name];

COMMIT TRANSACTION

这会导致UPDATE 语句出现错误消息Invalid column name 'a_id'.
可能的解决方案:在语句之间引入GO

迁移脚本 2(使用 GO:为“快乐情况”工作,但在出现错误时仅部分回滚)

BEGIN TRANSACTION
SET XACT_ABORT ON
GO

ALTER TABLE [b] ADD [a_id] [nvarchar](36) NULL;
GO
UPDATE [b] SET [a_id] = [a].[a_id] FROM [a] WHERE [a].[a_name] = [b].[a_name];
GO
ALTER TABLE [b] ALTER COLUMN [a_id] [nvarchar](36) NOT NULL;
GO
ALTER TABLE [b] DROP COLUMN [a_name];
GO

COMMIT TRANSACTION
  • 只要表 [b] 中的所有值在表 [a] 中都有匹配条目,就会执行所需的迁移。
  • 在给定的示例中,情况并非如此。 IE。我们得到两个错误:
    • 预期:Cannot insert the value NULL into column 'a_id', table 'test.dbo.b'; column does not allow nulls. UPDATE fails.
    • 意外:The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
    • 可怕的是:最后一个 ALTER TABLE [b] DROP COLUMN [a_name] 语句实际上已执行、提交且未回滚。即由于链接列丢失,之后无法修复此问题。

这种行为实际上是独立于flyway的,可以通过SSMS直接重现。

【问题讨论】:

    标签: tsql flyway


    【解决方案1】:

    问题是 GO 命令的基础。它不是 T-SQL 语言的一部分。它是在 SQL Server Management Studio、sqlcmd 和 Azure Data Studio 中使用的构造。 Flyway 只是通过 JDBC 连接将命令传递到您的 SQL Server 实例。它不会像 Microsoft 工具那样处理那些 GO 命令,将它们分成独立的批次。这就是为什么您不会看到单独的错误回滚,而是看到总回滚。

    据我所知,解决此问题的唯一方法是将批次拆分为单独的迁移脚本。以这样的方式命名它们,以便清楚,V3.1.1、V3.1.2 等,以便所有内容都在 V3.1* 版本(或类似的版本)之下。然后,每个单独的迁移将通过或失败,而不是全部进行或全部失败。

    【讨论】:

    • 我实际上想要完全回滚,目前还没有发生。
    【解决方案2】:

    已编辑 20201102 -- 对此了解了很多,并在很大程度上重写了它!到目前为止一直在 SSMS 中进行测试,并计划在 Flyway 中进行测试并写一篇博客文章。为了简化迁移,我相信您可以根据需要将@@trancount 检查/错误处理放入存储过程中,这也在我的测试列表中。

    修复中的成分

    对于 SQL Server 中的错误处理和事务管理,有三件事可能会有很大帮助:

    • 将 XACT_ABORT 设置为 ON(默认为关闭)。此设置“指定当 Transact-SQL 语句引发运行时错误时 SQL Server 是否自动回滚当前事务”docs
    • 在您发送的每个批次分隔符后检查 @@TRANCOUNT 状态,并在需要时使用 RAISERROR / RETURN 来“纾困”
    • Try/catch/throw -- 我在这些示例中使用 RAISERROR,如果您可以使用,Microsoft 建议您使用 THROW(我认为它可以使用 SQL Server 2016+)-docs

    处理原始示例代码

    两个变化:

    • 将 XACT_ABORT 设置为 ON;
    • 在发送每个批次分隔符后检查@@TRANCOUNT 以查看是否应该运行下一个批次。这里的关键是,如果发生错误,@@TRANCOUNT 将为 0。如果没有发生错误,则为 1。(注意:如果显式打开多个“嵌套”事务,则需要调整 trancount检查,因为它可以高于 1)

    在这种情况下,即使 XACT_ABORT 关闭,@@TRANCOUNT 检查子句也会起作用,但我相信您希望在其他情况下也可以使用它。 (需要阅读更多关于这方面的信息,但我还没有遇到过开启它的缺点。)

    BEGIN TRANSACTION;
    SET XACT_ABORT ON;
    GO
    
    -- Create a table with two nullable columns
    CREATE TABLE [dbo].[t1](
        [id] [nvarchar](36) NULL,
        [name] [nvarchar](36) NULL
    )
    
    -- add one row having one NULL column
    INSERT INTO [dbo].[t1] VALUES(NEWID(), NULL)
    
    -- set one column as NOT NULLABLE
    -- this fails because of the previous insert
    ALTER TABLE [dbo].[t1] ALTER COLUMN [name] [nvarchar](36) NOT NULL
    GO
    
    IF @@TRANCOUNT <> 1
    BEGIN
        DECLARE @ErrorMessage AS NVARCHAR(4000);
        SET @ErrorMessage
            = N'Transaction in an invalid or closed state (@@TRANCOUNT=' + CAST(@@TRANCOUNT AS NVARCHAR(10))
              + N'). Exactly 1 transaction should be open at this point.  Rolling-back any pending transactions.';
        RAISERROR(@ErrorMessage, 16, 127);
        RETURN;
    END;
    
    -- create a table as next action, so that we can test whether the rollback happened properly
    CREATE TABLE [dbo].[t2](
        [id] [nvarchar](36) NOT NULL
    )
    GO
    
    
    COMMIT TRANSACTION;
    

    替代示例

    我在顶部添加了一些代码,以便能够重置测试数据库。我重复了使用 XACT_ABORT ON 并在发送每个批处理终止符 (GO) 后检查 @@TRANCOUNT 的模式。

    /* Reset database */
    
    USE master;
    GO
    
    IF DB_ID('transactionlearning') IS NOT NULL
    BEGIN
        ALTER DATABASE transactionlearning
        SET SINGLE_USER
        WITH ROLLBACK IMMEDIATE;
        DROP DATABASE transactionlearning;
    END;
    GO
    CREATE DATABASE transactionlearning;
    GO
    
    
    /* set up simple schema */
    USE transactionlearning;
    GO
    
    BEGIN TRANSACTION;
    
    CREATE TABLE [a]
    (
        [a_id] [NVARCHAR](36) NOT NULL,
        [a_name] [NVARCHAR](100) NOT NULL
    );
    
    CREATE TABLE [b]
    (
        [b_id] [NVARCHAR](36) NOT NULL,
        [a_name] [NVARCHAR](100) NOT NULL
    );
    
    INSERT INTO [a]
    VALUES
    (NEWID(), 'name-1');
    INSERT INTO [b]
    VALUES
    (NEWID(), 'name-1'),
    (NEWID(), 'name-2');
    
    COMMIT TRANSACTION;
    
    GO
    
    /*******************************************************/
    /* Test transaction error handling starts here         */
    /*******************************************************/
    USE transactionlearning;
    GO
    
    BEGIN TRANSACTION;
    SET XACT_ABORT ON;
    GO
    
    IF @@TRANCOUNT <> 1
    BEGIN
        DECLARE @ErrorMessage AS NVARCHAR(4000);
        SET @ErrorMessage
            = N'Check 1: Transaction in an invalid or closed state (@@TRANCOUNT=' + CAST(@@TRANCOUNT AS NVARCHAR(10))
              + N'). Exactly 1 transaction should be open at this point.  Rolling-back any pending transactions.';
        RAISERROR(@ErrorMessage, 16, 127);
        RETURN;
    END;
    
    
    ALTER TABLE [b] ADD [a_id] [NVARCHAR](36) NULL;
    GO
    
    
    IF @@TRANCOUNT <> 1
    BEGIN
        DECLARE @ErrorMessage AS NVARCHAR(4000);
        SET @ErrorMessage
            = N'Check 2: Transaction in an invalid or closed state (@@TRANCOUNT=' + CAST(@@TRANCOUNT AS NVARCHAR(10))
              + N'). Exactly 1 transaction should be open at this point.  Rolling-back any pending transactions.';
        RAISERROR(@ErrorMessage, 16, 127);
        RETURN;
    END;
    
    UPDATE [b]
    SET [a_id] = [a].[a_id]
    FROM [a]
    WHERE [a].[a_name] = [b].[a_name];
    GO
    
    IF @@TRANCOUNT <> 1
    BEGIN
        DECLARE @ErrorMessage AS NVARCHAR(4000);
        SET @ErrorMessage
            = N'Check 3: Transaction in an invalid or closed state (@@TRANCOUNT=' + CAST(@@TRANCOUNT AS NVARCHAR(10))
              + N'). Exactly 1 transaction should be open at this point.  Rolling-back any pending transactions.';
        RAISERROR(@ErrorMessage, 16, 127);
        RETURN;
    END;
    
    ALTER TABLE [b] ALTER COLUMN [a_id] [NVARCHAR](36) NOT NULL;
    GO
    
    IF @@TRANCOUNT <> 1
    BEGIN
        DECLARE @ErrorMessage AS NVARCHAR(4000);
        SET @ErrorMessage
            = N'Check 4: Transaction in an invalid or closed state (@@TRANCOUNT=' + CAST(@@TRANCOUNT AS NVARCHAR(10))
              + N'). Exactly 1 transaction should be open at this point.  Rolling-back any pending transactions.';
        RAISERROR(@ErrorMessage, 16, 127);
        RETURN;
    END;
    
    ALTER TABLE [b] DROP COLUMN [a_name];
    GO
    
    
    COMMIT TRANSACTION;
    
    

    我最喜欢的关于这个主题的参考资料

    有一个很棒的免费在线资源,它非常详细地研究了错误和事务处理。它由 Erland Sommarskog 编写和维护:

    一个常见的问题是为什么仍然需要 XACT_ABORT/如果它完全被 TRY/CATCH 取代。不幸的是,它并没有被完全取代,Erland 在他的论文this is a good place to start on that 中有一些这样的例子。

    【讨论】:

    • 很遗憾,XACT_ABORT 只影响单个批次;即下一个GO 之后的所有内容仍在执行中。
    • 嗨——我认为您实际上需要将我的答案与格兰特的答案结合起来,我认为我们每个人都有一部分!当我在 SSMS 中运行您的示例命令时,它给出了一个额外的错误:Msg 3902, Level 16, State 1, Line 30 COMMIT TRANSACTION 请求没有相应的 BEGIN TRANSACTION。本质上,看起来 GO 正在默默地提交事务。正如格兰特所说,这是一种不寻常的模式,因为“GO”实际上不是 TSQL,所以这是将批处理分隔符混合到打开的事务中。这也可以通过以下方式验证:BEGIN TRANSACTION SET XACT_ABORT ON GO DBCC OPENTRAN
    • 好吧,如果GO 不应该在事务中使用,但我想要一个事务并且需要确保各个语句以特定顺序执行;然后我应该使用分号而不是GO 或者当“只有”分号时,执行计划中执行语句的顺序是否仍然可以更改?
    • 分号只是一个语句终止符。正如一位朋友曾经说过的,对于某些语句,它是语法糖(尽管我总是使用它)。 Flyway 将遵循的操作顺序是语句的顺序。所以终结者与否(我建议使用它们),您应该看到该订单保持不变。
    • @GrantFritchey 好的分号在这里没有解决方案。取点。而“操作/语句的顺序”对我来说是错误的措辞。我在问题的底部添加了另一个示例,说明了我的意思:(1)没有GO,它根本不起作用,因为看起来第一条语句的结果没有在第二条语句的执行计划中考虑,但在至少它完全中止了事务,并且 (2) 使用 GO,如果数据符合预期,它就可以工作,但在错误情况下只执行部分回滚。
    猜你喜欢
    • 2017-06-03
    • 2019-01-16
    • 2021-03-18
    • 2012-08-09
    • 2014-08-02
    • 2016-01-01
    • 2012-05-17
    • 2020-05-24
    • 2020-02-25
    相关资源
    最近更新 更多