使用Entity Framework仅更新已修改字段的最佳方法

时间:2015-10-14 23:42:51

标签: asp.net-mvc entity-framework

目前我这样做:

例如:

public update(Person model)
{
    // Here model is model return from form on post
    var oldobj = db.Person.where(x=>x.ID = model.ID).SingleOrDefault();
    db.Entry(oldobj).CurrentValues.SetValues(model);
}

它有效,但是,例如,

我的表格中有50列但我在表单中只显示了25个字段(我需要部分更新我的表格,其余25列保留相同的旧值)

我知道它可以通过"逐列映射来实现#34;或者为剩余的25列创建"隐藏字段"。

只是想知道有没有优雅的方法以更少的努力和最佳的表现来做到这一点?

6 个答案:

答案 0 :(得分:8)

这是一个非常好的问题。默认情况下,我发现只要启用了更改跟踪(默认情况下,除非您将其关闭),实体框架将很好地仅将您要求更改的内容应用于数据库。

因此,如果您只对对象更改1个字段,然后调用SaveChanges(),则EF只会在您调用SaveChanges()时更新该1个字段。

这里的问题是,当您将视图模型映射到实体对象时,所有值都会被覆盖。这是我处理这个问题的方法:

在此示例中,您有一个名为Person的实体:

Person
======
Id - int
FirstName - varchar
Surname - varchar
Dob - smalldatetime

现在假设我们要创建一个只会更新Dob的视图模型,并将所有其他字段保持原样,这就是我的工作方式。

首先,创建一个视图模型:

public class PersonDobVm
{
    public int Id { get; set; }
    public DateTime Dob { get; set; }

    public void MapToModel(Person p)
    {
        p.Dob = Dob;
    }
}

现在大致如下编写代码(您必须更改它以匹配您的上下文名称等):

DataContext db = new DataContext();
Person p = db.People.FirstOrDefault();

// you would have this posted in, but we are creating it here just for illustration
var vm = new PersonDobVm
{
    Id = p.Id, // the Id you want to update
    Dob = new DateTime(2015, 1, 1)  // the new DOB for that row
};

vm.MapToModel(p);
db.SaveChanges();

在将视图模型字段分配给实体对象之前,MapToModel方法可能更复杂,并进行各种额外的检查。

无论如何,调用SaveChanges时的结果是以下SQL:

exec sp_executesql N'UPDATE [dbo].[Person]
SET [Dob] = @0
WHERE ([Id] = @1)
',N'@0 datetime2(7),@1 int',@0='2015-01-01 00:00:00',@1=1

所以你可以清楚地看到,Entity Framework没有尝试更新任何其他字段 - 只是Dob字段。

我知道在你的例子中你想避免手工编写每个作业,但我认为这是最好的方法。你把它全部收集在你的VM中,这样它就不会乱丢你的主代码,这样你就可以满足特定需求(即那里的复合类型,数据验证等)。另一种选择是使用AutoMapper,但我不认为它们是安全的。如果您在VM中使用AutoMapper并将“Dob”拼写为“Doob”,则不会将“Doob”映射到“Dob”,也不会告诉您它!它将无声地失败,用户会认为一切正常,但不会保存更改。

如果您在VM中将“Dob”拼写为“Doob”,编译器会提醒您MapToModel()正在引用“Dob”,但您的VM中只有一个名为“Doob”的属性。

我希望这会对你有所帮助。

答案 1 :(得分:4)

我发誓EntityFramework.Extended。 Nuget Link

它可以让你写:

db.Person
  .Where(x => x.ID == model.ID)
  .Update(p => new Person() 
  {
    Name = newName,
    EditCount = p.EditCount+1
  });

这很清楚地转化为SQL。

答案 2 :(得分:0)

我通过使用FormCollection在表单中列出使用过的元素来解决我的问题,并且只更改数据库中的那些列。

我在下面提供了我的代码示例;伟大的,如果它可以帮助别人

// Here 
// collection = FormCollection from Post
// model = View Model for Person

