เพจด้วย LINQ สำหรับวัตถุ


94

คุณจะใช้การเพจในแบบสอบถาม LINQ ได้อย่างไร จริงๆแล้วในตอนนี้ฉันจะพอใจถ้าฟังก์ชัน sql TOP สามารถเลียนแบบได้ อย่างไรก็ตามฉันมั่นใจว่าความต้องการการสนับสนุนการเพจเต็มรูปแบบจะเกิดขึ้นในภายหลัง

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

คำตอบ:


234

คุณกำลังมองหาวิธีการSkipและTakeส่วนขยาย Skipเคลื่อนผ่านองค์ประกอบ N แรกในผลลัพธ์โดยส่งคืนส่วนที่เหลือ Takeส่งคืนองค์ประกอบ N แรกในผลลัพธ์โดยปล่อยองค์ประกอบที่เหลือ

ดู MSDN สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการใช้วิธีการเหล่านี้: http://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
สิ่งนี้จะส่งผลให้ข้ามหน้าแรก IF pageNumber ไม่อิงตามศูนย์ (0) ถ้า pageNumber ขึ้นต้นด้วย 1 จึงใช้สิ่งนี้ ".Skip (numberOfObjectsPerPage * (pageNumber - 1)) "
Alvin

SQL ที่ได้จะเป็นอย่างไรเมื่อกดปุ่มฐานข้อมูล
Faiz

54

ใช้SkipและTakeแน่นอนวิธีที่จะไป หากฉันใช้สิ่งนี้ฉันอาจจะเขียนวิธีการขยายของตัวเองเพื่อจัดการการเพจ (เพื่อให้โค้ดอ่านง่ายขึ้น) การนำไปใช้สามารถใช้หลักสูตรSkipและ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);
  }
}

คลาสนี้กำหนดวิธีการขยายสองวิธี - หนึ่งสำหรับIEnumerableและหนึ่งสำหรับ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 ถึง Objects ดังนั้นฉันจึงเขียนโอเวอร์โหลดเพียงครั้งเดียว
Tomas Petricek

ฉันแค่คิดเกี่ยวกับการดำเนินการนี้ด้วยตัวเอง ฉันแปลกใจเล็กน้อยที่ไม่ได้เป็นส่วนหนึ่งของการใช้งานมาตรฐาน ขอบคุณสำหรับโค้ดตัวอย่าง!
Michael Richardson

1
ฉันคิดว่าตัวอย่างควรเป็น: IQueryable สาธารณะคงที่ <T> หน้า <T> (... ฯลฯ
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       
    }
}

ไม่มีขยะนี้SkipและTakeจะไม่มีประสิทธิภาพอย่างมากหากคุณสนใจในหลาย ๆ หน้า


1
ทำงานใน Entity Framework ที่มี Azure SQL Data Warehouse ซึ่งไม่รองรับ Skip method (ภายในโดยใช้ประโยค OFFSET)
Michael Freidgeim

4
สิ่งนี้ต้องถูกขโมยและใส่ไว้ใน lib ทั่วไปของฉันขอบคุณ! ฉันเพิ่งเปลี่ยนชื่อวิธีPaginateการลบnounเทียบกับverbความไม่ชัดเจน
Gabrielius


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 loop:

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

5

แก้ไข - ลบ Skip (0) ออกเนื่องจากไม่จำเป็น

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

2
คุณไม่ควรเปลี่ยนลำดับของวิธี Take / Skip ใช่หรือไม่? ข้าม (0) หลังจาก Take ไม่สมเหตุสมผล ขอบคุณสำหรับการให้ตัวอย่างของคุณในรูปแบบแบบสอบถาม
user256890

2
ไม่เขาพูดถูก Take10, Skip0 นำ 10 องค์ประกอบแรก Skip0 ไม่มีจุดหมายและไม่ควรทำ และลำดับของTakeและSkipสำคัญ - Skip10, Take10 ใช้องค์ประกอบ 10-20; Take10, Skip10 ส่งคืนไม่มีองค์ประกอบ
David Pfeffer

คุณอาจต้องใช้วงเล็บรอบแบบสอบถามก่อนที่จะเรียกใช้ (จาก ... เลือก ... ) รับ (10) ฉันเรียกโครงสร้างด้วยการเลือกสตริง หากไม่มีวงเล็บ Take จะส่งคืนอักขระ 10 ตัวแรกของสตริงแทนที่จะ จำกัด ผลการสืบค้น :)
user256890

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

จะมีประโยชน์หากไม่รองรับ 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 };
  }

นี่คือสิ่งที่ฉันทำ Normaly คุณเริ่มต้นที่ 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);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.