回滚.NET事务的最佳方法是什么?

6

这个问题与我的问题相关:SQL Server和TransactionScope(使用MSDTC):偶尔无法获取连接

我正在使用.net的TransactionScope类进行一些事务编程。如果我理解正确,我可以通过将SQL调用包装在using ts as new TransactionScope()块中或使用new TransactionScope()然后在结尾处使用TransactionScope.Dispose()来在事务中执行一些SQL操作。

为了提交事务,MSDN建议使用TransactionScope.Commit()。假设我想在某些情况下回滚事务,仅仅调用TransactionScope.Dispose()而没有先调用Commit方法是否足够?这是良好的实践吗,还是应该以其他方式完成?


如果你“在某些情况下想要回滚事务”,那么你肯定不想“首先调用提交方法”。你应该调用回滚方法... - MatBailie
5个回答

9
如果您知道要回滚,则明确执行回滚操作。不能保证Dispose会回滚(如果已调用complete,在调用Dispose时将提交事务)。
关于使用using或new/Dispose的问题,它们并不等同。
using(var ts = new TransactionScope())
{
}

等同于

TransactionScope ts;
try
{
  ts = new TransactionScope();
}
finally
{
  ts.Dispose();
}

回答你的后续问题,如果你调用Dispose方法,事务将不会“挂起”,它将提交或回滚。但是,如果你像你写的那样使用new/dispose(没有finally块),当出现异常时,可能会出现dispose未被调用的情况。


6
如果你调用TransactionScope.Dispose()(通过using块或手动调用Dispose方法),它会回滚事务,除非你告诉它首先提交。通过加入Transaction.Rollback,你明确告诉其他程序员这就是你的意图。
为了清晰起见,我可能会添加这个操作在这种情况下是有意的。另一件事情是不显式添加回滚命令,你假设Dispose方法总是以这种方式运行,实际上这种情况可能发生,但这种假设是有风险的。最好明确地回滚它,而不是希望它会被自动执行。

只是一个快速的后续问题:是否存在任何情况,使得在错误的时间调用Dispose可能导致我的事务悬挂在虚空中? - Vivian River
会导致它挂起吗?可能发生的情况是,如果您调用dispose而事务尚未提交,则必须回滚准备提交的所有内容,这理论上可能会导致它在将系统恢复到就绪状态时“挂起”。 - kemiller2002
3
正如我在链接到的另一个问题中提到的那样,我的一些事务在 SQL 服务器上被“锁定”,就好像事务既没有提交也没有回滚,然后其他用户无法访问服务器上的数据。使用 TransactionScopeTransaction.Rollback() 的正确方法是什么?虽然 Rollback 是 Transaction 的成员,但它不是 TransactionScope 的成员。 - Vivian River
@VivianRiver 我有同样的问题!你最终做了什么? - Alex Gordon

3
TransactionScope类的意图,就我所知,是尽可能地让事务对应用程序开发人员来说更加易于使用,并且为此,推荐使用最少额外思考的一种方式来中止事务。根据TransactionScope的文档,我相信一个简单的Dispose是结束事务的推荐方式,如果没有提交,它将被回滚。请注意保留HTML标记。

1

我一直将TransactionScope封装在Using块中,因为如果有未处理的异常导致事务无法成功完成,Using块会自动调用Dispose。


如果你在退出using作用域之前调用了complete,事务仍然会提交。 - Rune FS
在这个文档示例中 https://learn.microsoft.com/en-us/dotnet/api/system.transactions.transactionscope?view=net-6.0#examples,`.complete()` 在 using 内部使用,并解释道:"Complete 方法提交事务。如果已经抛出异常,则不会调用 Complete 并且事务将被回滚。" - carloswm85

0

我会提供一个答案,以及我使用 EF、Unit of Work 和 Repository Pattern 实现的代码。

答案

文档中可以看到:

如果要回滚事务,则不应在事务范围内调用Complete方法。例如,您可以在范围内抛出异常。它所参与的事务将被回滚。

请查看他们在链接中分享的示例。这些示例非常有说明性(我将在下面复制其中一个示例,以防万一)。

实现示例

我的实现

这是我必须保存到数据库中的内容,来自 MVC 项目:

ERD, Entity Relationship Model, for example case, using transactions in C# programming laguange.

我在实现这个方法时遇到了以下问题:
void SaveTheThing(tracker, driver, manager, destination, transportation);

请阅读评论:

// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
    // Step 1
    int driverId = SavePerson(driver);
    int destinationId = SaveDestination(destination);

    // Step 2
    transportation.id_person_driver = driverId;
    transportation.id_destination = destinationId;
    int transportationId = SaveTransportation(transportation);

    // Step 3
    int managerId = SavePerson(manager);

    // Step 4
    tracker.id_person_manager = managerId;
    tracker.id_transportation = transportationId;
    SaveTracker(tracker);

    // Step 5
    // The Complete method commits explicitly the transaction.
    // If an exception has been thrown, then Complete is not
    // called and the transaction is rolled back.

    /* If I add an exception here, the transaction will roll-back */
    throw new Exception("Roll it back!");

    // This code becomes unreachable until you delete the Exception
    scope.Complete(); // If we get here things are looking good.
    _unitOfWork.Save(); // If we get here it is save to accept all changes.

}
// Implicitly rolls back the transaction to the DB if something goes wrong.
// Scope is disposed. Entity(ies) are not committed.

SaveWhatever(whatever)函数中,使用了SaveChanges()以及其他与DbContext和DbSet相关的方法。
并且,在控制器中(这是一个MVC项目):
try
{
    SaveTheThing(tracker, driver, manager, destination, transportation);
}
catch (TransactionAbortedException taEx)
{
    // If something wrong happens while committing the transaction,
    // it will rollback and throw this exception.
    Console.WriteLine(taEx.Message);
    return View("Edit", vm);
}
catch (Exception ex)
{
    // The thrown Exception is catched here.
    Console.WriteLine(ex.Message);
    return View("Edit", vm);
}

请记住,这种方法存在一些问题,我仍需调查:在EF或我的存储库中存在一些持久性,只要连接是打开的(我正在解决它,我会在修复后更新)。但是,是的,DB没有接收到数据并且回滚实际上已经完成。

阅读材料

示例(来自文档

// This function takes arguments for 2 connection strings and commands to create a transaction 
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the 
// transaction is rolled back. To test this code, you can connect to two different databases 
// on the same server by altering the connection string, or to another 3rd party RDBMS by 
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the 
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.   
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接