Entity Framework 中多次调用 SaveChanges 的问题

36
我正在构建一个基于实体框架的自定义仓库,并创建了一些扩展方法,使我可以将部分视图模型保存为实体模型,因此我正在构建自己的Add和Update方法。
目前,每个方法都会在末尾调用DbContext中的SaveChanges(),这意味着对于每个模型,都会被调用一次。
我正在为MVC4站点构建这个基础DAL模式,这意味着大多数情况下我将访问1个模型,但并不一定是这种情况。
当更新3个实体时,每个模型都调用SaveChanges()是否不好的做法,或者我应该先将所有内容添加到对象上下文中,然后执行SaveChanges()作为某种事务提交?
5个回答

90

我知道答案有点晚,但我认为分享还是有用的。

现在在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 ex)
        {
            //Log, handle or absorbe I don't care ^_^
        }
    }
}

更多信息请查看此链接

这是在EF6及以后版本中实现的。


22
发生异常时无需调用回滚,系统会自动调用。 - Trilok Chandra
14
因为 using 语句会在其作用范围结束时释放事务,如果在此之前没有成功调用 Commit 方法,则会将事务回滚。 - JohnnyHK
这是证明:https://dev59.com/d2Eh5IYBdhLWcg3wYSg2 - nmit026
1
@DFTR 这是一个非常笼统的评论 - 你能具体说明在Azure上有什么问题吗? - Phil Seeman
2
如果您的逻辑不需要,我建议避免使用这种模式。在事务之间执行步骤将导致服务器不必要地持有对象锁。通过每个工作单元仅有一次SaveChanges()调用来减少此影响。 - binki
显示剩余4条评论

9

在相关实体应该在单个事务中持久化时,调用SaveChanges多次(没有事务范围)是一个不好的做法。你所创建的是一个有缺陷的抽象。可以创建一个单独的工作单元类或直接使用ObjectContext/DbContext本身。


4
或者使用TransactionScope。 - Elisabeth
3
有时候,在处理遗留数据库时,你就没有其他选择了,只能多次调用“SaveChanges”。例如,你的数据库有一个存储过程或触发器生成一个非常特定格式的标识符,你需要在工作单元的中间获取该标识符,以便将其分配给其他实体。因此,你需要先调用“SaveChanges”将更改持久化到数据库并获取该标识符,再次调用“SaveChanges”以保存最终实体。 - JustAMartin
@JustAMartin 说得好。然而,糟糕的遗留技术不应该驱动糟糕的架构选择。在上面的例子中,如果第二次保存崩溃或服务器在保存过程中重新启动,数据库将处于不一致状态。应用程序必须足够智能,始终保持数据库干净和一致。 - GETah
1
@GETah 当然,我们在所有 SaveChanges 调用周围使用显式事务,尽可能避免不一致性。不幸的是,我生活在一个预算往往胜过质量的国家,特别是在大型政府项目中。客户不接受改进遗留数据结构的想法,因为这可能需要重新设计与其他系统的现有集成,这将导致未计划的费用。我们的应用程序必须使用已经存在的数据,并尽可能少地进行更改。 - JustAMartin

3

这是我目前使用的另一种处理多个 context.SaveChanges() 的 UnitOfWork 方法。

我们将 context.SaveChanges() 方法暂存,直到最后一个被调用时再一次性保存。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace DataAccess
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly Context context;
        private readonly Dictionary<Type, object> repositories = new Dictionary<Type, object>();

        private int beginChangeCount;
        private bool selfManagedTransaction = true;

        public UnitOfWork(Context context)
        {
            this.context = context;
        }     

        //Use generic repo or init the instance of your repos here
        public IGenericRepository<TEntity> GetRepository<TEntity>() where TEntity : BaseEntityModel
        {
            if (repositories.Keys.Contains(typeof(TEntity)))
                return repositories[typeof(TEntity)] as IGenericRepository<TEntity>;

            var repository = new Repository<TEntity>(context);
            repositories.Add(typeof(TEntity), repository);

            return repository;
        }

        public void SaveChanges()
        {           
            if (selfManagedTransaction)
            {
                CommitChanges();
            }
        }

        public void BeginChanges()
        {
            selfManagedTransaction = false;
            Interlocked.Increment(ref beginChangeCount);
        }

        public void CommitChanges()
        {
            if (Interlocked.Decrement(ref beginChangeCount) > 0)
            {
                return;
            }

            beginChangeCount = 0;
            context.SaveChanges();
            selfManagedTransaction = true;
        }
    }
}

