オブジェクトのLINQによるページング


90

LINQクエリでページングをどのように実装しますか?実際には、当分の間、SQLのTOP関数を模倣できれば十分です。ただし、いずれにしても、完全なページングサポートの必要性は、後でもっと早く発生するはずです。

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

回答:


231

SkipおよびTake拡張メソッドを探しています。Skip結果の最初のN個の要素を超えて移動し、残りを返します。Take結果の最初のN個の要素を返し、残りの要素はすべて削除します。

これらのメソッドの使用方法の詳細については、MSDNを参照してください。 //msdn.microsoft.com/en-us/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

1
ところで、内部で何が行われているのかに興味がある場合、ところで、ほとんどのLINQデータベースドライバーは、実行されている実際のSQLのデバッグ出力情報を取得する方法を提供します。
David Pfeffer

Rob ConeryがPagedList <T>クラスについてブログを書きました。blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
これにより、pageNumberがゼロ(0)ベースでない場合、最初のページがスキップされます。pageNumberが1で始まる場合は、この「.Skip(numberOfObjectsPerPage *(pageNumber-1))」を使用してください
Alvin

結果のSQLはどのようになりますか?データベースにアクセスするSQLはどうでしょうか?
ファイズ2014

53

とを使用するSkipと、Take間違いなく行く方法です。これを実装している場合は、ページングを処理するための独自の拡張メソッドを作成する(コードを読みやすくするため)でしょう。コース利用の実装缶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);
  }
}

クラスは2つの拡張メソッドを定義します。1つはfor IEnumerable、もう1つはforですIQueryable。これは、LINQ to ObjectsとLINQ to SQLの両方で使用できることを意味します(データベースクエリを作成するときに、コンパイラがIQueryableバージョンを選択します)。

ページング要件に応じて、いくつかの追加の動作を追加することもできます(たとえば、負pageSizeまたはpage値を処理するため)。以下は、クエリでこの拡張メソッドを使用する方法の例です。

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について言及していたため、オーバーロードを1つだけ記述しました。
Tomas Petricek

私はこれを自分で実装することを考えていました。それが標準実装の一部ではないことに少し驚いています。サンプルコードをありがとう!
マイケル・リチャードソン

1
例は次のようになるはずです:public static IQueryable <T> Page <T>(... etc
David Talbot

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
Azure SQL Data WarehouseのEntity Frameworkで機能し、Skipメソッド(内部でOFFSET句を使用)をサポートしていません
Michael Freidgeim

4
これは盗まれて私の共通ライブラリに入れられなければなりませんでした、ありがとう!私はちょうどにメソッドの名前を変更しPaginate削除するためにnounverb曖昧。
ガブリエリウス2016

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 14

5

編集-不要なためSkip(0)を削除

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

2
Take / Skipメソッドの順序を変更すべきではありませんか?Takeの後のSkip(0)は意味がありません。クエリスタイルで例を提供していただきありがとうございます。
user256890

2
いいえ、彼は正しいです。Take10、Skip0は最初の10個の要素をとります。Skip0は無意味であり、実行するべきではありません。そして、の順序TakeSkip事柄- Skip10は、Take10の要素10-20かかります。Take10、10はSkip要素を返しません。
David Pfeffer

Takeを呼び出す前に、クエリを角括弧で囲む必要がある場合もあります。(...から...を選択)。Take(10)。文字列を選択して構成を呼び出しました。ブラケットがないと、Takeはクエリ結果を制限する代わりに、文字列の最初の10文字を返しました:)
user256890

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

バッチサイズは明らかに整数になります。これは、整数が単に小数点以下を切り捨てるという事実を利用しています。

私はこの応答で冗談を言っていますが、それはあなたが望んでいることをします、そしてそれが延期されるので、あなたがしたとしてもあなたは大きなパフォーマンスのペナルティを被ることはありません

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);
                    }
                }
            }

SkipまたはTakeがサポートされていない場合に役立ちます。


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から開始します。つまり、ページング数が8であることを意味する152行がある場合、IListには7しかありません。



1

主なオプションは2つあります。

.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);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.