使用LINQ对对象进行分页


90

您将如何在LINQ查询中实现分页?实际上暂时来说,如果可以模仿sql TOP函数,我会感到满意。但是,我确信无论如何稍后都需要全面的页面支持。

var queryResult = from o in objects
                  where ...
                  select new
                      {
                         A = o.a,
                         B = o.b
                      }
                   ????????? TOP 10????????

Answers:


231

您正在寻找SkipTake扩展方法。Skip移过结果中的前N个元素,返回其余的元素;Take返回结果中的前N个元素,并删除所有剩余元素。

有关如何使用这些方法的更多信息,请参见MSDN:http : //msdn.microsoft.com/zh-cn/library/bb386988.aspx

假设您已经考虑到pageNumber应该从0开始(注释中建议的每1减少),您可以这样做:

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * pageNumber)
  .Take(numberOfObjectsPerPage);

否则@Alvin建议

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * (pageNumber - 1))
  .Take(numberOfObjectsPerPage);

7
我是否应该对具有大型数据库的SQL使用相同的技术,是否会先将整个表放入内存,然后丢弃不需要的表?
user256890 2010年

1
顺便说一下,如果您对引擎盖下的情况感兴趣,则大多数LINQ数据库驱动程序都提供了一种获取正在执行的实际SQL的调试输出信息的方法。
David Pfeffer

Rob Conery在博客中撰写了有关PagedList <T>类的信息,该类可以帮助您入门。blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello 2010年

49
如果pageNumber不基于零(0),则会导致跳过第一页。如果pageNumber以1开头,则使用此“ .Skip(numberOfObjectsPerPage *(pageNumber-1))”
Alvin

生成的SQL将会是什么样子,一个命中数据库?
Faiz 2014年

53

使用SkipTake绝对是要走的路。如果要实现这一点,我可能会编写自己的扩展方法来处理分页(以使代码更具可读性)。该实现当然可以使用SkipTake

static class PagingUtils {
  public static IEnumerable<T> Page<T>(this IEnumerable<T> en, int pageSize, int page) {
    return en.Skip(page * pageSize).Take(pageSize);
  }
  public static IQueryable<T> Page<T>(this IQueryable<T> en, int pageSize, int page) {
    return en.Skip(page * pageSize).Take(pageSize);
  }
}

该类定义了两种扩展方法-一种用于for IEnumerable,一种用于for IQueryable,这意味着您可以将其与LINQ to Objects和LINQ to SQL一起使用(在编写数据库查询时,编译器将选择IQueryable版本)。

根据您的页面调度要求,您还可以添加一些其他行为(例如,处理否定值pageSizepage值)。这是一个示例如何在查询中使用此扩展方法:

var q = (from p in products
         where p.Show == true
         select new { p.Name }).Page(10, pageIndex);

3
我相信这将返回整个结果集,然后在内存中而不是在服务器上进行过滤。如果这是SQL,则会对数据库造成巨大的性能影响。
jvenema

1
@jvenema你是对的。由于这是使用IEnumerable接口而不是接口,IQueryable因此会拉入整个数据库表,这将对性能造成重大影响。
David Pfeffer

2
您当然可以轻松地为其添加重载,IQueryable使其也可以与数据库查询一起使用(我编辑了答案并添加了它)。不幸的是,您不能以完全通用的方式编写代码(在Haskell中,类型类可能会实现)。最初的问题提到了LINQ to Objects,所以我只写了一个重载。
Tomas Petricek 2010年

我只是在考虑自己实施此操作。我有点惊讶,它不是标准实现的一部分。感谢您的示例代码!
迈克尔·理查森

1
我认为示例应该是:public static IQueryable <T> Page <T>(... etc
David Talbot 2014年

37

这是使用LINQ进行对象时我分页的高性能方法:

public static IEnumerable<IEnumerable<T>> Page<T>(this IEnumerable<T> source, int pageSize)
{
    Contract.Requires(source != null);
    Contract.Requires(pageSize > 0);
    Contract.Ensures(Contract.Result<IEnumerable<IEnumerable<T>>>() != null);

    using (var enumerator = source.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            var currentPage = new List<T>(pageSize)
            {
                enumerator.Current
            };

            while (currentPage.Count < pageSize && enumerator.MoveNext())
            {
                currentPage.Add(enumerator.Current);
            }
            yield return new ReadOnlyCollection<T>(currentPage);
        }
    }
}

然后可以这样使用:

var items = Enumerable.Range(0, 12);

foreach(var page in items.Page(3))
{
    // Do something with each page
    foreach(var item in page)
    {
        // Do something with the item in the current page       
    }
}

