Transact-SQL迁移中的错误导致Flyway失败 给出:简单的数据库迁移脚本1失败,不使用GO迁移脚本2使用GO:适用于“很高兴的情况”,但发生错误时仅部分回滚

问题描述

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

基本上,这样的迁移脚本不会在另一部分发生故障时回滚成功的GO分隔的批处理:

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

在以上示例中,即使前面的t2语句失败,也正在创建表ALTER TABLE

在链接的问题上,建议使用以下方法(在飞行通道上下文之外):

  1. 多批处理脚本应具有单个错误处理程序范围,该范围将在发生错误时回滚事务,并在最后提交。在TSQL中,您可以使用动态sql完成此操作

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

    • 这在飞行通道中可用吗?
  3. 或者滚动自己的脚本运行器

    • 在飞机道上可能是这种情况吗?是否有特定于飞行路线的配置来启用正确的错误排除功能?

编辑:替代示例

给出:简单的数据库

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

这将导致Invalid column name 'a_id'.语句出现错误消息UPDATE
可能的解决方案:在语句之间引入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]中具有匹配的条目,这就会执行所需的迁移。
  • 在给定的示例中,情况并非如此。即我们得到两个错误:
    • 预期: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]语句实际上已执行,提交并没有回滚。后来由于链接列丢失,无法解决此问题。

此行为实际上与飞行路线无关,可以直接通过SSMS复制。

解决方法

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

修复中的成分

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

  • 将XACT_ABORT设置为ON(默认情况下处于关闭状态)。此设置“指定当Transact-SQL语句引发运行时错误时SQL Server是否自动回滚当前事务” docs
  • 在您发送的每个批量定界符之后检查@@ TRANCOUNT状态,并在需要时使用它通过RAISERROR / RETURN“纾困”
  • 尝试/捕获/抛出-在这些示例中使用RAISERROR,Microsoft建议您使用THROW(如果可用)(我认为它可以使用SQL Server 2016+)-docs

处理原始示例代码

两项更改:

  • 将XACT_ABORT设置为开;
  • 在发送每个批次定界符之后,对@@ TRANCOUNT进行检查,以查看是否应运行下一个批次。此处的关键是,如果发生错误,则@@ TRANCOUNT将为0。如果未发生错误,则将为1。(注意:如果您明确打开多个“嵌套”交易,则需要调整笔数检查,因为它可能高于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;

替代示例

我在顶部添加了一些代码,以便能够重置测试数据库。在发送每个批处理终止符(GO)之后,我重复了使用XACT_ABORT ON并检查@@ 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,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,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,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,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中有一些例子。

,

问题是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 *版本(或类似版本)之下。然后,每个迁移都会通过或失败,而不是全部或全部失败。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...