Paging dengan LINQ untuk objek

94

Bagaimana Anda menerapkan paging dalam kueri LINQ? Sebenarnya untuk saat ini, saya akan puas jika fungsi sql TOP bisa ditiru. Namun, saya yakin bahwa kebutuhan untuk dukungan paging penuh akan muncul lebih cepat nanti.

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

Jawaban:

234

Anda sedang mencari metode ekstensi Skipdan Take. Skipbergerak melewati elemen N pertama dalam hasil, mengembalikan sisanya; Takemengembalikan elemen N pertama dalam hasil, membuang elemen yang tersisa.

Lihat MSDN untuk informasi lebih lanjut tentang cara menggunakan metode ini: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Dengan asumsi Anda sudah memperhitungkan bahwa pageNumber harus dimulai dari 0 (turunkan per 1 seperti yang disarankan di komentar) Anda dapat melakukannya seperti ini:

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

Sebaliknya seperti yang disarankan oleh @Alvin

int numberOfObjectsPerPage = 10;
var queryResultPage = queryResult
  .Skip(numberOfObjectsPerPage * (pageNumber - 1))
  .Take(numberOfObjectsPerPage);
David Pfeffer
sumber
7
Haruskah saya menggunakan teknik yang sama di atas SQL dengan database yang besar, apakah itu akan mengambil seluruh tabel ke dalam memori terlebih dahulu dan kemudian membuang yang tidak diinginkan?
pengguna256890
1
Jika Anda tertarik dengan apa yang terjadi di balik terpal, sebagian besar driver basis data LINQ menyediakan cara untuk mendapatkan informasi keluaran debug untuk SQL aktual yang sedang dieksekusi.
David Pfeffer
Rob Conery membuat blog tentang kelas PagedList <T> yang dapat membantu Anda memulai. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello
49
ini akan mengakibatkan melewatkan halaman pertama JIKA pageNumber tidak berbasis nol (0). jika pageNumber dimulai dengan 1, maka gunakan ini ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin
Akan seperti apa SQL yang dihasilkan, yang mencapai database?
Faiz
54

Menggunakan Skipdan Takejelas merupakan cara yang tepat. Jika saya menerapkan ini, saya mungkin akan menulis metode ekstensi saya sendiri untuk menangani paging (untuk membuat kode lebih mudah dibaca). Penerapannya tentu saja dapat menggunakan Skipdan Take:

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

Kelas mendefinisikan dua metode ekstensi - satu untuk IEnumerabledan satu lagi untuk IQueryable, yang berarti Anda dapat menggunakannya dengan LINQ ke Objek dan LINQ ke SQL (saat menulis kueri database, kompilator akan memilih IQueryableversinya).

Bergantung pada persyaratan halaman Anda, Anda juga dapat menambahkan beberapa perilaku tambahan (misalnya untuk menangani negatif pageSizeatau pagenilai). Berikut adalah contoh bagaimana Anda akan menggunakan metode ekstensi ini dalam kueri Anda:

var q = (from p in products
         where p.Show == true
         select new { p.Name }).Page(10, pageIndex);
Tomas Petricek
sumber
3
Saya yakin ini akan mengembalikan seluruh rangkaian hasil, dan kemudian memfilter dalam memori, bukan di server. Kinerja yang sangat besar akan memukul database jika ini adalah SQL.
jvenema
1
@jvenema Anda benar. Karena ini menggunakan IEnumerableantarmuka daripada IQueryableini akan menarik seluruh tabel database, yang akan menjadi hit kinerja utama.
David Pfeffer
2
Anda tentu saja dapat dengan mudah menambahkan kelebihan untuk IQueryablemembuatnya berfungsi dengan kueri databse juga (saya mengedit jawabannya dan menambahkannya). Sangat disayangkan bahwa Anda tidak dapat menulis kode dengan cara yang sepenuhnya umum (di Haskell hal ini dapat dilakukan dengan kelas tipe). Pertanyaan asli menyebutkan LINQ ke Objek, jadi saya menulis hanya satu kelebihan beban.
Tomas Petricek
Saya baru saja berpikir untuk menerapkan ini sendiri. Saya sedikit terkejut bahwa ini bukan bagian dari penerapan standar. Terima kasih atas kode sampelnya!
Michael Richardson
1
Saya pikir contohnya harus: public static IQueryable <T> Page <T> (... etc
David Talbot
37

Berikut adalah pendekatan performant saya untuk paging saat menggunakan LINQ ke objek:

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

Ini kemudian dapat digunakan seperti ini:

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

Tak satu pun dari sampah ini Skipdan Takeyang akan sangat tidak efisien jika Anda tertarik pada banyak halaman.

Lukazoid
sumber
1
Ia bekerja di Entity Framework dengan Azure SQL Data Warehouse, yang tidak mendukung metode Skip (secara internal menggunakan klausa OFFSET)
Michael Freidgeim
4
Ini hanya harus dicuri dan dimasukkan ke dalam lib umum saya, terima kasih! Saya baru saja mengganti nama metode Paginateuntuk menghapus nounvs verbambiguitas.
Gabrielius
9
   ( for o in objects
    where ...
    select new
   {
     A=o.a,
     B=o.b
   })
.Skip((page-1)*pageSize)
.Take(pageSize)
Noel
sumber
6

Tidak tahu apakah ini akan membantu siapa pun, tetapi saya merasa ini berguna untuk tujuan saya:

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

Untuk menggunakan ini, Anda akan memiliki beberapa query linq, dan meneruskan hasilnya bersama dengan ukuran halaman ke loop 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
}

