在Entity Framework中保存实体的通用方法

我正在尝试编写一个GenericEFRepository,它将被其他存储库使用。 我有一个Save方法如下。

public virtual void Save(T entity) // where T : class, IEntity, new() And IEntity enforces long Id { get; set; } { var entry = _dbContext.Entry(entity); if (entry.State != EntityState.Detached) return; // context already knows about entity, don't do anything if (entity.Id  e.Id == entity.Id); if (attachedEntity != null) _dbContext.Entry(attachedEntity).State = EntityState.Detached; entry.State = EntityState.Modified; } 

您可以在以下代码的注释中找到问题

  using (var uow = ObjectFactory.GetInstance()) // uow is implemented like EFUnitOfWork which gives the DbContext instance to repositories in GetRepository { var userRepo = uow.GetRepository(); var user = userRepo.Get(1); user.Name += " Updated"; userRepo.Save(user); uow.Save(); // OK only the Name of User is Updated } using (var uow = ObjectFactory.GetInstance()) { var userRepo = uow.GetRepository(); var user = new User { Id = 1, Name = "Brand New Name" }; userRepo.Save(user); uow.Save(); // NOT OK // All fields (Name, Surname, BirthDate etc.) in User are updated // which causes unassigned fields to be cleared on db } 

我能想到的唯一解决方案是通过像userRepo.CreateEntity(id: 1)这样的存储库创建实体,并且存储库将返回附加到DbContext的实体。 但这似乎很容易出错,仍然任何开发人员都可以使用new关键字创建实体。

您对此特定问题的解决方案建议是什么?

注意:我已经知道使用GenericRepository和IEntity接口的缺点和优点。 因此,“不要使用GenericRepository,不要使用IEntity,不要在每个实体中放置长ID,不要做你想做的事情”评论也无济于事。

是的,它容易出错,但这只是EF和存储库的问题。 您必须先创建实体并在设置要更新的任何数据之前附加它(在您的情况下为Name ),或者您必须为要保留的每个属性而不是整个实体设置修改状态(如您所能想象的那样,开发人员可以忘记去做)。

第一个解决方案导致您的存储库执行以下特殊方法:

 public T Create(long id) { T entity = _dbContext.Set().Create(); entity.Id = id; _dbContext.Set().Attach(entity); return entity; } 

第二个解决方案需要像

 public void Save(T entity, params Expression>[] properties) { ... _dbContext.Set().Attach(entity); if (properties.Length > 0) { foreach (var propertyAccessor in properties) { _dbContext.Entry(entity).Property(propertyAccessor).IsModified = true; } } else { _dbContext.Entry(entity).State = EntityState.Modified; } } 

你会称之为:

 userRepository(user, u => u.Name); 

这是这种方法的一个基本问题,因为您希望存储库能够神奇地知道您更改了哪些字段以及哪些字段没有更改。 如果null是有效值,则使用null作为“未更改”的信号不起作用。

您需要告诉存储库您要写入哪些字段,例如发送带字段名称的string[] 。 或每个场地一个布尔。 我不认为这是一个很好的解决方案。

也许你可以像这样反转控制流程:

 var entity = repo.Get(1); entity.Name += "x"; repo.SaveChanges(); 

这将允许变更跟踪工作。 它更接近于EF如何使用。

替代方案:

 var entity = repo.Get(1); entity.Name += "x"; repo.Save(entity); 

虽然其他两个答案提供了很好的洞察力,你可以如何避免这个问题,我认为它值得指出一些事情。

  • 你正在尝试做什么(即代理实体更新)是非常EF中心的,IMO实际上在EF上下文之外没有意义,因此通用存储库预计会以这种方式运行是没有意义的。
  • 实际上你甚至没有得到适合EF的流程,如果你附加了一个已经设置了几个字段的对象,EF会简化你告诉它的当前数据库状态,除非你修改一个值或设置一个修改过的标志。 要在没有选择的情况下执行您正在尝试的操作,通常会附加没有名称的对象,然后在附加ID对象后设置名称
  • 您的方法通常用于性能原因,我建议通过在现有框架的顶部进行抽象,您几乎总会遇到一些逻辑性能下降。 如果这是一个大问题,你可能不应该使用存储库? 为了满足性能问题,您添加到存储库的次数越多,它就越复杂和限制,并且提供多个实现就越难。

所有这一切,我认为你可以在一般情况下处理这种特殊情况。

这是一种可行的方法

 public void UpdateProperty(Expression> selector, FunctionToSetAProperty setter/*not quite sure of the correct syntax off the top of my head*/) { // look in local graph for T and see if you have an already attached version // if not attach it with your selector value set // set the property of the setter } 

希望这是有道理的,我不是我的开发箱atm所以我不能真正做一个工作样本。

我认为这对于通用存储库来说是一种更好的方法,因为它允许您以多种不同的方式实现相同的行为,abovc可能适用于EF,但如果您有内存存储库(例如),则会有不同的方法。 此方法允许您实现满足意图的不同实现,而不是将您的存储库限制为仅像EF一样。