实体框架中的部分更新验证

23

我正在使用带有DbContext和POCO实体的Entity Framework 5.0。有一个简单实体包含3个属性:

public class Record
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsActive { get; set; }
}

标题字段始终不可更改,UI仅显示它而不提供任何输入框以修改它,这就是为什么当表单发送到服务器时,标题字段被设置为null的原因。

以下是我告诉EF如何执行实体(仅针对IsActive字段)的部分更新:

public class EFRepository<TEntity>
{
   ...
   public void PartialUpdate(TEntity entity, params Expression<Func<TEntity, object>>[] propsToUpdate)
   {
       dbSet.Attach(entity);
       var entry = _dbContext.Entry(entity);
       foreach(var prop in propsToUpdate)
           contextEntry.Property(prop).IsModified = true;
   }
}

调用代码:

repository.PartialUpdate(updatedRecord, r => r.IsActive);

调用SaveChanges方法时,我遇到了DbEntityValidationException异常,告诉我Title是必需的。当我设置dbContext.Configuration.ValidateOnSaveEnabled = false时,一切都正常。 是否有办法避免在整个上下文中禁用验证,并告诉EF不要验证未更新的属性? 提前致谢。


1
这似乎是很多工作,来处理本来很简单的事情。你只需要在表单上包含一个隐藏字段,其中包含只读模型项,然后它们将被包含在更新中,EF会进行更改跟踪,并知道该值未发生变化。 - Erik Funkenbusch
2
那么存根实体呢?例如,我有一个操作方法将实体标记为已删除。以下代码:var person = new Person { Id = 5 }; dbSet.Attach(person); dbSet.Entry(person).Property(p => p.IsDeleted).IsModified = true; dbContext.SaveChanges();也会导致相同的异常。DbContext验证是否能够很好地处理存根实体?我想避免仅为标记而从数据库中检索整个实体。 - Skog
它真的有效吗?当您手头有一个实体并且确定该实体存在于数据库中且是相同的时,使用Attach。您附加了一个与存储中不同的实体(标题不同)。然后,您将实体标记为已修改(通过标记要修改的属性来标记)。由于EF操作的是实体而不是属性,因此它将更新所有属性而不仅仅是标记为已修改的属性。我对EF的心理模型告诉我,在此之后,数据库中的Title列将设置为null。您能否检查一下这是否正确? - Pawel
1
EF允许对实体进行部分更新。例如,dbSet.Attach(entity); dbContext.Entry(entity).State = EntityState.Modified; dbContext.SaveChanges();将更新整个实体。如果您明确告诉EF要更新哪些属性,则只会更新这些属性。dbSet.Attach(entity); dbContext.Entry(entity).Property(e => e.Title).IsModified = true; dbContext.SaveChanges();将仅更新标题。在禁用验证的情况下,这可以很好地工作。 - Skog
通过这种方法(明确告诉EF要更新哪些属性),我甚至可以更新另一个属性(entity.IsActive = false),但是目前我没有将其标记为已更新(e => e.IsActive).IsModified = true;),因此它不会被更新。entity.State = EntityState.Modified一次性将整个实体标记为已更新。 - Skog
显示剩余5条评论
3个回答

25
如果您使用部分更新或存根实体(这两种方法都很有效!),则无法使用全局EF验证,因为它不会尊重您的部分更改-它总是验证整个实体。使用默认验证逻辑,您必须通过调用上述内容来关闭它:
dbContext.Configuration.ValidateOnSaveEnabled = false

并且单独验证每个更新的属性。这样应该会奏效,但我没有尝试过,因为我根本不使用EF验证:

foreach(var prop in propsToUpdate) {
    var errors = contextEntry.Property(prop).GetValidationErrors();
    if (erros.Count == 0) {
        contextEntry.Property(prop).IsModified = true;
    } else {
        ...
    }
}