Jadi ini akan mengulang setiap penulis yang mengambil 100 penulis sekaligus.

Bitfiddler
sumber
Saat Count () menghitung koleksi, Anda juga dapat mengonversinya menjadi List () dan mengulanginya dengan indeks.
Kaerber
5

EDIT - Lewati Dihapus (0) karena tidak perlu

var queryResult = (from o in objects where ...
                      select new
                      {
                          A = o.a,
                          B = o.b
                      }
                  ).Take(10);
Jack Marchetti
sumber
2
Tidakkah sebaiknya Anda mengubah urutan metode Ambil / Lewati? Lewati (0) setelah Take tidak masuk akal. Terima kasih untuk memberikan contoh Anda dalam gaya kueri.
pengguna256890
2
Tidak, dia benar. Take10, Skip0 mengambil 10 elemen pertama. Skip0 tidak ada gunanya dan tidak boleh dilakukan. Dan urutan Takedan Skipmasalah - Skip10, Take10 mengambil elemen 10-20; Take10, Skip10 tidak mengembalikan elemen.
David Pfeffer
Anda mungkin juga membutuhkan tanda kurung di sekitar kueri sebelum memanggil Take. (dari ... pilih ...). Ambil (10). Saya menyebut konstruksi dengan memilih string. Tanpa tanda kurung, Take mengembalikan 10 karakter pertama dari string alih-alih membatasi hasil kueri :)
user256890
3
var pages = items.Select((item, index) => new { item, Page = index / batchSize }).GroupBy(g => g.Page);

Batchsize jelas akan menjadi bilangan bulat. Ini mengambil keuntungan dari fakta bahwa bilangan bulat hanya menghilangkan tempat desimal.

Saya setengah bercanda dengan tanggapan ini, tetapi itu akan melakukan apa yang Anda inginkan, dan karena ditangguhkan, Anda tidak akan dikenai penalti kinerja yang besar jika melakukannya.

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

Solusi ini bukan untuk LinqToEntities, saya bahkan tidak tahu apakah ini bisa mengubahnya menjadi kueri yang bagus.

Todd A. Stedel
sumber
3

Mirip dengan jawaban Lukazoid, saya telah membuat ekstensi untuk IQuerable.

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

Berguna jika Lewati atau Ambil tidak didukung.

Michael Freidgeim
sumber
1

Saya menggunakan metode ekstensi ini:

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();
}
Randy
sumber
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 };
  }

inilah yang saya lakukan. Normaly Anda mulai dari 1 tetapi di IList Anda mulai dengan 0. jadi jika Anda memiliki 152 baris yang berarti Anda memiliki 8 halaman tetapi di IList Anda hanya memiliki 7. hop ini dapat memperjelas bagi Anda

Alen.Toma
sumber
1

var results = (medicineInfo.OrderBy(x=>x.id)
                       .Skip((pages -1) * 2)
                       .Take(2));

Debendra Dash
sumber
1

Ada dua opsi utama:

.NET> = 4.0 Dynamic LINQ :

  1. Tambahkan menggunakan System.Linq.Dynamic; di atas.
  2. Menggunakan: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Anda juga bisa mendapatkannya dengan NuGet .

.NET <4.0 Metode Ekstensi :

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