事务回滚未按预期工作

Posted

技术标签:

【中文标题】事务回滚未按预期工作【英文标题】:TRANSACTION rollback not working as expected 【发布时间】:2017-04-03 12:54:37 【问题描述】:

我正在做一些数据库架构重组。

我有一个大致如下的脚本:

BEGIN TRAN LabelledTransaction
    --Remove FKs
    ALTER TABLE myOtherTable1 DROP CONSTRAINT <constraintStuff>
    ALTER TABLE myOtherTable2 DROP CONSTRAINT <constraintStuff>

    --Remove PK
    ALTER TABLE myTable DROP CONSTRAINT PK_for_myTable

    --Add replacement id column with new type and IDENTITY
    ALTER TABLE myTable ADD id_new int Identity(1, 1) NOT NULL
    GO
    ALTER TABLE myTable ADD CONSTRAINT PK_for_myTable PRIMARY KEY CLUSTERED (id_new)
    GO
    SELECT * FROM myTable

    --Change referencing table types
    ALTER TABLE myOtherTable1 ALTER COLUMN col_id int NULL
    ALTER TABLE myOtherTable2 ALTER COLUMN col_id int NOT NULL

    --Change referencing table values
    UPDATE myOtherTable1 SET consignment_id = Target.id_new FROM myOtherTable1 AS Source JOIN <on key table>
    UPDATE myOtherTable2 SET consignment_id = Target.id_new FROM myOtherTable2 AS Source JOIN <on key table>

    --Replace old column with new column 
    ALTER TABLE myTable DROP COLUMN col_id
    GO
    EXEC sp_rename 'myTable.id_new', 'col_id', 'Column'
    GO

    --Reinstate any OTHER PKs disabled
    ALTER TABLE myTable ADD CONSTRAINT <PK defn>

    --Reinstate FKs
    ALTER TABLE myOtherTable1 WITH CHECK ADD CONSTRAINT <constraintStuff>
    ALTER TABLE myOtherTable2 WITH CHECK ADD CONSTRAINT <constraintStuff>   

    SELECT * FROM myTable

    -- Reload out-of-date views
    EXEC sp_refreshview 'someView'

    -- Remove obsolete sequence
    DROP SEQUENCE mySeq
ROLLBACK TRAN LabelledTransaction

显然,这一切都有些删节,但细节并不是这里的重要内容。

当然,很难找到在核心更改之前需要关闭/编辑的所有内容(即使有一些元查询来帮助我),所以我并不总是第一次得到正确的脚本。

但我放入了 ROLLBACK 以确保失败的尝试使数据库保持不变。

但我实际看到的是,如果 TRAN 中出现错误,则不会发生 ROLLBACK。我想我收到有关“回滚没有匹配的 TRAN”的错误?

我的第一直觉是它与 GO 语句有关,但 https://***.com/a/11121382/1662268 建议标记 TRAN 应该解决这个问题? 发生了什么?如果有错误,为什么不能正确回滚更改。

如何编写和测试这些脚本,以便在脚本第一次不完美时无需手动恢复任何部分更改?


编辑: 基于第一个答案的其他 cmets。

如果链接的答案不适用于此查询,您能否详细说明原因,以及为什么它与他们在答案中给出的示例不同?

我不能(或者更确切地说,我相信我不能)删除GOs,因为上面的脚本需要GOs 才能编译。如果我删除GOs,那么以后依赖于新添加/重命名的列的语句将不会编译。并且查询无法运行。

有没有办法解决这个问题,删除GOs?

【问题讨论】:

将整个东西放在一个 try/catch 块中,并在 catch 中回滚。 @SeanLange -TRY/CATCH 也不允许跨批次,所以如果需要GOs,那将不起作用。 @Damien_The_Unbeliever - 天哪,我什至没有注意到它是多批次的......这将教会我在周一早上喝完第一杯咖啡之前发表评论。 【参考方案1】:

如果您有任何错误会自动导致事务回滚,那么事务将作为当前批处理的一部分回滚。

然后,控制权将返回给客户端工具,该工具将下一批发送到服务器,并且下一批(和后续的)不会被包装在任何事务中。

最后,当执行最后一批尝试运行回滚时,您将收到收到的错误消息。

因此,您需要保护每个批次在不受事务保护时运行。

一种方法是插入我们的老油炸GOTO

GO
IF @@TRANCOUNT=0 GOTO NBATCH
...Rest of Code
NBATCH:
GO

SET FMTONLY:

GO
IF @@TRANCOUNT=0 BEGIN
    SET FMTONLY ON
END
...Rest of Code
GO

当然,这不会解决所有问题 - 某些语句需要是批处理中的第一个或唯一一个语句。为了解决这些问题,我们必须将上述技术之一与某种形式的EXEC 结合起来:

GO
IF @@TRANCOUNT=0 BEGIN
    SET FMTONLY ON
END
EXEC sp_executesql N'/*Code that needs to be in its own batch*/'
GO

