在失败的ALTER TABLE … ADD CONSTRAINT上将事务回滚到保存点

有没有办法在事务中添加检查约束,如果失败回滚到以前的保存点(而不是回滚整个事务)?

在我的情况下,当ALTER TABLE … ADD CONSTRAINT命令失败时,事务无法回滚到保存点(尝试这样做会抛出InvalidOperationException)。

概述以certificate关键点:

SqlTransaction transaction = connection.BeginTransaction(); // ... execute SQL commands on the transaction ... // Create savepoint transaction.Save("mySavepoint"); try { // This will fail... SqlCommand boom = new SqlCommand( "ALTER TABLE table WITH CHECK ADD CONSTRAINT ...", connection, transaction); boom.ExecuteNonQuery(); } catch { // ...and should be rolled back to the savepoint, but can't. try { transaction.Rollback("mySavepoint"); } catch (InvalidOperationException) { // Instead, an InvalidOperationException is thrown. // The transaction is unusable and can only be rolled back entirely. transaction.Rollback(); } } 

这里是可以运行的可运行的演示代码(需要一个名为“test”的数据集):

 public class Demo { private const string _connectionString = "Data Source=(local);Integrated security=true;Initial Catalog=test;"; private const string _savepoint = "save"; private static readonly string _tableName = DateTime.Now.ToString("hhmmss"); private static readonly string _constraintName = "CK" + DateTime.Now.ToString("hhmmss"); private static readonly string _createTable = "CREATE TABLE [dbo].[" + _tableName + "] ([one] [int] NULL,[two] [int] NULL) ON [PRIMARY]"; private static readonly string _insert1 = "INSERT INTO [" + _tableName + "] VALUES (1,1)"; private static readonly string _addConstraint = "ALTER TABLE [dbo].[" + _tableName + "] WITH CHECK ADD CONSTRAINT [" + _constraintName + "] CHECK (([one]>(1)))"; private static readonly string _insert2 = "INSERT INTO [" + _tableName + "] VALUES (2,2)"; public static void Main(string[] args) { // Example code! Please ignore missing using statements. SqlConnection connection = new SqlConnection(_connectionString); connection.Open(); SqlTransaction transaction = connection.BeginTransaction(); SqlCommand createTable = new SqlCommand(_createTable, connection, transaction); createTable.ExecuteNonQuery(); // Create savepoint transaction.Save(_savepoint); SqlCommand insert1 = new SqlCommand(_insert1, connection, transaction); insert1.ExecuteNonQuery(); try { // This will fail... SqlCommand boom = new SqlCommand(_addConstraint, connection, transaction); boom.ExecuteNonQuery(); } catch { // ...and should be rolled back to the savepoint, but can't transaction.Rollback(_savepoint); } SqlCommand insert2 = new SqlCommand(_insert2, connection, transaction); insert2.ExecuteNonQuery(); transaction.Commit(); connection.Close(); } } 

当我尝试TSQL时,我得到了相同的行为。

 BEGIN TRAN CREATE TABLE foo (col int) INSERT INTO foo values (1) SAVE TRANSACTION ProcedureSave; BEGIN TRY ALTER TABLE foo WITH CHECK ADD CONSTRAINT ck CHECK (col= 2) END TRY BEGIN CATCH SELECT XACT_STATE() AS XACT_STATE /*Returns -1, transaction is uncommittable. Next line will fail*/ ROLLBACK TRANSACTION ProcedureSave /*Msg 3931, Level 16, State 1: The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction.*/ END CATCH GO SELECT @@TRANCOUNT AS [@@TRANCOUNT] /*Zero the transaction was rolled back*/ 

我没有在文档中找到任何信息,说明哪些错误会导致交易以这种方式注定失败。 我认为此连接项评论中不存在此类文档。

答案是,error handling是逐个案例的。 它不仅取决于服务器性,还取决于错误类型和上下文。 遗憾的是,没有针对不同错误的error handling行为的已发布列表。 通常,只有严重错误才能终止连接,并且非常严重的关闭服务器。 但是,当涉及到语句中止与事务中止时,很难总结规则 – 即它是逐个案例的。

我不认为你可以在脚本和C#中混合使用保存点。 我执行以下SQL:

 BEGIN TRANSACTION INSERT INTO Foos (Fooname) VALUES ('Bar1') SAVE TRANSACTION MySavePoint; INSERT INTO Foos (FooName) VALUES ('Bar2') ROLLBACK TRANSACTION MySavePoint COMMIT TRANSACTION 

这将在SQL中工作,并将使用以下代码:

 using (SqlConnection conn = new SqlConnection("connectionString")) { conn.Open(); using (SqlTransaction trans = conn.BeginTransaction()) using (SqlCommand comm = new SqlCommand("The Above SQL", conn, trans)) { comm.ExecuteNonQuery(); trans.Commit(); } } 

如果你尝试trans.Rollback("MySavePoint"); 它会失败,因为trans对象不能控制保存点 – 它不知道它。

如果将SQL拆分为两个独立插入并使用以下代码:

 using (SqlConnection conn = new SqlConnection("connectionString")) { conn.Open(); using (SqlTransaction trans = conn.BeginTransaction()) using (SqlCommand comm1 = new SqlCommand("INSERT INTO Foos(fooName) VALUES('Bar1')", conn, trans)) using (SqlCommand comm2 = new SqlCommand("INSERT INTO Foos(fooName) VALUES('Bar2')", conn, trans)) { comm1.ExecuteNonQuery(); trans.Save("MySavePoint"); comm2.ExecuteNonQuery(); trans.Rollback("MySavePoint"); trans.Commit(); } } 

它会像你期望的那样工作。

只需注意,始终处理实现IDisposable的对象 – 最好是在using语句中。

进一步阅读:

http://www.davidhayden.com/blog/dave/archive/2005/10/15/2517.aspx

更新:使用您的示例代码使用了一段时间之后,看起来由于来自SQL的错误,事务正在回滚并变得无法使用。 正如在另一个答案中所述,似乎与SQL一起,由于某些错误,无论保存点如何,事务都被强制回滚。 唯一可靠的方法是重新排序针对数据库运行的命令,而不依赖于保存点,或者至少不依赖于保存点中的该操作。