var result = db.Person.Where(x => x.ID == model.ID).SingleOrDefault();
if (result != null)
{
    List<string> formcollist = new List<string>();
    foreach (var key in collection.ToArray<string>())
    {
        // Here apply your filter code to remove system properties if any
        formcollist.Add(key);
    }
    foreach (var prop in result.GetType().GetProperties())
    {
            if( formcollist.Contains(prop.Name))
            {
                    prop.SetValue(result, model.GetType().GetProperty(prop.Name).GetValue(model, null)); 
            }
    }
    db.SaveChanges();
}

答案 3 :(得分:0)

这是我做的方法,假设新对象有更多列要更新,而我们要保留该列。

                if (theClass.ClassId == 0)
                {
                    theClass.CreatedOn = DateTime.Now;

                    context.theClasses.Add(theClass);
                }
                else {
                    var currentClass = context.theClasses.Where(c => c.ClassId == theClass.ClassId)
                        .Select(c => new TheClasses {
                            CreatedOn = c.CreatedOn
                            // Add here others fields you want to keep as the original record
                        }).FirstOrDefault();
                    theClass.CreatedOn = currentClass.CreatedOn;

                    // The new class will replace the current, all fields
                    context.theClasses.Add(theClass);
                    context.Entry(theClass).State = EntityState.Modified;
                }
                context.SaveChanges();

答案 4 :(得分:0)

我仍然没有为我的问题找到一个很好的解决方案,所以我创建了一个解决方法。加载Entity的时候,我直接复制了一份,命名为entityInit。保存实体时,我比较两者以查看真正发生了什么变化。所有未更改的属性,我设置为未更改并用数据库值填充它们。这对于我没有跟踪的实体来说是必要的:

        // load entity without tracking
        var entityWithoutTracking = Context.Person.AsNoTracking().FirstOrDefault(x => x.ID == _entity.ID); 
        var entityInit = CopyEntity(entityWithoutTracking);

        // do business logic and change entity
        entityWithoutTracking.surname = newValue;

        // for saving, find entity in context
        var entity = Context.Person.FirstOrDefault(x => x.ID == _entity.ID);
        var entry = Context.Entry(entity);
        entry.CurrentValues.SetValues(entityWithoutTracking);
        entry.State = EntityState.Modified;

        // get List of all changed properties (in my case these are all existing properties, including those which shouldn't have changed)
        var changedPropertiesList = entry.CurrentValues.PropertyNames.Where(x => entry.Property(x).IsModified).ToList();
        foreach (var checkProperty in changedPropertiesList)
        {
            try
            {
                var p1 = entityWithoutTracking.GetType().GetProperty(checkProperty).GetValue(entityWithoutTracking);
                var p2 = entityInit.GetType().GetProperty(checkProperty).GetValue(entityInit);
                if ((p1 == null && p2 == null) || p1.Equals(p2))
                {
                    entry.Property(checkProperty).CurrentValue = entry.Property(checkProperty).OriginalValue; // restore DB-Value
                    entry.Property(checkProperty).IsModified = false; // throws Exception for Primary Keys
                }
            } catch(Exception) { }
        }
        Context.SaveChanges(); // only surname will be updated

答案 5 :(得分:-1)

请尝试这种方式

public update(Person model)
{
    // Here model is model return from form on post
    var oldobj = db.Person.where(x=>x.ID = model.ID).SingleOrDefault();

    // Newly Inserted Code
    var UpdatedObj = (Person) Entity.CheckUpdateObject(oldobj, model);

    db.Entry(oldobj).CurrentValues.SetValues(UpdatedObj);
}

public static object CheckUpdateObject(object originalObj, object updateObj)
{
   foreach (var property in updateObj.GetType().GetProperties())
   {
      if (property.GetValue(updateObj, null) == null)
      {
         property.SetValue(updateObj,originalObj.GetType().GetProperty(property.Name)
         .GetValue(originalObj, null));
      }
   }
   return updateObj;
}