(如果一批代码依赖于前一批已执行的工作,其中引入了数据库对象(表、列等),那么您也必须采用这种技术,因为如果以前的批处理从未执行,新对象将不存在)


我还刚刚发现sqlcmd tool 的-b 选项的存在。以下脚本在通过 SSMS 运行时会产生两个错误:

begin transaction
go
set xact_abort on
go
create table T(ID int not null,constraint CK_ID check (ID=4))
go
insert into T(ID) values (3)
go
rollback

错误:

Msg 547, Level 16, State 0, Line 7
The INSERT statement conflicted with the CHECK constraint "CK_ID". The conflict occurred in database "TestDB", table "dbo.T", column 'ID'.
Msg 3903, Level 16, State 1, Line 9
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.

但是,相同的脚本保存为 Abortable.sql 并使用以下命令行运行:

sqlcmd -b -E -i Abortable.sql -S .\SQL2014 -d TestDB

生成单个错误:

Msg 547, Level 16, State 1, Server .\SQL2014, Line 1
The INSERT statement conflicted with the CHECK constraint "CK_ID". The conflict
occurred in database "TestDB", table "dbo.T", column 'ID'.

因此,看起来从命令行运行脚本并使用-b 选项可能是另一种方法。我刚刚搜索了 SSMS 选项/属性,看看是否可以找到与 -b 等效的内容,但我没有找到。

【讨论】:

是的,这种方法奏效了。它像罪恶一样丑陋,但在没有更好的情况下,这似乎是最好的方法。原则上会留问题几天。如果我不回来接受这个答案,请随意纠缠。 @Brondahl - 我刚刚做了一个实质性的补充 - 如果你愿意使用 sqlcmd 而不是SSMS. 不错的发现!遗憾的是 SSMS 不支持它,但对于未来来说,这是一件非常有价值的事情。谢谢!【参考方案2】:

删除“GO”,完成交易

【讨论】:

根据我的笔记,链接的帖子断言命名交易应该规避这一点。你认为这不准确吗?如果是这样,请扩展为什么?此外,删除GO 是不可能的......该脚本需要GOs 才能编译。如果我删除了GOs,那么以后依赖于新添加/重命名的列的语句将不会编译。【参考方案3】:

只有在完成后才回滚 - 只需使用 TRY/CATCH:

BEGIN TRANSACTION;
BEGIN TRY
     --Remove FKs
    ALTER TABLE myOtherTable1 DROP CONSTRAINT <constraintStuff>
    ALTER TABLE myOtherTable2 DROP CONSTRAINT <constraintStuff>
    --Remove PK
    ALTER TABLE myTable DROP CONSTRAINT PK_for_myTable
    --Add replacement id column with new type and IDENTITY
    ALTER TABLE myTable ADD id_new int Identity(1, 1) NOT NULL
    ALTER TABLE myTable ADD CONSTRAINT PK_for_myTable PRIMARY KEY CLUSTERED (id_new)
    SELECT * FROM myTable
    --Change referencing table types
    ALTER TABLE myOtherTable1 ALTER COLUMN col_id int NULL
    ALTER TABLE myOtherTable2 ALTER COLUMN col_id int NOT NULL
    --Change referencing table values
    UPDATE myOtherTable1 SET consignment_id = Target.id_new FROM myOtherTable1 AS Source JOIN <on key table>
    UPDATE myOtherTable2 SET consignment_id = Target.id_new FROM myOtherTable2 AS Source JOIN <on key table>
    --Replace old column with new column 
    ALTER TABLE myTable DROP COLUMN col_id
    EXEC sp_rename 'myTable.id_new', 'col_id', 'Column'
    --Reinstate any OTHER PKs disabled
    ALTER TABLE myTable ADD CONSTRAINT <PK defn>
    --Reinstate FKs
    ALTER TABLE myOtherTable1 WITH CHECK ADD CONSTRAINT <constraintStuff>
    ALTER TABLE myOtherTable2 WITH CHECK ADD CONSTRAINT <constraintStuff>   
    SELECT * FROM myTable
    -- Reload out-of-date views
    EXEC sp_refreshview 'someView'
    -- Remove obsolete sequence
    DROP SEQUENCE mySeq
    ROLLBACK TRANSACTION
END TRY
BEGIN CATCH
   print 'Error caught'
   select ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() AS ErrorMessage;
END CATCH;

【讨论】:

您还需要回滚 catch 内的事务,否则您将有一个事务处于打开状态。 这就是他想要的,不是吗?是用来测试脚本的吗? 是的,但是如果出现错误,它将落到您的捕获中,并且还需要在那里回滚,否则事务将被孤立。 据我所知,它是孤立的,因此可以在该状态下测试结果。

以上是关于事务回滚未按预期工作的主要内容,如果未能解决你的问题,请参考以下文章

Postgresql 可序列化事务未按预期工作

实体未按预期提交(Hibernate ORM with Panache)

用日志文件恢复事务的过程

为啥这个回滚功能不能按预期工作

Postgresql 捕获事务错误和回滚

sql server 2000日志有啥用处