【问题标题】:A generic way to save an entity in Entity Framework在实体框架中保存实体的通用方法
【发布时间】:2013-01-20 17:23:09
【问题描述】:

我正在尝试编写一个可供其他存储库使用的 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;
}

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

 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,不要做你想做的事情” cmets 无济于事。

【问题讨论】:

    标签: c# entity-framework repository-pattern


    【解决方案1】:

    是的,这很容易出错,但这就是 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);
    

    【讨论】:

    • 第一种方法有一个小问题,将值类型属性设置为其默认值不会被视为更改。例如,如果您保留一个失败的登录尝试计数器,并且您想将其重置为 0,那么它将不起作用。
    • 您可以在附加之前将其设置为 0 以外的值(例如 1),然后在附加后将其更改为零。
    【解决方案2】:

    这是这种方法的一个基本问题,因为您希望存储库神奇地知道您更改了哪些字段以及您没有更改哪些字段。如果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);
    

    【讨论】:

      【解决方案3】:

      虽然其他两个答案很好地了解了如何避免这个问题,但我认为值得指出几件事。

      • 您正在尝试做的事情(即代理实体更新)以 EF 为中心,而 IMO 实际上在 EF 上下文之外没有意义,因此期望通用存储库在这边。
      • 实际上,您甚至还没有为 EF 获得完全正确的流程,如果您附加一个已设置几个字段的对象,除非您修改一个值或设置一个已修改旗帜。要在没有选择的情况下执行您尝试的操作,您通常会附加一个对象没有名称,然后在附加 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
      }
      

      希望这是有道理的,我不在我的开发箱 atm 旁边,所以我不能真正做一个工作示例。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-08-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多