这些都不是垃圾SkipTake如果您对多页感兴趣的话,效率会很低。


1
它可以在不支持Skip方法(内部使用OFFSET子句)的Azure SQL数据仓库的实体框架中工作
Michael Freidgeim 2016年

4
只需将其偷走并放到我的通用库中,谢谢!我只是将方法重命名Paginate为消除nounvs verb模糊性。
百利修斯

9
   ( for o in objects
    where ...
    select new
   {
     A=o.a,
     B=o.b
   })
.Skip((page-1)*pageSize)
.Take(pageSize)

6

不知道这是否对任何人都有帮助,但是我发现它对于我的目的很有用:

private static IEnumerable<T> PagedIterator<T>(IEnumerable<T> objectList, int PageSize)
{
    var page = 0;
    var recordCount = objectList.Count();
    var pageCount = (int)((recordCount + PageSize)/PageSize);

    if (recordCount < 1)
    {
        yield break;
    }

    while (page < pageCount)
    {
        var pageData = objectList.Skip(PageSize*page).Take(PageSize).ToList();

        foreach (var rd in pageData)
        {
            yield return rd;
        }
        page++;
    }
}

要使用它,您将需要一些linq查询,并将结果和页面大小一起传递到foreach循环中:

var results = from a in dbContext.Authors
              where a.PublishDate > someDate
              orderby a.Publisher
              select a;

foreach(var author in PagedIterator(results, 100))
{
    // Do Stuff
}

因此,这将遍历每个作者一次获取100位作者的过程。


当Count()枚举集合时,您也可以将其转换为List()并使用索引进行迭代。
Kaerber 2014年

5

编辑-删除了跳过(0),因为它没有必要

var queryResult = (from o in objects where ...
                      select new
                      {
                          A = o.a,
                          B = o.b
                      }
                  ).Take(10);

2
您不应该更改Take / Skip方法的顺序吗?Take之后跳过(0)没有意义。感谢您以查询方式提供示例。
user256890 2010年

2
不,他是对的。Take10,Skip0取前10个元素。Skip0是没有意义的,永远不应该这样做。和顺序TakeSkip事项- Skip10,Take10需要的元素10-20; Take10、10 Skip不返回任何元素。
David Pfeffer

在调用Take之前,您可能还需要在查询前后加上括号。(从...选择...)。取(10)。我通过选择字符串来调用该构造。没有方括号,
汇整

3
var pages = items.Select((item, index) => new { item, Page = index / batchSize }).GroupBy(g => g.Page);

Batchsize显然是整数。这利用了整数仅掉小数位这一事实。

我对此回应开玩笑,但它会按您的意愿去做,而且由于延迟了,如果您这样做,也不会造成较大的性能损失

pages.First(p => p.Key == thePage)

该解决方案不适用于LinqToEntities,我什至不知道它是否可以将其变成一个很好的查询。


3

Lukazoid的答案类似,我为IQueryable创建了扩展。

   public static IEnumerable<IEnumerable<T>> PageIterator<T>(this IQueryable<T> source, int pageSize)
            {
                Contract.Requires(source != null);
                Contract.Requires(pageSize > 0);
                Contract.Ensures(Contract.Result<IEnumerable<IQueryable<T>>>() != null);

                using (var enumerator = source.GetEnumerator())
                {
                    while (enumerator.MoveNext())
                    {
                        var currentPage = new List<T>(pageSize)
                        {
                            enumerator.Current
                        };

                        while (currentPage.Count < pageSize && enumerator.MoveNext())
                        {
                            currentPage.Add(enumerator.Current);
                        }
                        yield return new ReadOnlyCollection<T>(currentPage);
                    }
                }
            }

如果不支持“跳过”或“取走”,则很有用。


1

我使用这种扩展方法:

public static IQueryable<T> Page<T, TResult>(this IQueryable<T> obj, int page, int pageSize, System.Linq.Expressions.Expression<Func<T, TResult>> keySelector, bool asc, out int rowsCount)
{
    rowsCount = obj.Count();
    int innerRows = rowsCount - (page * pageSize);
    if (innerRows < 0)
    {
        innerRows = 0;
    }
    if (asc)
        return obj.OrderByDescending(keySelector).Take(innerRows).OrderBy(keySelector).Take(pageSize).AsQueryable();
    else
        return obj.OrderBy(keySelector).Take(innerRows).OrderByDescending(keySelector).Take(pageSize).AsQueryable();
}