使用示例。

在下面的代码中查找我的注释

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;

namespace BusinessServices.Domain
{
    public class AService : BaseBusinessService, IAService
    {
        private readonly IBService BService;
        private readonly ICService CService;
        private readonly IUnitOfWork uow;

        public AService (IBService BService, ICService CService, IUnitOfWork uow)
        {
            this.BService = BService;
            this.CService = CService;
            this.uow = uow;
        }

        public void DoSomeThingComplicated()
        {
            uow.BeginChanges();

            //Create object B - already have uow.SaveChanges() inside
            //still not save to database yet
            BService.CreateB();

            //Create object C  - already have uow.SaveChanges() inside
            //still not save to databse yet
            CService.CreateC();

            //if there are no exceptions, all data will be saved in database
            //else nothing in database
            uow.CommitChanges();

        }
    }
}

3

10
我熟悉UnitOfWork模式,但我认为它已经过时了,因为DbContext本身就是UnitOfWork(将鼠标悬停在上面并阅读说明即可)。实现UnitOfWork是对相同目的的现有抽象化的抽象化,这只会让事情变得更加复杂,而不是更好。此外,我的模型验证是在控制器层面上完成的,而不是在数据库提交时完成的,因此在执行Add / Update之前可以确保数据有效。我的担心主要是性能问题,即将所有内容作为一个事务发送还是分别发送多个小事务是否有区别。 - Admir Tuzović
2
是的,DbContext是一个工作单元并且是可被释放的。但是如果你想要最小化数据库调用,那么你需要实现一个工作单元方法。你的IUnitOfWork将有一个SaveChanges方法,而你的UnitOfWork将继承IUnitOfWorkIDisposable - Colin Bacon

0

在这种情况下,建议采用此处所述的新的现代方法。

如果您熟悉TransactionScope类,则已经知道如何使用DbContextScope。它们在本质上非常相似 - 唯一的区别是DbContextScope创建和管理DbContext实例而不是数据库事务。但与TransactionScope一样,DbContextScope是环境感知的,可以嵌套,可以禁用其嵌套行为,并且可以很好地处理异步执行流程。

public void MarkUserAsPremium(Guid userId)  
{
    using (var dbContextScope = _dbContextScopeFactory.Create())
    {
        var user = _userRepository.Get(userId);
        user.IsPremiumUser = true;
        dbContextScope.SaveChanges();
    }
}

DbContextScope 中,您可以通过两种方式访问范围管理的 DbContext 实例。您可以通过以下方式通过 DbContextScope.DbContexts 属性获取它们:
public void SomeServiceMethod(Guid userId)  
{
    using (var dbContextScope = _dbContextScopeFactory.Create())
    {
        var user = dbContextScope.DbContexts.Get<MyDbContext>.Set<User>.Find(userId);
        [...]
        dbContextScope.SaveChanges();
    }
}

但这当然只在创建DbContextScope的方法中可用。如果您需要在任何其他地方访问环境DbContext实例(例如在存储库类中),您可以依赖于IAmbientDbContextLocator,使用方式如下:

public class UserRepository : IUserRepository  
{
    private readonly IAmbientDbContextLocator _contextLocator;

    public UserRepository(IAmbientDbContextLocator contextLocator)
    {
        if (contextLocator == null) throw new ArgumentNullException("contextLocator");
        _contextLocator = contextLocator;
    }

    public User Get(Guid userId)
    {
        return _contextLocator.Get<MyDbContext>.Set<User>().Find(userId);
    }
}

2
为什么你在构造函数中检查(contextLocator == null)?你开玩笑吗? - ZOXEXIVO
IAmbientDbContextLocator这个接口应该是Singleton还是Transient?在.NET Core中。 - rock_walker

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