用PagedList分页,效率高吗?
Paging with PagedList, is it efficient?
我尝试实现分页已经有一段时间了,我发现了这个使用 MVC 进行分页的教程:ASP.NET MVC Paging Done Perfectly
现在,在此解决方案中,我在数据库中查询 整组客户 ,然后我 return 分页的客户列表而不是普通列表。
我觉得这很烦人,因为我只打算每页显示 10 或 20 个条目,而我的数据库很容易就会有超过一百万个条目。因此,每次我想显示 Index
页面时查询整个数据库似乎充其量是一个糟糕的解决方案。
如果我理解有误,请随时打扰我,但对我来说这个解决方案一点也不完美。
我是不是误会了什么?是否有更有效的解决方案或库可用于 MVC 分页?
自然地,分页将需要了解总结果数,以便逻辑确定有多少页等。但是,与其降低所有结果,不如将查询构建到数据库 return分页数量(例如 30)以及所有结果的计数。
例如,如果您使用的是 Entity Framework 或 LINQ2SQL,您可以这样做
IQueryable<Result> allResults = MyRepository.RetrieveAll();
var resultGroup = allResults.OrderByDescending(r => r.DatePosted)
.Skip(60)
.Take(30)
.GroupBy(p => new {Total = allResults.Count()})
.First();
var results = new ResultObject
{
ResultCount = resultGroup.Key.Total,
Results = resultGrouping.Select(r => r)
};
因为在我们最终确定我们想要的内容之前,我们还没有对结果集执行 .ToList(),所以我们没有将结果存入内存。这是在我们对结果集调用 .First() 时完成的。
最后我们得到的对象 (ResultObject) 可以用于稍后进行分页。由于我们有计数,我们已经知道我们在哪个页面(3,因为我们跳过 60,每页 30)并且我们有结果要显示。
进一步阅读和信息
您可以通过三种方式在您的应用程序中实现分页:
- 强制您的
Repository
将数据量最少的 DTO return 返回给客户端,并使用一些 jquery
插件自己提供分页。这是一种简单的方法,但有时(如您的情况)这不是一种选择。所以你必须实现服务器端分页
- 缓存整个集合,return 需要的页面
LINQ
扩展。我认为许多存储库和 ORM 在内部执行此操作(不适用于 Entity Framework
,不能肯定)。这个解决方案的问题是你必须同步缓存和数据库,你必须让服务器有足够的内存来存储你的所有数据(或者去云,或其他东西)。与其他答案一样,您可以使用惰性 IEnumerable
工作跳过不需要的数据,因此您不需要缓存集合..
- 在数据库端实现分页。如果你正在使用
SQL
,你可以使用 ROW_NUMBER
结构,它可以在 MS SQL or in Oracle or in MySQL 中工作(实际上不是 ROW_NUMBER
本身,只是模拟)。如果您有 NoSQL
解决方案,那么您必须检查它的文档。
如果您转到 PagedList
addon 的 github 页面,您会看到如果您有一个返回 IQueryable<T>
的方法,那么 PagedList 魔法可以处理它而无需返回每个来自数据库的项目。如果您无法控制从数据库 returns 向您查询的内容,那么您必须依赖其他方法。
该页面的示例是这样的
public class ProductController : Controller
{
public object Index(int? page)
{
var products = MyProductDataSource.FindAllProducts(); //returns IQueryable<Product> representing an unknown number of products. a thousand maybe?
var pageNumber = page ?? 1; // if no page was specified in the querystring, default to the first page (1)
var onePageOfProducts = products.ToPagedList(pageNumber, 25); // will only contain 25 products max because of the pageSize
ViewBag.OnePageOfProducts = onePageOfProducts;
return View();
}
}
链接的教程看起来很奇怪,因为它使用了 List<Client>
。这确实会将所有客户端带入内存,然后翻页。相反,您应该寻找使用 IQueryable<T>
的方法,特别是 Skip
和 Take
,因此分页应该看起来像
IQueryable<Client> clients = repo.GetClients(); // lazy load - does nothing
List<Client> paged = clients.Skip(20).Take(10).ToList(); // execute final SQL
根据您使用的映射器,您会在 EF、NHibernate、Linq-to-SQL 等中找到类似的方法
github 上的示例说明它正在使用一个 IQueryable,然后由 ToPagedList() 使用,这意味着代码已经非常优化并且本身不会 return 所有记录。 ..
查看class PagedList
的代码
// superset is the IQueryable.
TotalItemCount = superset == null ? 0 : superset.Count();
// add items to internal list
if (superset != null && TotalItemCount > 0)
Subset.AddRange(pageNumber == 1
? superset.Skip(0).Take(pageSize).ToList()
: superset.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList()
如您所见,它已经使用了推荐的服务器端分页方法 skip 和 take,然后执行 ToList()。
但是,如果您没有使用 IQueryable,即 IEnumerable,则使用以下代码:
/// <summary>
/// Initializes a new instance of the <see cref="PagedList{T}"/> class that divides the supplied superset into subsets the size of the supplied pageSize. The instance then only containes the objects contained in the subset specified by index.
/// </summary>
/// <param name="superset">The collection of objects to be divided into subsets. If the collection implements <see cref="IQueryable{T}"/>, it will be treated as such.</param>
/// <param name="pageNumber">The one-based index of the subset of objects to be contained by this instance.</param>
/// <param name="pageSize">The maximum size of any individual subset.</param>
/// <exception cref="ArgumentOutOfRangeException">The specified index cannot be less than zero.</exception>
/// <exception cref="ArgumentOutOfRangeException">The specified page size cannot be less than one.</exception>
public PagedList(IEnumerable<T> superset, int pageNumber, int pageSize)
: this(superset.AsQueryable<T>(), pageNumber, pageSize)
{
}
问题在于,根据最初用于获取 IEnumerable 的筛选可能包含所有记录,因此尽可能使用 IQueryable 以获得 PagedList 的最佳性能。
此组件 (PagedList) 可以很好地处理大量记录,第一次和每次 select 一个页面都会对数据库进行 2 次调用。一个 return 记录的数量,另一个 return 只 select 页面的记录。只要确保您不调用 ToList() 方法
- 在 .cshtml 中使用 Webgrid 对象编写代码 & 它会很好。
- 分页复杂度会很低。
- 干净的代码。
- 微软 BCL class。错误更少。
如果您有一些模块或服务,并且您的分页在服务器端,或者可能是自定义分页,您可以在asp.net中使用这段代码核心
在控制器中:
var result = Get paginated result from your service;
ViewData["YourList"] = new StaticPagedList<YourListType>(result.Entity, result.Index, result.Size, result.Total);
在剃须刀页面中:
var list = ViewData["YourList"] as IPagedList<YourListType>;
我尝试实现分页已经有一段时间了,我发现了这个使用 MVC 进行分页的教程:ASP.NET MVC Paging Done Perfectly
现在,在此解决方案中,我在数据库中查询 整组客户 ,然后我 return 分页的客户列表而不是普通列表。
我觉得这很烦人,因为我只打算每页显示 10 或 20 个条目,而我的数据库很容易就会有超过一百万个条目。因此,每次我想显示 Index
页面时查询整个数据库似乎充其量是一个糟糕的解决方案。
如果我理解有误,请随时打扰我,但对我来说这个解决方案一点也不完美。
我是不是误会了什么?是否有更有效的解决方案或库可用于 MVC 分页?
自然地,分页将需要了解总结果数,以便逻辑确定有多少页等。但是,与其降低所有结果,不如将查询构建到数据库 return分页数量(例如 30)以及所有结果的计数。
例如,如果您使用的是 Entity Framework 或 LINQ2SQL,您可以这样做
IQueryable<Result> allResults = MyRepository.RetrieveAll();
var resultGroup = allResults.OrderByDescending(r => r.DatePosted)
.Skip(60)
.Take(30)
.GroupBy(p => new {Total = allResults.Count()})
.First();
var results = new ResultObject
{
ResultCount = resultGroup.Key.Total,
Results = resultGrouping.Select(r => r)
};
因为在我们最终确定我们想要的内容之前,我们还没有对结果集执行 .ToList(),所以我们没有将结果存入内存。这是在我们对结果集调用 .First() 时完成的。
最后我们得到的对象 (ResultObject) 可以用于稍后进行分页。由于我们有计数,我们已经知道我们在哪个页面(3,因为我们跳过 60,每页 30)并且我们有结果要显示。
进一步阅读和信息
您可以通过三种方式在您的应用程序中实现分页:
- 强制您的
Repository
将数据量最少的 DTO return 返回给客户端,并使用一些jquery
插件自己提供分页。这是一种简单的方法,但有时(如您的情况)这不是一种选择。所以你必须实现服务器端分页 - 缓存整个集合,return 需要的页面
LINQ
扩展。我认为许多存储库和 ORM 在内部执行此操作(不适用于Entity Framework
,不能肯定)。这个解决方案的问题是你必须同步缓存和数据库,你必须让服务器有足够的内存来存储你的所有数据(或者去云,或其他东西)。与其他答案一样,您可以使用惰性IEnumerable
工作跳过不需要的数据,因此您不需要缓存集合.. - 在数据库端实现分页。如果你正在使用
SQL
,你可以使用ROW_NUMBER
结构,它可以在 MS SQL or in Oracle or in MySQL 中工作(实际上不是ROW_NUMBER
本身,只是模拟)。如果您有NoSQL
解决方案,那么您必须检查它的文档。
如果您转到 PagedList
addon 的 github 页面,您会看到如果您有一个返回 IQueryable<T>
的方法,那么 PagedList 魔法可以处理它而无需返回每个来自数据库的项目。如果您无法控制从数据库 returns 向您查询的内容,那么您必须依赖其他方法。
该页面的示例是这样的
public class ProductController : Controller
{
public object Index(int? page)
{
var products = MyProductDataSource.FindAllProducts(); //returns IQueryable<Product> representing an unknown number of products. a thousand maybe?
var pageNumber = page ?? 1; // if no page was specified in the querystring, default to the first page (1)
var onePageOfProducts = products.ToPagedList(pageNumber, 25); // will only contain 25 products max because of the pageSize
ViewBag.OnePageOfProducts = onePageOfProducts;
return View();
}
}
链接的教程看起来很奇怪,因为它使用了 List<Client>
。这确实会将所有客户端带入内存,然后翻页。相反,您应该寻找使用 IQueryable<T>
的方法,特别是 Skip
和 Take
,因此分页应该看起来像
IQueryable<Client> clients = repo.GetClients(); // lazy load - does nothing
List<Client> paged = clients.Skip(20).Take(10).ToList(); // execute final SQL
根据您使用的映射器,您会在 EF、NHibernate、Linq-to-SQL 等中找到类似的方法
github 上的示例说明它正在使用一个 IQueryable,然后由 ToPagedList() 使用,这意味着代码已经非常优化并且本身不会 return 所有记录。 ..
查看class PagedList
的代码// superset is the IQueryable.
TotalItemCount = superset == null ? 0 : superset.Count();
// add items to internal list
if (superset != null && TotalItemCount > 0)
Subset.AddRange(pageNumber == 1
? superset.Skip(0).Take(pageSize).ToList()
: superset.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList()
如您所见,它已经使用了推荐的服务器端分页方法 skip 和 take,然后执行 ToList()。
但是,如果您没有使用 IQueryable,即 IEnumerable,则使用以下代码:
/// <summary>
/// Initializes a new instance of the <see cref="PagedList{T}"/> class that divides the supplied superset into subsets the size of the supplied pageSize. The instance then only containes the objects contained in the subset specified by index.
/// </summary>
/// <param name="superset">The collection of objects to be divided into subsets. If the collection implements <see cref="IQueryable{T}"/>, it will be treated as such.</param>
/// <param name="pageNumber">The one-based index of the subset of objects to be contained by this instance.</param>
/// <param name="pageSize">The maximum size of any individual subset.</param>
/// <exception cref="ArgumentOutOfRangeException">The specified index cannot be less than zero.</exception>
/// <exception cref="ArgumentOutOfRangeException">The specified page size cannot be less than one.</exception>
public PagedList(IEnumerable<T> superset, int pageNumber, int pageSize)
: this(superset.AsQueryable<T>(), pageNumber, pageSize)
{
}
问题在于,根据最初用于获取 IEnumerable 的筛选可能包含所有记录,因此尽可能使用 IQueryable 以获得 PagedList 的最佳性能。
此组件 (PagedList) 可以很好地处理大量记录,第一次和每次 select 一个页面都会对数据库进行 2 次调用。一个 return 记录的数量,另一个 return 只 select 页面的记录。只要确保您不调用 ToList() 方法
- 在 .cshtml 中使用 Webgrid 对象编写代码 & 它会很好。
- 分页复杂度会很低。
- 干净的代码。
- 微软 BCL class。错误更少。
如果您有一些模块或服务,并且您的分页在服务器端,或者可能是自定义分页,您可以在asp.net中使用这段代码核心
在控制器中:
var result = Get paginated result from your service;
ViewData["YourList"] = new StaticPagedList<YourListType>(result.Entity, result.Index, result.Size, result.Total);
在剃须刀页面中:
var list = ViewData["YourList"] as IPagedList<YourListType>;