public IEnumerable<Data> GetAll(int RowIndex, int PageSize, string SortExpression)
{
    int totalRows;
    int pageIndex = RowIndex / PageSize;

    List<Data> data= new List<Data>();
    IEnumerable<Data> dataPage;

    bool asc = !SortExpression.Contains("DESC");
    switch (SortExpression.Split(' ')[0])
    {
        case "ColumnName":
            dataPage = DataContext.Data.Page(pageIndex, PageSize, p => p.ColumnName, asc, out totalRows);
            break;
        default:
            dataPage = DataContext.vwClientDetails1s.Page(pageIndex, PageSize, p => p.IdColumn, asc, out totalRows);
            break;
    }

    foreach (var d in dataPage)
    {
        clients.Add(d);
    }

    return data;
}
public int CountAll()
{
    return DataContext.Data.Count();
}

1
    public LightDataTable PagerSelection(int pageNumber, int setsPerPage, Func<LightDataRow, bool> prection = null)
    {
        this.setsPerPage = setsPerPage;
        this.pageNumber = pageNumber > 0 ? pageNumber - 1 : pageNumber;
        if (!ValidatePagerByPageNumber(pageNumber))
            return this;

        var rowList = rows.Cast<LightDataRow>();
        if (prection != null)
            rowList = rows.Where(prection).ToList();

        if (!rowList.Any())
            return new LightDataTable() { TablePrimaryKey = this.tablePrimaryKey };
        //if (rowList.Count() < (pageNumber * setsPerPage))
        //    return new LightDataTable(new LightDataRowCollection(rowList)) { TablePrimaryKey = this.tablePrimaryKey };

        return new LightDataTable(new LightDataRowCollection(rowList.Skip(this.pageNumber * setsPerPage).Take(setsPerPage).ToList())) { TablePrimaryKey = this.tablePrimaryKey };
  }

这就是我所做的。通常,您从1开始,但是在IList中,您从0开始。因此,如果您有152行,这意味着您有8个分页,但是在IList中,您只有7行。



1

有两个主要选项:

.NET> = 4.0 动态LINQ

  1. 使用System.Linq.Dynamic添加;在顶部。
  2. 用: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

您也可以通过NuGet获取它。

.NET <4.0 扩展方法

private static readonly Hashtable accessors = new Hashtable();

private static readonly Hashtable callSites = new Hashtable();

private static CallSite<Func<CallSite, object, object>> GetCallSiteLocked(string name) {
    var callSite = (CallSite<Func<CallSite, object, object>>)callSites[name];
    if(callSite == null)
    {
        callSites[name] = callSite = CallSite<Func<CallSite, object, object>>.Create(
                    Binder.GetMember(CSharpBinderFlags.None, name, typeof(AccessorCache),
                new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
    }
    return callSite;
}

internal static Func<dynamic,object> GetAccessor(string name)
{
    Func<dynamic, object> accessor = (Func<dynamic, object>)accessors[name];
    if (accessor == null)
    {
        lock (accessors )
        {
            accessor = (Func<dynamic, object>)accessors[name];
            if (accessor == null)
            {
                if(name.IndexOf('.') >= 0) {
                    string[] props = name.Split('.');
                    CallSite<Func<CallSite, object, object>>[] arr = Array.ConvertAll(props, GetCallSiteLocked);
                    accessor = target =>
                    {
                        object val = (object)target;
                        for (int i = 0; i < arr.Length; i++)
                        {
                            var cs = arr[i];
                            val = cs.Target(cs, val);
                        }
                        return val;
                    };
                } else {
                    var callSite = GetCallSiteLocked(name);
                    accessor = target =>
                    {
                        return callSite.Target(callSite, (object)target);
                    };
                }
                accessors[name] = accessor;
            }
        }
    }
    return accessor;
}
public static IOrderedEnumerable<dynamic> OrderBy(this IEnumerable<dynamic> source, string property)
{
    return Enumerable.OrderBy<dynamic, object>(source, AccessorCache.GetAccessor(property), Comparer<object>.Default);
}
public static IOrderedEnumerable<dynamic> OrderByDescending(this IEnumerable<dynamic> source, string property)
{
    return Enumerable.OrderByDescending<dynamic, object>(source, AccessorCache.GetAccessor(property), Comparer<object>.Default);
}
public static IOrderedEnumerable<dynamic> ThenBy(this IOrderedEnumerable<dynamic> source, string property)
{
    return Enumerable.ThenBy<dynamic, object>(source, AccessorCache.GetAccessor(property), Comparer<object>.Default);
}
public static IOrderedEnumerable<dynamic> ThenByDescending(this IOrderedEnumerable<dynamic> source, string property)
{
    return Enumerable.ThenByDescending<dynamic, object>(source, AccessorCache.GetAccessor(property), Comparer<object>.Default);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.