在MVC模式中,使用EntityObjects作为模型是一个好的实践吗?

12

我正在使用Entity Framework 5构建我的第一个MVC 4/Razor Web应用程序,并在做一些调研工作以便在做出任何设计决策之前了解更多信息。

我看到EF对象都是从EntityObject继承的,这个类似乎内置了许多有用的最佳实践内容,尤其是乐观并发处理。换句话说,如果两个人同时加载了Jane Doe的123 Maple Street记录,第一个人将她的名字改为Jane Smith,第二个人将她的地址改为321 Maple Street,那么很容易将两个更改合并到记录中而不会产生冲突,但第二个用户试图修改与第一个用户相同的字段将导致错误。

另一方面,创建轻量级的数据传输对象(Data Transfer Objects)并在服务器和客户端之间传递数据,这似乎是相当标准的做法,并且可以用作MVC框架中的模型。这对确保最小化客户端流量非常有用,但会破坏并发检查。

因此,我对使用DTO的理由表示质疑。使用DTO的原因是什么?将EntityObject作为MVC模型或在MVC模型中使用EntityObject到底有多糟糕?您提出的其他解决方案是什么,以实现如上所述的乐观并发处理?


我对并发处理很好奇...如果你不使用EntityObject,而是使用自己的模型,然后将它们转换/附加到对象上下文中,你最终会得到基本相同的并发处理吗? - Bartosz
@Bartosz - 使用DTO会破坏乐观并发检查。您仍然可以通过使用RowVersion字段来进行并发保护,但是您将无法像使用“EntityObject”那样自动合并非冲突数据。这就是我提出问题的原因。 - Shaul Behr
自从几个版本之前,EF对象不是POCO吗?我认为如果你想要“EntityObject”,你必须使用某种适配器...如果你的模型类有EF相关方法,那么这样做确实很糟糕。但是EF 5不应该这样。自从我认为4.1以来,它们使用代理而不是扩展EntityObject,正是出于这个原因 - 让它成为使用它们作为模型的良好实践。 - Pluc
@Pluc - 听起来很有趣。你有任何链接可以支持你的评论吗?如果它们是POCOs,并且没有危险将懒加载的相关数据传递到客户端,同时仍然保留负载值,那将是最好的结果。 - Shaul Behr
@Shaul - 我没有。如果我有支持我的链接,我会将其发布为答案的;)但是,请查看您的.tt和生成的.cs文件。它们是普通的POCOs。没有接口,没有基类。如果您从实体框架获取对象并检查对象的类型,则会发现类似于System.Data.Entity.DynamicProxies.Employee_5E43C6C196 [...]的内容。这是代理生成的类。但是,如果您执行完全相同的操作,但在此之前更改了数据库上下文配置(dbContext.Configuration.ProxyCreationEnabled = false;),那么您就可以获得一个漂亮的Employee实体! - Pluc
5个回答

10

=发布评论作为答案=

自从几个版本之前(不确定是哪个版本),EF对象就已经是POCO了。如果您想要一个"EntityObject",则必须使用某种适配器(我认为有一种适配器可用于应用程序迁移,但我不建议将其用作新项目的一部分)。

如果您的模型类具有EF相关方法,则确实很糟糕。但是 EF 5 不会这样。自从4.1版以来,它们使用代理而不是扩展 EntityObject,正是出于这个原因 - 使其成为使用它们作为模型的良好实践。

只需查看您的.tt和生成的.cs文件。它们是纯粹的POCO。没有接口,也没有基类。如果您从entity framework获取对象并检查对象的类型,您会发现像 System.Data.Entity.DynamicProxies.Employee_5E43C6C196[...] 这样的东西。这是代理生成的类。但是,如果您做完全相同的事情,只是在此之前更改数据库上下文配置(dbContext.Configuration.ProxyCreationEnabled = false;),您将获得一个漂亮的Employee实体!

因此,回答最初的问题,使用EF POCO作为模型是完全可以接受/良好的实践,但请确保将它们用作非持久化对象。

其他信息

您应该考虑DDD概念以及DDD兼容模式的实现,例如存储库或您感到舒适使用的任何内容。

您绝不能直接在视图中使用这些实体,持久性或非持久性。

您应该阅读有关AutoMapper的内容,以使您的生活更轻松(与存储库或独立使用很好)。它将促进从ProxyEmployee -> Employee -> ViewModel和相反的传输。

EF实体的可怕用法示例:

return View(dbContext.employees.First());

EF实体的 错误用法示例#1

Employee e = dbContext.employees.First();
return View(new Employee { name = e.name, [...] });

EF实体的错误用法#2示例:

Employee e = dbContext.employees.First();
return View(new EmployeeViewModel{ employee = e });

EF实体的正确使用示例:

Employee dbEmploye = dbContext.employees.First();
Employee e = new Employee { name = dbEmploye.name, [...] };
return View(new EmployeeViewModel { employee = e });

EF实体的良好使用示例:

Employee e = dbContext.employees.First();
EmployeeViewModel evm = Mapper.Map<Employee, EmployeeViewModel>(e);
return View(evm);

EF实体的精彩用法示例:

Employee e = employeRepository.GetFirstEmployee();
EmployeeViewModel evm = Mapper.Map<Employee, EmployeeViewModel>(e);
return View(evm);

