是否在Asp.net MVC中延期执行查看非常糟糕的事情?

假设我有通过entity framework获得的以下模型:

public class User{ public string Name {get;set;} public int Id {get;set;} } 

从用户表中获取所有用户:

 IEnumerable users = from u in myDbContext.Users select u; 

在View中重新查看用户(@model IEnumerable):

 @foreach(var item in Model) { @item.Id @item.Name } 

我的新老板告诉我,这样做是不符合MVC,性能会非常糟糕。 我应该做的是使用.ToList()将Controller中的所有USERS资格化。 我不确定什么是更好的,因为无论哪种方式用户必须实现,我没有看到任何性能损失。

延迟执行意味着,在您开始访问users集合中的项目时(在foreach循环中迭代这些项目时),将不会执行LINQ表达式后面的实际SQL查询。 这意味着,如果您尝试访问User实体上的导航属性(并且它是唯一记录),则会执行SELECT查询

如果您只有一个用户表(就像您在问题中显示的那样)没有任何外键/移动属性,使用上面的代码,EF将只执行一个查询来从您的User表中获取数据。 但通常情况并非如此,您可能拥有User表中许多不同表的外键。 在这种情况下,entity framework会做不同的事情。

我们来举个例子吧。

假设我们有第二个表, UserType有4列, IdNameCodeIsAdmin ,你的User表有第三列名为UserTypeId ,它有一个外键给这个新的UserType表。 每个用户记录都与UserType记录相关联。

现在,您要显示具有UserType名称的所有用户。 让我们看看不同的方法。

1.延期执行,

您将执行此代码以获取所有用户

 var users = dbContext.Users; 

并将用户传递到剃刀视图,您将遍历该集合。

 @model IEnumerable @foreach (var user in Model) { 
@user.Name - @user.UserType.Name
}

当我们执行此页面时,Entity框架将在User表上运行1个select查询以获取用户记录以及UserTypeId并且当执行foreach块时,它将从UserType表中查询每个唯一的UserTypeId原始结果集( users )。 如果运行SQL事件探查器,则可以看到此信息。

在此处输入图像描述

您可以看到EF正在传递UserTypeId (我的图片中为2)。 我在User表中使用了3个不同的UserTypeId,因此它查询了UserType表3次,每个UserTypeId

执行的SQL查询数:4 (用户表为1,UserType表为3)

我在UserType表中有3个不同的记录,我使用了我的用户表中的所有记录。

2.查询数据时使用Include方法

Include关键字用于实现预先加载。 预先加载是一种过程,其中对一种类型的实体的查询也将相关实体作为查询的一部分加载。

 var users = dbContext.Users.Include(s=>s.UserType); 

在这里,您要告诉Entity框架从UserType表和User表中查询。 entity framework将在两个表之间生成INNER JOIN sql查询并执行它。

在此处输入图像描述

你可以看到,它查询了UserType表的所​​有列)

执行的SQL查询数 :1

更好的解决方案?

通常,在其他层中使用Entity框架生成的实体类并不是一个好主意。 这使您的代码紧密耦合。 我建议只查询所需的数据(列)并将其映射到POCO类(简单的DTO)并在视图/其他层中使用它。 您可以将这些DTO类保存在可以在其他项目中引用的公共项目中(您的数据访问项目和UI项目)

 public class UserDto { public int Id {get;set;} public string Name {get;set;} public UserTypeDto UserType { set; get; } } public class UserTypeDto { public int Id { set; get; } public string Name { set; get; } } 

我们的视图将绑定到用户entityUserDto集合

 @model IEnumerable @foreach (var user in Model) { 
@user.Name - @user.UserType.Name
}

现在,从您的数据访问层,您将返回UserDto的集合,而不是Entity框架创建的User实体。

 var users = dbContext.Users.Select(s => new UserDto { Id = s.Id, Name = s.Name, UserType = new UserTypeDto { Id = s.UserType.Id, Name = s.UserType.Name } }); 

在这里,您可以看到我们正在使用Select子句告诉EF我们真正需要哪些列。 EF将执行INNER JOIN ,但只有那些列,我们指定

在此处输入图像描述

执行的SQL查询数 :1

这种方法的好处是,如果您想要将数据访问实现从Entity框架切换到其他技术(Pure ADO.NET / NHibernate),出于您自己的原因,您将只更新您的GetUsers方法,所有其他层(您的Razor视图/其他业务层代码等。)不需要更新,因为它们不使用entity framework创建的实体。

如果执行ToList() ,EF立即执行SQL并返回结果。 但是如果你不这样做,因为延迟执行,它将在实际需要数据时执行SQL语句(例如:你在循环中的视图中呈现一些属性值)。

可能会导致性能问题,但并不总是导致它们。

如果将IEnumerable传递给视图,则视图不一定了解有关后备数据源的任何信息。 可能存在一个未说出口的假设,即消费代码(视图)将仅枚举该集合一次。 如果多次枚举集合,则可能会多次查询后备数据存储。

请注意,这并不一定意味着多次访问数据库,但枚举一个集合可能带有各种隐藏的“陷阱”。

在这方面,将IList传递给视图更加明确。 两个接口之间的语义差异在于IList是一个完整的列表,而不仅仅是一个模糊的枚举。 预计在没有性能损失的情况下,可以根据需要多次查询和枚举。 (当然不能保证实现类型提供此function,但预计会这样做。)

这不是“延期执行”的问题。 查询将在非常接近的同时执行服务器端,无论是在控制器中还是在视图中。 它不会被推迟很长时间。 这只是消费代码如何处理枚举的问题。

我的新老板告诉我,这样做是不符合MVC,性能会非常糟糕。

你的新老板真的应该更好地解释一下。 这与MVC无关,与IEnumerableIList ,以及LINQ如何表现。 在使用这些技术的任何应用程序中也是如此。 您将模型从控制器传递到视图的事实使得这个MVC成为现实。 如果你不知道为什么这样,那么表现可能会很糟糕。 然而,通过测量它,只有一种真正的方式来了解性能。

你正在做什么它是一个“快速和肮脏”的解决方案。 它很容易做到并且有效,但是如果你想做“正确”的代码它有一些缺点:

您基本上是从视图进行数据库查询,如果您正在执行分层应用程序,那么绝对不是这个地方。 您应该在控制器中为简单应用程序或应用程序服务甚至存储库执行数据库查询,以获得更结构化和复杂的应用程序。

在视图中实现查询在exception处理中也存在问题。 数据库exception不会在控制器中抛出,因此您无法在控制器或操作filter中处理它们。

在性能方面,我看不出它有什么不同。 你应该避免使用延迟加载。 如果您的用户实体具有依赖实体或集合,则EF将为每个用户触发额外查询,这将真正影响性能。 理想情况下,您应该禁用延迟加载。 如果遵循将所有数据发送到视图的规则(无延迟查询),则还包括预先加载所有相关实体。

按照自己的方式,至少可以延长风险的生命周期(从不确定):

  • db lock(db上的跨上下文并发);
  • 连接池故障:当处理上下文时,连接被返回到池
  • 您的视图中的“上下文不再存在”exception

保持您的上下文范围尽可能短。

顺便说一句,从mvc的角度来看,没有理由为视图和数据访问使用相同的模型。 这不是性能问题,而是关注问题的分离,即可维护性问题。