Phân trang với LINQ cho các đối tượng


90

Bạn sẽ triển khai phân trang như thế nào trong một truy vấn LINQ? Thực sự trong thời điểm hiện tại, tôi sẽ hài lòng nếu hàm sql TOP có thể bắt chước được. Tuy nhiên, tôi chắc chắn rằng dù sao thì nhu cầu hỗ trợ phân trang đầy đủ cũng xuất hiện sớm hơn.

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

Câu trả lời:


231

Bạn đang tìm kiếm SkipTakecác phương pháp mở rộng. Skipdi chuyển qua N phần tử đầu tiên trong kết quả, trả về phần còn lại; Taketrả về N phần tử đầu tiên trong kết quả, loại bỏ bất kỳ phần tử nào còn lại.

Xem MSDN để biết thêm thông tin về cách sử dụng các phương pháp này: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Giả sử bạn đã tính đến việc Số trang phải bắt đầu từ 0 (giảm cho mỗi 1 như được đề xuất trong các nhận xét) Bạn có thể làm như sau:

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

Mặt khác theo đề xuất của @Alvin

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

7
Liệu tôi có nên sử dụng kỹ thuật tương tự đối với SQL với một cơ sở dữ liệu khổng lồ, liệu nó có đưa toàn bộ bảng vào bộ nhớ trước rồi mới loại bỏ những thứ không mong muốn không?
user256890

1
Nhân tiện, nếu bạn quan tâm đến những gì đang diễn ra, hầu hết các trình điều khiển cơ sở dữ liệu LINQ đều cung cấp một cách để lấy thông tin đầu ra gỡ lỗi cho SQL thực đang được thực thi.
David Pfeffer

Rob C Office đã viết blog về lớp PagedList <T> có thể giúp bạn bắt đầu. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
điều này sẽ dẫn đến việc bỏ qua trang đầu tiên NẾU pageNumber không dựa trên 0 (0). nếu pageNumber bắt đầu bằng 1, do đó hãy sử dụng điều này ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin

SQL kết quả sẽ như thế nào, câu trả lời vào cơ sở dữ liệu?
Faiz

53

Sử dụng SkipTakechắc chắn là con đường để đi. Nếu tôi đang triển khai điều này, có lẽ tôi sẽ viết phương thức mở rộng của riêng mình để xử lý phân trang (để làm cho mã dễ đọc hơn). Việc triển khai tất nhiên có thể sử dụng 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);
  }
}

Lớp định nghĩa hai phương thức mở rộng - một cho IEnumerablevà một cho IQueryable, có nghĩa là bạn có thể sử dụng nó với cả LINQ to Objects và LINQ to SQL (khi viết truy vấn cơ sở dữ liệu, trình biên dịch sẽ chọnIQueryable phiên bản).

Tùy thuộc vào yêu cầu phân trang của bạn, bạn cũng có thể thêm một số hành vi bổ sung (ví dụ: để xử lý âm pageSizehoặc pagegiá trị). Dưới đây là ví dụ về cách bạn sử dụng phương thức tiện ích mở rộng này trong truy vấn của mình:

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

3
Tôi tin rằng điều này sẽ trả về toàn bộ tập kết quả và sau đó lọc trong bộ nhớ thay vì trên máy chủ. Hiệu suất rất lớn đánh vào cơ sở dữ liệu nếu đây là SQL.
jvenema

1
@jvenema Bạn nói đúng. Vì điều này đang sử dụng IEnumerablegiao diện chứ không phải IQueryableđiều này sẽ kéo toàn bộ bảng cơ sở dữ liệu, đây sẽ là một tác động lớn đến hiệu suất.
David Pfeffer

2
Tất nhiên, bạn có thể dễ dàng thêm một quá tải IQueryableđể làm cho nó hoạt động với các truy vấn databse (Tôi đã chỉnh sửa câu trả lời và thêm nó). Có một chút đáng tiếc là bạn không thể viết mã một cách hoàn toàn chung chung (trong Haskell, điều này có thể xảy ra với các lớp kiểu). Câu hỏi ban đầu đề cập đến LINQ cho Đối tượng, vì vậy tôi chỉ viết một quá tải.
Tomas Petricek

