EF:如何在事务中两次调用SaveChanges?

16

我在使用 Entity Framework(代码优先方式)时,有一个操作需要我调用 SaveChanges 来更新数据库中的一个对象,然后再次调用 SaveChanges 来更新另一个对象(我需要第一次 SaveChanges 来解决 EF 无法确定首先更新哪个对象的问题)。

我尝试过:

using (var transaction = new TransactionScope())
{
    // Do something

    db.SaveChanges();

    // Do something else

    db.SaveChanges();

    tramsaction.Complete();
}

我运行代码时,第二个SaveChanges调用会引发异常,错误信息为“底层提供程序在打开时失败”。内部异常表示我的计算机上未启用MSDTC。

现在,我已经在其他地方看到了启用MSDTC的帖子,但似乎我还需要启用网络访问等。这听起来对于这里而言完全是过度杀手,因为没有涉及其他数据库,更不用说其他服务器了。我不想做任何可能使整个应用程序不安全(或变慢)的事情。

肯定有一种更轻量级的方法可以实现这一点(最好不需要使用MSDTC)吧?!


你正在使用SQL 2008吗?根据你的实际逻辑,你可以打开多个连接而不升级。这是一篇很棒的文章,详细解释了何时会调用DTC:Transaction scope Automatically escalating to MSDTC - Mark Oreta
如果您想在一个事务中完成所有操作,那么一次性保存和多次保存有什么区别?在两种情况下,要么全部保存,要么全部不保存,因此我看不出多次保存的任何优势。根据下面的评论 - 在Sql Server 2005中,在事务中打开多个连接(即使源相同)会导致事务被提升为分布式事务。这在Sql Server 2008中得到了改进,您可以在trx中打开多个到同一数据源的连接而不会导致提升。 - Pawel
@markoreta:我正在使用SQL Server 2012。我没有(也不想要)多个连接,并且真的不想使用MSDTC! - Gary McGill
@GaryMcGill - 我假设你是在事务内而不是外部打开连接。 - Pawel
对于上面选择的最终答案,有一个打字错误。以下是更正后的代码行:objectContext.Connection.Open(); 另外,需要添加 System.Data.Entity.Infrastructure 和 System.Transactions 的引用。 - MJDConsultingGroup
显示剩余5条评论
3个回答

13

我知道这个回答有一点晚了,但我发现分享这个信息很有用。

现在在EF6中,可以通过使用dbContext.Database.BeginTransaction()更容易地实现这一点。

就像这样:

using (var context = new BloggingContext())
{
    using (var dbContextTransaction = context.Database.BeginTransaction())
    {
        try
        {
            // do your changes
            context.SaveChanges();

            // do another changes
            context.SaveChanges();

            dbContextTransaction.Commit();
        }
        catch (Exception)
        {
            dbContextTransaction.Rollback();
        }
    }
}

更多信息请查看此处

再次强调,此功能仅适用于EF6及以上版本


谢谢,这更像话了! - Gary McGill
你应该在 catch 块中做更多的事情,而不仅仅是回滚和忽略异常。记录日志、重新抛出等。 - Eric J.
@WahidBitar:try/catch块是必要的吗? using不会处理异常吗?(类似于在dbContextTransaction的Dispose方法中调用Rollback - Isaac
@Isaac 我不确定100%,但我知道dispose本身不会执行回滚。 - Wahid Bitar

8

这可能是由您在事务中使用的两个不同连接引起的。尝试手动控制操作的连接:

var objectContext = ((IObjectContextAdapter)db).ObjectContext;

try {
    //Open Connection
    objectContext.Connection.Open();

    using (var transaction = new TransactionScope()) {
        // Do something

        db.SaveChanges();

        // Do something else

        db.SaveChanges();

        transaction.Complete();
    }
} finally {
    //Close connection after commit
    objectContext.Connection.Close();
} 

如果在同一个DbContext中,EF6.0有context.Database.BeginTransaction()。但是如果操作在不同的DBContext中,则这种方法可能无法正常工作。 - Tony Bao

7
通过像您这样调用SaveChanges()会导致数据持久化到数据库,并使EF忘记刚刚所做的更改。
关键是使用SaveChanges(false),以便将更改持久化到数据库,但EF不会忘记其所做的更改,从而使日志记录/重试成为可能。
        var scope = new TransactionScope(
            TransactionScopeOption.RequiresNew,
            new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }
        );

        using (scope)
        {
            Entities context1 = new Entities();
            // Do Stuff
            context1.SaveChanges(false);

            Entities context2 = new Entities();
            // Do Stuff
            context2.SaveChanges(false);

            scope.Complete();
            context1.AcceptAllChanges();
            context2.AcceptAllChanges();
        }

顺便提一句,只要在事务范围内打开多个连接,它就会升级到DTC。


谢谢。那听起来非常有道理,尽管在我的情况下,如果出现错误,我不关心本地上下文的状态,因为在那种情况下,我将会崩溃并销毁上下文。不过,了解这一点还是很好的。 - Gary McGill
如果第一次更新操作成功了,但第二次更新操作失败了,那么你的数据是否还在数据库中保持一致性呢? - Paul Zahra
既然整个事务都被包含在TransactionScope中,那我希望(可以保证)这样! - Gary McGill
实际上,我的意思是在更新数据库时使用SaveChanges(false),它会保留当前的上下文,这样更容易确定函数出了什么问题,可以进行重试。 - Paul Zahra

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