在实体框架中保存实体的通用方法

本文关键字:实体 方法 保存 框架 | 更新日期: 2023-09-27 17:55:03

我正试图写一个GenericEFRepository,这将被其他存储库使用。我有一个保存方法如下:

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 < 1)
    {
        _dbSet.Add(entity);
        return;
    }
    var attachedEntity = _dbSet.Local.SingleOrDefault(e => e.Id == entity.Id);
    if (attachedEntity != null)
        _dbContext.Entry(attachedEntity).State = EntityState.Detached;
    entry.State = EntityState.Modified;
}

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

 using (var uow = ObjectFactory.GetInstance<IUnitOfWork>()) // uow is implemented like EFUnitOfWork which gives the DbContext instance to repositories in GetRepository
 {
    var userRepo = uow.GetRepository<IUserRepository>();
    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<IUnitOfWork>())
 {
    var userRepo = uow.GetRepository<IUserRepository>();
    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,不要在每个Entity中都放一个很长的Id,不要做你想做的事情"的注释是没有帮助的。

在实体框架中保存实体的通用方法

是的,这很容易出错,但这就是EF和存储库的问题。你必须要么创建实体并附加它之前,你设置任何数据你想要更新(Name在你的情况下),或者你必须设置修改状态为每个属性,你想要坚持,而不是整个实体(你可以想象再次开发人员可以忘记这样做)。

第一个解决方案导致在您的存储库上使用特殊方法执行以下操作:

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

第二个解决方案需要像

这样的东西
public void Save(T entity, params Expression<Func<T, TProperty>>[] properties) {
    ...
    _dbContext.Set<T>().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[]。或者每个字段对应一个bool值。我认为这不是一个好的解决方案。

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

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将简化你告诉它是当前的DB状态,除非你修改一个值或设置一个修改的标志。要在没有选择的情况下完成您所尝试的操作,您通常会附加一个对象而不包含名称,然后在附加ID对象
  • 之后设置名称
  • 您的方法通常用于性能原因,我建议通过在现有框架之上进行抽象,您几乎总是会遭受一些逻辑性能下降。如果这是一个大问题,也许你不应该使用存储库?为满足性能考虑而向存储库中添加的内容越多,它就会变得越复杂、越受限,提供多个实现也就越困难。

话虽如此,我确实认为你可以在一般情况下处理这种特殊情况。

这是一种可能的方法

public void UpdateProperty(Expression<Func<T,bool>> 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
}

希望这是有意义的,我不是我的开发箱,所以我不能真正做一个工作的例子。

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