多次执行同一查询延迟执行



我正在为网球选手和他们可以参加的锦标赛构建一个非常简单的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>
}
}

此页面的控制器是锦标赛控制器。此控制器正在使用一个包含数据库上下文的管理器对象。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(。在我看来,这仍然比连续三次重复同一个查询要好,还是我错了?我还有其他办法解决这个问题吗?

非常感谢!

通过返回IEnumerable,您告诉调用方它将获得可以枚举的内容,而不会设置底层类型。例如,如果您的经理/存储库方法返回:

var result = context.Tournaments.Include(t => t.OrganizingClub).Include(t => t.Participants);
return result;

则发送回的实际上是可以枚举的EF查询。您的Razor代码每次都会有效地执行它,以获得CountAny,然后使用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();

将细节加载到内存中一次,然后CountAny将仅在内存中操作。对于这个例子来说,这本身并不是糟糕的,但值得了解这样操作的潜在后果。当你处理的数据量在一个屏幕上是可管理的(即10条记录,而不是1000条以上),那么返回一个具体的数据列表基本上没有害处。然而,随着系统的发展,基于较小数据集的设计决策可能会反噬你的屁股。例如,如果您想为结果引入分页。您希望视图的填充运行一个查询,该查询最终只加载并返回一页数据,而不是加载所有行以向视图发送一页数据。

即使是处理较小的数据集,也值得理解和利用EF的投影能力,使用Select来填充视图模型。在您的示例中,您将加载来自锦标赛、组织俱乐部和参与者的所有数据,即使您的视图只需要几个字段。这也可能为将来意外的性能命中打开大门,因为您正在序列化实体。如果我们稍后将另一个导航属性或集合添加到Tournament、Club等,并且即使该视图不需要该额外的属性/集合,简单地将Tournament发送到该视图也可能导致序列化程序";触摸";导航属性并触发延迟加载。(额外的查询)突然之间,对应用程序某个领域的新需求会对许多其他领域的性能产生影响,而这些领域你甚至都没有接触过。

查看视图代码:

<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)一起使用时,您甚至不需要担心急于加载/wInclude。它还减少了视图的有效负载大小,并有助于隐藏应用程序的整个域结构,以免人们窥探甚至篡改通过浏览器调试工具发送的数据。

在上面的例子中,我们只是在假设数据量是合理的情况下发出ToList()调用,但我们也可以很容易地让它返回IQueryable,让Razor代码与之交互。您可以映射字段以展平数据(如从OrganizingClub添加详细信息)或合并数据(如需要ParticpantCount = t.Paricipants.Count()),或者嵌套从相关数据中选择的其他视图模型。关键是要避免在返回的ViewModel中嵌入实体本身。(因为它们可以形成一颗滴答作响的定时炸弹。)

最新更新