通过 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<TSource,TDestination>
来帮助解决这个问题。
即
Mapper.Map(personVm, person);
... 以节省手动复制值。如果使用 Automapper,那么使用默认地图调用是很重要的不:
person = Mapper.Map(personVm);
这会将 'person' 设置为对未附加到 DbContext 的 Person 实体的新引用。
Automapper 可以在通过 ProjectTo<TDestination>()
方法代替 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 更新记录的方法的主要内容,如果未能解决你的问题,请参考以下文章