通过 ViewModel 使用 Entity Framework 更新记录的方法

Posted

技术标签:

【中文标题】通过 ViewModel 使用 Entity Framework 更新记录的方法【英文标题】:Way to update record with Entity Framework via ViewModel 【发布时间】:2022-01-18 01:42:19 【问题描述】:

看看这个实体和他关联的视图模型类:

class MyEntity

    public int Id  get; set; 
    public String Name  get; set; 
    ...


class MyViewModel

    public int Id  get; set; 
    public String Name  get; set; 
    ...

当我想更新记录时,我会这样做:

var jack = MyDbContext.MyEntity.FirstOrDefault(e => e.Id=2);
jack.Name = "New name";
MyDbContext.SaveChanges();

效果很好!

现在,我想知道在使用视图模型时应该如何做同样的事情:

var all = MyDbContext.MyEntity.Select(e => new MyViewModel
  
    Id = e.Id,
    Name = e.Name,
    ...
  ;
// Let's suppose I have a grid with all records.
// User may click on one record in order to see the details
var jackvm = all[1];  // For example jack !
jackvm.Name = "New Name";

在这一步,我无法调用 SaveChanges,因为 jackvm 不是附加实体。它是一个视图模型实例。 这是我在一个项目中看到的,我想知道这样工作是否是一种好习惯:

  var jackEntity = new MyEntity  Id = jackvm.Id, Name = jackvm.Name, ... ;
  MyDbContext.Entry(jackEntity).State = EntityState.Modified;
  MyDbContext.SaveChanges();

如您所见,实体是根据视图模型的实例值创建的。然后,将该实体附加到 dbcontext。 SaveChanges 将更新数据库中的记录。我的第一个问题是您如何看待这种模式?这是一个好习惯吗?

我有第二个问题:我需要记录每个写操作。我已经重写 SaveChanges 以跟踪更改,这样:

public override int SaveChanges()
    
  foreach (var entry in this.ChangeTracker.Entries())
  
     if (entry.State == EntityState.Modified)
     
        Console.WriteLine(entry.OriginalValues.GetValue<String>("Name"));        
        Console.WriteLine(entry.CurrentValues.GetValue<String>("Name"));        
     
  
  return base.SaveChanges();

它适用于我的第一个示例。 但是对于第二个示例, OrignalValues 包含新值! 在这种情况下,如何获取 name 字段的原始值? 请不要我想避免进行选择查询。

非常感谢

【问题讨论】:

你检查过 Automapper 吗? 是的,但 automapper 只是一个避免线条的助手。结果相同 【参考方案1】:

不,不建议使用该模式进行更新。对于您的实体和视图模型与 all 列具有一对一关系的简单场景,它可以正常工作,但一旦您移动到更复杂的实体,它就会崩溃。以这样的实体为例:

public class Person

    public int PersonId  get; set; 
    public string Name  get; set; 
    public DateTime DoB  get; set; 
    public virtual Address Address  get; set; 

和一个视图模型

[Serializable]
public class PersonSummaryVm

    public int PersonId  get; set; 
    public string Name  get; set; 
    public DateTime DoB  get; set; 
    public int Age => DateTime.Now.Subtract(DoB).TotalYears;
    public string Address  get; set; 

为了争论,这个视图模型是一个投影,我们想要显示他们的年龄而不是他们的 DoB,我们将地址展平。假设我们有一个操作,用户可以更新他们的一些详细信息。即使上面是一个非常简单的示例,但想象一个具有 50 多个列和关系的实体。我们可以传回一个 PersonSummaryVm,或者我们可以传回类似 UpdatePersonVm 之类的东西,其中只包含我们想要更新的值,或者如果我们只是想要一个 UpdatePersonName 方法,我们可以只将 PersonId 和 Name 传递给函数。

让我们使用最后一个示例来演示这种方法的一些问题,无论您传递什么视图模型或字段,这些问题都将适用,除非您传递所有内容

[HttpPost]
public JsonResult UpdatePersonName(int personId, string name)

    try
    
        var person = new Person  PersonId = personId, Name = name ;
        _context.Attach(person);
        _context.Entry(person).State = EntityState.Modified;
        _context.SaveChanges();

        return Json(ActionResponse.Success()); // Return ActionResult or View etc.
     
     catch (Exception ex)
     
         var logId = Logger.LogException(ex, personId);
         return Json(ActionResponse.Failure($"Person could not be updated due to an error. (Ref #logId)"));
      

代码一般会执行,但这里有几个重大问题。

    没有验证具有该 personId 的 Person 记录确实存在,或者它处于应该允许编辑的状态。 当我们创建一个新的 Person 对象时,DoB 将默认为 DateTime.Min() (1/1/0001),并且地址引用将为空。当我们保存该 Person 时,值将被覆盖,因为我们没有填充它们,因此 EF 将部分填充的实体视为当前状态。

即使您使用的视图模型与您暴露问题的实体具有相同的字段:

    您在视图/消费者之间传入和传出的数据比该视图可能永远需要的数据更多,或者他们甚至应该看到的数据。 因为您从视图/消费者传回每个字段,即使您只显示了一些字段并允许他们编辑一些字段,其余数据对于任何拥有浏览器调试器正在运行。 没有检测或保护过时数据覆盖。自从该消费者检索到您要更新的源数据后,数据是否发生了变化? 随着系统的发展以及您向实体添加新属性和关系,您的视图模型可能会不同步。视图不一定需要显示新的数据/关系,但您的更新方法现在只会复制它知道的数据,从而导致数据被擦除,这意味着只需通过网络扩展视图模型和数据以满足更新场景。

更好的方法是获取您的实体,验证编辑是否有效并更新值。

[HttpPost]
public JsonResult UpdatePersonName(UpdatePersonVm personVm)

    try
    
        if ( personVm == null) throw new ArgumentNullException("personVm");

        var person = _context.Persons.Single(x => x.PersonId == personVm.PersonId);
        if (personVm.RowVersion != person.RowVersion)
            return Json(ActionResponse.StaleUpdate(Mapper.Map<PersonSummaryVm>(person)));

        person.Name = personVm.Name;
        _context.SaveChanges();

        return Json(ActionResponse.Success(Mapper.Map<PersonSummaryVm>(person))); // Return ActionResult or View etc.
     
     catch (Exception ex)
     
         var logId = Logger.LogException(ex, personId);
         return Json(ActionResponse.Failure($"Person could not be updated due to an error. (Ref #logId)"));
      

使用这种方法,您可以验证 Person 确实存在。如果找不到此人,则可以选择执行SingleOrDefault 以向消费者提供更好的失败消息。实体/表和视图模型都可以包含一个 RowVersion 标记,该标记会随着行的更新而自动更新。当我们获取传递给客户端进行更新的 VM 时,它会有一个特定的 RowVersion,该用户可能会在发布更新操作之前花费 10 分钟进行更改,在此期间其他人可能已经更新了它。我们可以比较行版本并处理它们可能覆盖已更改内容的情况。现在,因为我们已经加载了一个实体,我们可以只复制预期已更改的值,并且仅当其中任何一个实际更改时,EF 才会为这些修改的值生成 UPDATE 语句。我们可以在这里使用 Automapper 的Map&lt;TSource,TDestination&gt; 来帮助解决这个问题。

Mapper.Map(personVm, person);

... 以节省手动复制值。如果使用 Automapper,那么使用默认地图调用是很重要的

person = Mapper.Map(personVm);

这会将 'person' 设置为对未附加到 DbContext 的 Person 实体的新引用。

Automapper 可以在通过 ProjectTo&lt;TDestination&gt;() 方法代替 Select() 读取数据时提供帮助。这有助于构建高效的查询:

var personVm = context.Persons
    .Where(x => x.PersonId == personId)
    .ProjectTo<PersonSummaryVm>(config)
    .Single();

其中传递的映射器配置包含有关如何将实体映射到视图模型的配置。例如扁平化地址,而不是:

var personVm = context.Persons
    .Where(x => x.PersonId == personId)
    .Select(x => new PersonSummaryVm
    
        PersonId = x.PersonId,
        Name = x.Name,
        DoB = x.DoB,
        Address = x.Address.AddressLine1 + ", " + x.Address.City
        RowVersion = x.RowVersion
    ).Single();

另一个性能缺陷 /w Automapper 将尝试在 Select() 中使用 Map()

var personVm = context.Persons
    .Where(x => x.PersonId == personId)
    .ToList()
    .Select(x => Mapper.Map<PersonSummaryVm>(x))
    .Single();

使用.Map()的问题是它不能被翻译成SQL,所以你需要把ToList().AsEnumerable()放在Select前面,然后在上面的情况下,因为Mapper会想要解析属性从 Address 我们将触发对 Address 的延迟加载,或者必须记住急切加载 Person.Address。即使在这种情况下,Select 也会从 Person 加载 all 属性以及 Map 在映射我们的 VM 之前需要触及的引用实体,其中 ProjectTo 将能够仅在生成的 SQL。

所以 Automapper 不仅仅是减少代码行数。值得花时间去熟悉。

【讨论】:

以上是关于通过 ViewModel 使用 Entity Framework 更新记录的方法的主要内容,如果未能解决你的问题,请参考以下文章

EF中更新操作 ID自增但不是主键 ;根据ViewModel更新实体的部分属性

将viewModel对象映射到ICollection实体

3AutoMapper In Asp.net Core

3AutoMapper In Asp.net Core

对ViewModel自定义约束

cesium01_加载影像-BaseLayerPicker使用