如何查克·诺里斯会做:

return View(EmployeeViewModel.Build(employeRepository.GetFirstEmployee()));

2
如果我说“+1”给查克·诺里斯,你会标记我吗?如果是这样,那我就为整个答案说“+1”(反正这是我倾向于做的) :) - Bartosz
2
查克·诺里斯让你这么做了。(顺便说一句,我喜欢沙乌尔编辑我的帖子来纠正我的查克·诺里斯拼写错误) - Pluc
你能在你的Chuck Norris答案中发布EmployeeViewModel.Build方法吗?我猜它只是调用了映射器。另外,我对你的Chuck Norris是好事还是坏事感到困惑... :P - Ciaran Gallagher

7

当直接将EntityObject传递到视图时,我只看到了一些负面点:

  • 你需要手动进行白名单或黑名单操作以防止过度发布和大量分配
  • 很容易在视图中意外地惰性加载额外的数据,导致选择N+1问题
  • 在我个人看来,模型应该与视图中显示的信息密切相关,在大多数情况下(除了基本的CRUD操作),一个视图包含来自多个EntityObject的信息

1
那么,您如何建议在没有“EntityObject”的情况下启用乐观并发检查,就像我所描述的那样? - Shaul Behr
顺便说一句,关于意外地懒加载额外数据的注释加1。这曾经让我吃过亏。 - Shaul Behr
说实话,我从来没有遇到过不允许“最后一个胜出”解决方案的并发情况,所以我不知道在使用DTO时如何实现一些乐观的并发控制。 - Kristof Claes

3
On the other hand, it seems pretty standard practice to create lightweight Data Transfer Objects to pass data between the server and the client, and which serve as or in models for the MVC framework. This is great for ensuring minimal traffic to the client, but it screws up concurrency checking

在这里,你似乎在谈论发送到浏览器的内容。从这个意义上讲,即使你在控制器中使用EntityObject类,数据仍然必须以更基本的形式呈现给客户端并提交回来。因此,一旦进入客户端,任何并发支持都不是真正相关的。

但是,如果您将负载值发送到客户端并再次发送回来(就像发送EntityObject一样),您仍然可以获得乐观并发检查的好处。这就是我所追求的。 - Shaul Behr
不,通常情况下上下文只在网络请求的持续时间内打开,所以当客户端回传时会有一个新的DB上下文,除非你提出一些会话管理的方法? - Justin Harvey
EntityObjects 隐式地包含加载值,不是吗?或者我的前提根本就错了? - Shaul Behr
没错,您说得对,但该状态在一个网页请求和下一个请求之间不会被保留。 - Justin Harvey
但是,如果您将EntityObject传递给客户端,然后在浏览器中对其进行更新,并将相同的EntityObject发送回服务器并重新附加它,那么它不会保留加载值吗? - Shaul Behr
显示剩余3条评论

1

Dtos有几个优点,但取决于您的计划。

如果您将它们设计为平面,则以下方面更好且更容易:

  • 映射到视图模型
  • 缓存这些DTO对象,因为您的域对象可能具有大型图形并且不适合放入缓存中。 DTO可以为您提供良好的粒度以进行缓存的内容和方式
  • 简化API签名并防止意外的后期加载代理对象
  • 发送数据通过网络,请阅读stripper pattern的相关内容

但是,如果您不需要任何此类功能,则可以不使用。


0
基于目前所有答案似乎都表明:
  • EntityObjects推送到客户端是不好的实践,主要原因是您会有意无意地懒惰加载大量相关数据的风险。
  • DTO会干扰您进行乐观并发检查的能力。
我将提供自己的解决方案,并邀请您对此提出评论,看看是否认为这是一个好主意。
我们创建一个抽象的GenericDTO<T>类,其中T表示EntityObject。而不是向客户端传递一个简单的普通DTO,我们还创建了一个GenericDTOProperty<T>类,它具有两个属性:

public class GenericDTOProperty<T> {
  public T LoadValue { get; internal set; }
  public T Value { get; set; }
}

现在假设我有一个名为"Customer"的实体对象(EntityObject),其包含“ID”和“Name”属性。我会创建一个像这样的DTO类:
public class CustomerDTO : GenericDTO<Customer> {
  public GenericDTOProperty<int> ID;
  public GenericDTOProperty<string> Name;
}

不要在这里深入探讨太多猜测性的代码,我会封装代码到GenericDTO类中,该类使用反射将值从EntityObject复制到其中,并在加载时设置LoadValue ,在保存时验证是否已更改。 如果您确定每个表都将具有单个ID字段,则可以更聪明地将属性放在基类中。

这似乎是一个合理的模式吗? 它似乎足够简单-几乎太简单了...这让我担心可能有些问题,否则它应该是实际上成为框架的一部分?


2
这种方法意味着(在使用ASP.NET MVC时),您需要将LoadValues添加到隐藏字段中,然后使用自定义模型绑定器将传入的值映射到提交表单时的LoadValues和Values。这可能比您最初想象的要复杂。 - Kristof Claes
@KristofClaes - 公正的评论。你能对设计提出改进意见吗?或者有没有完全不同的替代方案,可以更高效地实现我追求的目标? - Shaul Behr
不是我脑海中的想法。这是一个棘手的主题,有许多不那么明显的陷阱。 - Kristof Claes

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