不,不建议使用该模式进行更新。对于您的实体和视图模型与 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 不仅仅是减少代码行数。值得花时间去熟悉。