查询数据库集(C#)时需要Skip和Take



我正在进行实验,并试图为每个DbSet查询要求Skip()Take()

例如,如果我尝试这样做:

var result = _context.Employee.Include(x => x.Person).ToList();

我得到一个编译时错误,比如缺少Skip()Take()值。

编辑:我面临的一个障碍是需要一个IQueryable才能调用Skip()Take()。如果我能够在DbSet上使用这些,那么我只需要修改Context构造函数。

欢迎任何其他关于自动减少记录数量的建议。

您应该自己思考:我为什么要添加Skip(…(.Take(…(

我不希望任何人要求一百万客户

可能是你想防止用户(=软件,而不是运营商(查询所有1000万客户:

var customers = repository.Customers.Skip(0).Take(Int2.MaxValue);

问题是,只要你允许用户访问你的原始DbContext,你就不能禁止他们这样做。

即使您编写了自己的ToList(...),其中添加了Skip/Take,也无法阻止他们使用原始ToList,或使用foreach,甚至在最低级别使用GetEnumerator / MoveNext

如果你想充分证明这一点,你必须创建自己的Repository类,它隐藏DbContext。如果他们要求IDbSet<...>,你不会给他们DbContext中的DbSet,而是给他们自己的对象,它包含DbContext的DbSet。

如果用户向这个DbSet请求IQueryable<...>,那么您不会给它DbContext的IQueryable,而是给它自己的。

当用户开始枚举此IQueryable时,您可以添加Skip/Take

除此之外,您还必须执行IQueryable操作,还必须将项写入Add/Remove/Update元素。

这是相当多的工作,问题是:它很难完全证明。恶意用户仍然可以发明查询,在这些查询中,您添加的Skip/Take不会阻止提取太多项目。

例如,如果你想防止用户一次请求超过1000个客户,那么恶意用户仍然可以组成用户组,每个用户组有1000个客户。加上Skip(0(和Take(1000(,他仍然有一百万客户。

所以我不认为你可以很容易地完成这个完整的证明,同时仍然允许用户访问DbSet的所有可能性。

我想鼓励人们限量取货

这个要求要求稍微低一点,但更容易实现。鼓励用户询问数据";"每页";。

您可以创建一个PageCollection,其中每个页面都有多个项目。如果用户要求页面[4],他将获得Skip/Take添加到查询中的页面。

若要生成此集合类的对象,则不必执行查询。即使获取页面,也不必执行查询。只有当您请求页面上的元素时,才会执行添加了Skip/Take的查询。

这个怎么样:

interface IPage<TResult> : IReadOnlyCollection<TResult>
{
int PageCount {get;}
int PageNr {get;}
int PageSize {get;}
}
internal class Page<TResult> : IPage<TResult>
{
internal IQueryable<TResult> Query {get; internal set;}
public int PageCount {get; internal set;}
public int PageNr {get; internal set; }
public int PageSize {get; internal set;}
// Implementation IReadOnlyCollection<TResult>
public int Count => this.PageSize;
public IEnumerator<TResult> GetEnumerator()
{
return this.Query.GetEnumerator();
}

IEnumerator IEnumerator.GetEnumerator()
{
return this.GetEnumerator();
}
}

如果这样做,则创建页面不会执行查询。如果您开始枚举页面上的项目,就会执行它。因此,获取页面并不昂贵。

如果您认为提取的页面几乎总是完全枚举的,请考虑在创建页面时执行查询,并将数据保存在列表中。通过这种方式,您可以相当容易地实现IReadOnlyList<TResult>。缺点:每次创建Page[4]时都会执行查询,即使没有人询问页面中的元素。

private class PageCollection<TResult> : IReadOnlyList<TResult>
{
private readonly IQueryable<Query> query;
private readonly int pageSize;
private readonly int pageCount;
public PageCollection(IQueryable<TResult> query, int pageSize)
{
this.query = query;
this.pageSize = pageSize;
int totalCount = query.Count();
this.pageCount = totalCount / pageSize;
}
public int PageSize => this.pageSize;
public int PageCount => this.pageCount;
public IPage<TResult> GetPage(int pageNr)
{
if (pageNr >= this.PageCount)
{
return null; // consider to return empty page
}
return new Page<TResult>
{
PageCount = this.PageCount,
PageNr = this.PageNr,
PageSize = this.PageSize,
Query = this.Query.Skip(this.PageSize * pageNr).Take(this.PageSize),
}
}
}

通过PageCount和GetPage 实现IReadOnlyList和IReadOnly Collection

注意:为了计算获取Count()所需的页数。这是一个相当便宜的操作,不会经常执行。

如果您真的根本不想执行,可以考虑实现IReadOnlyCollection<TResult>而不是IReadOnlyList<TResult>。缺点:你不能问:"给我第[4]页";

要创建PageCollection,您需要使用一个返回页面集合的方法来扩展IQueryable<TResult>,而无需执行查询。如果您不熟悉扩展方法,请参阅扩展方法解密

public IReadOnlyList<IPage<TResult>> ToPageCollection<TResult>(
this IQueryable<TResult> query,
int pageSize)
{
// TODO: decide what to do if query == null. Exception? return empty collection?
// TODO: what if pageSize <= 0? What if PageSize is too large?
return new PageCollection<TResult>(query, pageSize);
}

用法:

int pageSize = this.DataGridView.GetVisibleRows.Count;
var oldCustomerPages = dbContext.Customers
.Where(customer => customer.Birthday.Year <= 1980)
.ToPageCollection(pageSize);

注意:只执行了query.Count()!这比获取客户要快得多。

int pageNrToShow = Int32.Parse(this.textBoxPageNr.Text);
var pageToShow = oldCustomerPages[pageNrToShow];

仍然没有提取数据!

foreach (var customer in pageToShow)
{
...
}

这将最终执行查询。

获取下一页和上一页的成本很低,因为它不会执行查询。

var pageCollection = ...
IPage<Customer> currentPage;
IPage<Customer> NextPage => pageCollection[currentPage.PageNr+1];
IPage<Customer> PreviousPage => pageCollection[currentPage.PageNr-1];

最新更新