如果您想更进一步,可以尝试在上下文中覆盖 ValidateEntity 并以验证整个实体或仅基于实体状态和属性的 IsModified 状态验证所选属性的方式重新实现验证,这将允许您使用带有部分更新和存根实体的EF验证。
在EF中进行验证是错误的概念 - 它引入了额外的逻辑到数据访问层中,而这种逻辑并不属于其中。它主要基于这样一个想法,即您始终与整个实体甚至整个实体图一起工作,如果您在导航属性上放置必需的验证规则,则会使其生效。一旦您违反了此方法,您将发现单个固定的验证规则集硬编码到实体中是不足够的。
我非常长的待办事项清单中的一件事情是调查验证如何影响 SaveChanges 操作的速度 - 我曾经在EF4(在EF4.1之前)中使用过自己的验证API,基于DataAnnotations及其 Validator 类,但由于性能非常差,我很快就停止使用它。
使用本机SQL的解决方法与使用关闭验证的存根实体或部分更新具有相同的效果 = 您的实体仍未经过验证,但除此之外,您的更改不是同一工作单元的一部分。

1
@Pascal:如果它们的值没有改变,你应该填充那个值(这样默认值就不会被使用),或者你应该使用部分更新,并且不将它们作为更新传递到数据库中。 - Ladislav Mrnka
选项1:我不想填充那个值,因为这样我就必须为每个要更新的实体再次去数据库查询。选项2:我不明白,请示一下代码或者给出更详细的解释。我使用了部分更新,加上了 property.IsModified = true; 还需要什么? - Pascal
1
@Pascal:我的意思是,如果您不想将这些默认值发送回数据库,请不要将这些属性标记为已修改。 - Ladislav Mrnka
但是我不会将那些属性标记为已修改。我只会设置一个属性IsModified = true而已。 - Pascal
@Pascal:我可能误解了你的问题。所以你的其他属性被设置为默认值-但只在应用程序中(而不是在数据库中),这正确吗?这些属性可为空吗?如果不行,它们必须有值,因此您需要明确设置该值,否则将使用默认值。 - Ladislav Mrnka
显示剩余4条评论

20

参考 Ladislav的回答,我已经将这段代码添加到DbContext类中,现在它可以删除未修改的所有属性。虽然它并没有完全跳过这些属性的验证,只是省略了它们,但EF是按实体而不是属性进行验证的,重新编写整个验证过程对我来说太麻烦了。

protected override DbEntityValidationResult ValidateEntity(
  DbEntityEntry entityEntry,
  IDictionary<object, object> items)
{
  var result = base.ValidateEntity(entityEntry, items);
  var falseErrors = result.ValidationErrors
    .Where(error =>
    {
      if (entityEntry.State != EntityState.Modified) return false;
      var member = entityEntry.Member(error.PropertyName);
      var property = member as DbPropertyEntry;
      if (property != null)
        return !property.IsModified;
      else
        return false;//not false err;
    });

  foreach (var error in falseErrors.ToArray())
    result.ValidationErrors.Remove(error);
  return result;
}

1
谢谢!这是我找到的最优雅的方法,但我建议在Where中添加此条件 if (entityEntry.State != EntityState.Modified) return false;,以确保仅在更新实体时删除falseErrors,否则当添加验证时将全部删除,因为未设置IsModified标志。 - Michael Denny
жҲ‘еҸӘжҳҜеңЁ var falseErrors е’Ң foreach еүҚеҠ дәҶдёҖдёӘ if (entityEntry.State == EntityState.Modified) жқҘйҳІжӯў Michael Denny жҸҸиҝ°зҡ„иЎҢдёәгҖӮиҝҷжҜ”еңЁ Linq жҹҘиҜўдёӯжү§иЎҢжӣҙеҘҪгҖӮ - Marcos Lima
嗨Shimmy,谢谢你提供这个片段。想知道你是否知道EF6.1仍然需要这个吗?我刚刚查了一下,EF6在2013年就发布了,但自去年4月以来EF方面似乎已经有所改进了? - Jono
@MichaelDenny 我已添加了它。我不再使用它,所以我不知道它是否有效。 - Shimmy Weitzhandler

3
这是对先前@Shimmy回答的改编版,是我目前使用的版本。
我添加了一个子句(entityEntry.State != EntityState.Modified) return false;Where中:
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
    var result = base.ValidateEntity(entityEntry, items);

    var falseErrors = result
        .ValidationErrors
        .Where(error =>
        {
            if (entityEntry.State != EntityState.Modified) return false;
            var member = entityEntry.Member(error.PropertyName);
            var property = member as DbPropertyEntry;
            if (property != null) return !property.IsModified;
            return false;
        });

    foreach (var error in falseErrors.ToArray())
    {
        result.ValidationErrors.Remove(error);
    }

    return result;
}

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