如何解决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
。
-
多批处理脚本应具有单个错误处理程序范围,该范围将在发生错误时回滚事务,并在最后提交。在Tsql中,您可以使用动态sql完成此操作
- 动态sql使得脚本难以阅读,并且非常不方便
-
使用sqlCMD,您可以使用
-b
选项在出错时中止脚本- 这在飞行通道中可用吗?
-
或者滚动自己的脚本运行器
编辑:替代示例
给出:简单的数据库
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]
中具有匹配的条目,这就会执行所需的迁移。 - 在给定的示例中,情况并非如此。即我们得到两个错误:
此行为实际上与飞行路线无关,可以直接通过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编写和维护的:
- 第一部分– Jumpstart Error Handling
- 第二部分– Commands and Mechanisms
- 第三部分– Implementation
一个常见的问题是,为什么仍需要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 *版本(或类似版本)之下。然后,每个迁移都会通过或失败,而不是全部或全部失败。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。