问题描述
我正在为网球运动员及其可以参加的锦标赛构建一个非常简单的 CRUD Web 应用程序 (ASP.NET MVC)。 在特定页面上,我想显示数据库中的所有锦标赛,顶部标题为“所有锦标赛”,括号之间是数据库中的记录数量。
我的 cshtml 看起来像这样:
@model System.Collections.Generic.IEnumerable<TMS.BL.Domain.Tournament>
@{
ViewBag.Title = "All Tournaments";
Layout = "_Layout";
}
<h3>All Tournaments (@Model.Count())</h3>
@if (!@Model.Any())
{
<p>No tournaments were found...</p>
}
else
{
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Starts</th>
<th scope="col">Ends</th>
<th scope="col">Org. Club</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@foreach (var t in Model)
{
<tr>
<td>@t.Name</td>
<td>@t.StartDate.ToString("ddd,dd/MM/yyyy")</td>
<td>@t.EndDate.ToString("ddd,dd/MM/yyyy")</td>
<td>@t.OrganizingClub.Name (@t.OrganizingClub.Province - @t.OrganizingClub.Town)</td>
<td>
<a asp-controller="Tournament" asp-action="Details" asp-route-id="@t.Id" class="btn btn-primary btn-sm">Details</a>
</td>
</tr>
}
</tbody>
</table>
}
}
这个页面的控制器是TournamentController。此控制器使用包含 dbcontext 的 Manager 对象。 GetAllTournamentsWithOrgClubAndParticipants() 方法返回一个 IEnumerable 的锦标赛对象(包括俱乐部和参赛者,但对于我的问题,这并不重要)。
public class TournamentController : Controller
{
private IManager _mgr;
public TournamentController(IManager manager)
{
_mgr = manager;
}
public IActionResult Index()
{
return View(_mgr.GetAllTournamentsWithOrgClubAndParticipants());
}
当我加载页面时,我看到同一个查询被触发了 3 次。一次用于网页标题中的 @Model.Count(),一次用于 @Model.Any() 以确定是否显示表格,一次用于 foreach 循环。现在我知道这是因为延迟执行,我可以通过在控制器类中的 GetAllTournamentsWithOrgClubAndParticipants() 后面添加一个 ToList() 来解决这个问题,但我经常听到不要使用 ToList() 方法,因为你加载所有东西的方式进入内存这样做。我对这种情况的感觉仍然比连续 3 次执行相同的查询要好,还是我错了?我还有什么办法可以解决这个问题?
非常感谢!
解决方法
通过返回 IEnumerable
您是在告诉调用者它会得到一些可以枚举的东西,它不会设置底层类型。例如,如果您的管理器/存储库方法返回:
var result = context.Tournaments.Include(t => t.OrganizingClub).Include(t => t.Participants);
return result;
然后发回的实际上是一个可以枚举的 EF 查询。您的 Razor 代码隐藏每次都会有效地执行它以获取 Count
、Any
,然后迭代您的 foreach
。这应该执行 3 个略有不同的查询。第一个是 SELECT COUNT(*) FROM...
,第二个是 IF EXISTS SELECT TOP (1) FROM...
,然后是具有相同过滤器和连接的 SELECT t.Id,t.Name,... FROM
。
将其更改为:
var result = context.Tournaments.Include(t => t.OrganizingClub).Include(t => t.Participants).ToList();
会将详细信息加载到内存中一次,然后 Count
和 Any
将只是内存中的操作。对于本示例而言,这本身并不坏,但值得了解此类操作的潜在后果。当您处理可在单个屏幕上管理的数据量(即 10 条记录,而不是 1000 条以上)时,返回具体化的数据列表基本上没有坏处。然而,随着系统的发展,基于较小数据集的设计决策可能会反过来咬你一口。例如,如果您想为结果引入分页。您希望视图的填充运行查询,最终加载并返回一页数据,而不是加载所有行以将一页发送到视图.
即使处理较小的数据集,理解和利用 EF 的投影功能使用 Select
填充视图模型也是值得的。在您的示例中,您将加载来自锦标赛、组织俱乐部和参与者的所有数据,即使您的视图只需要少数几个字段。由于您正在序列化实体,这也可能为意外的未来性能影响打开大门。如果我们稍后将另一个导航属性或集合添加到锦标赛、俱乐部等,并且即使此视图不需要该附加属性/集合,只需将锦标赛发送到视图可能会导致序列化程序“接触”该导航属性并触发延迟加载。 (额外查询)突然间,对应用程序某个领域的新要求对许多您甚至没有触及的其他领域的性能产生了影响。
查看查看代码:
<td>@t.Name</td>
<td>@t.StartDate.ToString("ddd,dd/MM/yyyy")</td>
<td>@t.EndDate.ToString("ddd,dd/MM/yyyy")</td>
<td>@t.OrganizingClub.Name (@t.OrganizingClub.Province - @t.OrganizingClub.Town)</td>
<td>
<a asp-controller="Tournament" asp-action="Details" asp-route-id="@t.Id" class="btn btn-primary btn-sm">Details</a>
</td>
我们需要锦标赛名称、开始日期、结束日期、组织俱乐部名称、省份和城镇,以及锦标赛 ID。
这可以简化为一个最小的视图模型,例如 TournamentSummaryViewModel:
[Serializable]
public class TournamentSummaryViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string OrganizingClubName { get; set; }
public string ClubProvince { get; set; }
public string ClubTown { get; set; }
}
然后进行投影:
var result = context.Tournaments
.Select( t => new TournamentSummaryViewModel
{
Id = t.Id,Name = t.Name,StartDate = t.StartDate,EndDate = t.EndDate,OrganizingClubName = t.OrganizingClub.Name,ClubProvice = t.OrganizingClub.Province,ClubTown = t.OrganizingClub.Town
}).ToList();
这样做的优点是它可以最大限度地减少对仅视图需要的列的查询。这避免了数据模型随时间变化/增长时出现的意外,因为我们不序列化实体,因此不存在延迟加载风险。当使用带有 Select
(或 Automapper 的 ProjectTo
)的投影时,您甚至不必担心急切加载 /w Include
。它还减少了视图的有效负载大小,并有助于隐藏应用程序的整体域结构,防止人们窥视甚至篡改通过浏览器调试工具发送的数据。
在上面的例子中,我们只是在假设数据量合理的情况下发出 ToList()
调用,但我们可以很容易地让它返回 IQueryable
以便 Razor 代码与之交互.您可以映射字段以扁平化数据(例如添加来自 OrganizingClub 的详细信息)或合并数据(例如您是否需要 ParticpantCount = t.Paricipants.Count()
),或者嵌套从相关数据中选择的其他视图模型。关键是避免将实体本身嵌入到返回的 ViewModel 中。 (因为它们可以形成一个滴答作响的定时炸弹。)