事务回滚未按预期工作
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。
如果链接的答案不适用于此查询,您能否详细说明原因,以及为什么它与他们在答案中给出的示例不同?
我不能(或者更确切地说,我相信我不能)删除GO
s,因为上面的脚本需要GO
s 才能编译。如果我删除GO
s,那么以后依赖于新添加/重命名的列的语句将不会编译。并且查询无法运行。
有没有办法解决这个问题,删除GO
s?
【问题讨论】:
将整个东西放在一个 try/catch 块中,并在 catch 中回滚。 @SeanLange -TRY
/CATCH
也不允许跨批次,所以如果需要GO
s,那将不起作用。
@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
是不可能的......该脚本需要GO
s 才能编译。如果我删除了GO
s,那么以后依赖于新添加/重命名的列的语句将不会编译。【参考方案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 内的事务,否则您将有一个事务处于打开状态。 这就是他想要的,不是吗?是用来测试脚本的吗? 是的,但是如果出现错误,它将落到您的捕获中,并且还需要在那里回滚,否则事务将被孤立。 据我所知,它是孤立的,因此可以在该状态下测试结果。以上是关于事务回滚未按预期工作的主要内容,如果未能解决你的问题,请参考以下文章