Tôi chỉ đang nghĩ về việc tự mình thực hiện điều này. Tôi hơi ngạc nhiên rằng nó không phải là một phần của việc triển khai tiêu chuẩn. Cảm ơn vì mã mẫu!
Michael Richardson

1
Tôi nghĩ rằng ví dụ nên là: public static IQueryable <T> Trang <T> (... vv
David Talbot

37

Đây là cách tiếp cận hiệu quả của tôi để phân trang khi sử dụng LINQ cho các đối tượng:

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

Điều này sau đó có thể được sử dụng như vậy:

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

Không có thứ gì rác rưởi này SkipTakesẽ rất kém hiệu quả nếu bạn quan tâm đến nhiều trang.


1
Nó hoạt động trong Entity Framework với Azure SQL Data Warehouse, mà không hỗ trợ phương pháp Skip (nội bộ sử dụng khoản OFFSET)
Michael Freidgeim

4
Cái này chỉ cần bị đánh cắp và đưa vào lib chung của tôi, cảm ơn! Tôi vừa đổi tên phương thức Paginateđể loại bỏ sự mơ hồ nounvs. verb
Gabrielius

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

6

Không biết liệu điều này có giúp ích được ai không, nhưng tôi thấy nó hữu ích cho mục đích của mình:

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

Để sử dụng điều này, bạn sẽ có một số truy vấn linq và chuyển kết quả cùng với kích thước trang vào một vòng lặp 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
}

Vì vậy, điều này sẽ lặp lại trên mỗi tác giả tìm nạp 100 tác giả cùng một lúc.


Khi Count () liệt kê tập hợp, bạn cũng có thể chuyển đổi nó thành List () và lặp lại với các chỉ mục.
Kaerber

5

EDIT - Đã xóa Bỏ qua (0) vì không cần thiết

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

2
Bạn không nên thay đổi thứ tự của các phương thức Take / Skip? Bỏ qua (0) sau khi Take không có ý nghĩa. Thanx vì đưa ra ví dụ của bạn theo kiểu truy vấn.
dùng256890

2
Không, anh ấy đúng. Take10, Skip0 lấy 10 phần tử đầu tiên. Skip0 là vô nghĩa và không bao giờ nên làm. Và thứ tự của TakeSkipcác vấn đề - Skip10, Take10 lấy các phần tử 10-20; Take10, Skip10 không trả về phần tử nào.
David Pfeffer

Bạn cũng có thể cần dấu ngoặc quanh truy vấn trước khi gọi Take. (từ ... chọn ...). Lấy (10). Tôi đã gọi cấu trúc với việc chọn một chuỗi. Nếu không có dấu ngoặc, Take trả lại 10 ký tự đầu tiên của chuỗi thay vì giới hạn kết quả truy vấn :)
user256890

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

Batchsize rõ ràng sẽ là một số nguyên. Điều này tận dụng lợi thế của thực tế là các số nguyên chỉ đơn giản là giảm vị trí thập phân.

Tôi nửa đùa nửa thật với câu trả lời này, nhưng nó sẽ làm những gì bạn muốn và vì nó được trì hoãn, bạn sẽ không phải chịu một khoản phạt về hiệu suất lớn nếu bạn làm

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

Giải pháp này không dành cho LinqToEntities, tôi thậm chí không biết liệu nó có thể biến điều này thành một truy vấn tốt hay không.


3

Tương tự như câu trả lời của Lukazoid, tôi đã tạo một phần mở rộng cho 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);
                    }
                }
            }

Sẽ rất hữu ích nếu Skip hoặc Take không được hỗ trợ.


1

Tôi sử dụng phương pháp mở rộng này:

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

Đây là những gì tôi đã làm. Normaly bạn bắt đầu từ 1 nhưng trong IList bạn bắt đầu bằng 0. vì vậy nếu bạn có 152 hàng có nghĩa là bạn có 8 phân trang nhưng trong IList bạn chỉ có 7. hop điều này có thể làm rõ cho bạn



1

Có hai lựa chọn chính:

.NET> = 4.0 LINQ động :

  1. Thêm bằng cách sử dụng System.Linq.Dynamic; ở trên cùng.
  2. Sử dụng: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Bạn cũng có thể lấy nó bằng NuGet .

.NET <4.0 Phương thức mở rộng :

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);
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.