Stronicowanie za pomocą LINQ dla obiektów


94

Jak zaimplementowałbyś stronicowanie w zapytaniu LINQ? Właściwie na razie byłbym zadowolony, gdyby można było naśladować funkcję sql TOP. Jestem jednak pewien, że potrzeba pełnej obsługi stronicowania i tak pojawi się wcześniej.

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

Odpowiedzi:


234

Szukasz metod rozszerzających Skipi Take. Skipprzechodzi poza pierwsze N ​​elementów wyniku, zwracając resztę; Takezwraca pierwsze N ​​elementów w wyniku, pomijając pozostałe elementy.

Aby uzyskać więcej informacji na temat korzystania z tych metod, zobacz MSDN: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Zakładając, że już bierzesz pod uwagę, że numer strony powinien zaczynać się od 0 (zmniejszaj o 1 zgodnie z sugestiami w komentarzach) Możesz to zrobić tak:

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

W przeciwnym razie, jak sugeruje @Alvin

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

7
Czy powinienem używać tej samej techniki w przypadku SQL z ogromną bazą danych, czy najpierw przeniesie ona całą tabelę do pamięci, a następnie wyrzuci niechciane?
user256890

1
Nawiasem mówiąc, jeśli interesuje Cię, co się dzieje pod maską, większość sterowników bazy danych LINQ zapewnia sposób na uzyskanie informacji wyjściowych debugowania dla rzeczywistego wykonywanego kodu SQL.
David Pfeffer,

Rob Conery napisał na blogu o klasie PagedList <T>, która może pomóc w rozpoczęciu. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
spowoduje to pominięcie pierwszej strony, jeśli numer strony nie jest oparty na zero (0). jeśli pageNumber zaczyna się od 1, użyj tego „.Skip (numberOfObjectsPerPage * (pageNumber - 1))”
Alvin,

Jaki będzie wynikowy kod SQL, który trafi do bazy danych?
Faiz

54

Korzystanie Skipi Takejest zdecydowanie właściwą drogą. Gdybym to implementował, prawdopodobnie napisałbym własną metodę rozszerzenia do obsługi stronicowania (aby kod był bardziej czytelny). Implementacja może oczywiście korzystać Skipi 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);
  }
}

Klasa definiuje dwie metody rozszerzające - jedną for IEnumerablei jedną for IQueryable, co oznacza, że ​​można jej używać zarówno z LINQ to Objects, jak i LINQ to SQL (podczas pisania zapytania do bazy danych kompilator wybierze IQueryablewersję).

W zależności od wymagań dotyczących stronicowania możesz także dodać dodatkowe zachowanie (na przykład obsługę wartości ujemnych pageSizelub pagewartości). Oto przykład, jak można użyć tej metody rozszerzenia w zapytaniu:

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

3
Uważam, że zwróci to cały zestaw wyników, a następnie przefiltruje w pamięci zamiast na serwerze. Ogromny spadek wydajności bazy danych, jeśli jest to SQL.
jvenema

1
@jvenema Masz rację. Ponieważ używa IEnumerableinterfejsu, a nie IQueryablespowoduje to ściągnięcia całej tabeli bazy danych, co będzie dużym spadkiem wydajności.
David Pfeffer

2
Możesz oczywiście łatwo dodać przeciążenie programu, IQueryableaby działało również z zapytaniami z bazy danych (poprawiłem odpowiedź i dodałem ją). Trochę niefortunne jest to, że nie możesz napisać kodu w sposób w pełni ogólny (w Haskell byłoby to możliwe w przypadku klas typów). Oryginalne pytanie dotyczyło LINQ to Objects, więc napisałem tylko jedno przeciążenie.
Tomas Petricek

Właśnie myślałem o wdrożeniu tego samodzielnie. Jestem trochę zaskoczony, że nie jest to część standardowej implementacji. Dzięki za przykładowy kod!
Michael Richardson,

1
Myślę, że przykładem powinno być: public static IQueryable <T> Page <T> (... itd.
David Talbot

37

Oto moje wydajne podejście do stronicowania podczas korzystania z LINQ do obiektów:

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

Można to następnie wykorzystać w następujący sposób:

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

Żadna z tych śmieci Skipi Takektóre będą wysoce nieefektywne, jeśli jesteś zainteresowany w wielu stronach.


1
To działa w Entity Framework z SQL Azure Data Warehouse, który nie obsługuje metody Pomiń (wewnętrznie przy użyciu klauzula OFFSET)
Michael Freidgeim

4
To po prostu trzeba było ukraść i włożyć do mojej wspólnej biblioteki, dzięki! Właśnie zmieniłem nazwę metody, Paginateaby usunąć nounvs verbniejednoznaczność.
Gabrielius

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

6

Nie wiem, czy to komuś pomoże, ale uznałem to za przydatne do moich celów:

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

Aby tego użyć, należałoby wykonać zapytanie linq i przekazać wynik wraz z rozmiarem strony do pętli 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
}

Więc to będzie iterować przez każdego autora, pobierając 100 autorów na raz.


Ponieważ Count () wylicza kolekcję, równie dobrze możesz przekonwertować ją na List () i wykonać iterację z indeksami.
Kaerber

5

EDYCJA - Usunięto Pomiń (0), ponieważ nie jest to konieczne

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

2
Czy nie powinieneś zmieniać kolejności metod Take / Skip? Skip (0) po Take nie ma sensu. Dzięki za podanie przykładu w stylu zapytania.
user256890

2
Nie, on ma rację. Take10, Skip0 przyjmuje pierwsze 10 elementów. Skip0 jest bezcelowe i nigdy nie powinno się go robić. A kolejność Takei ma Skipznaczenie - Skip10, Take10 przyjmuje elementy 10-20; Take10, Skip10 nie zwraca żadnych elementów.
David Pfeffer

Możesz również potrzebować nawiasów wokół zapytania przed wywołaniem Take. (z ... wybierz ...) Weź (10). Nazwałem konstrukcję, wybierając ciąg. Bez nawiasów, Take zwrócił pierwsze 10 znaków ciągu zamiast ograniczać wynik zapytania :)
user256890

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

Wielkość partii będzie oczywiście liczbą całkowitą. Wykorzystuje to fakt, że liczby całkowite po prostu pomijają miejsca dziesiętne.

W połowie żartuję z tą odpowiedzią, ale zrobi to, co chcesz, a ponieważ jest odroczona, nie poniesiesz dużej kary za wydajność, jeśli to zrobisz

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

To rozwiązanie nie jest przeznaczone dla LinqToEntities, nie wiem nawet, czy może to przekształcić w dobre zapytanie.


3

Podobnie jak odpowiedź Lukazoida, stworzyłem rozszerzenie dla 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);
                    }
                }
            }

Jest to przydatne, jeśli opcja Skip lub Take nie jest obsługiwana.


1

Używam tej metody rozszerzenia:

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

to właśnie zrobiłem. Normalnie zaczynasz od 1, ale w IList zaczynasz od 0, więc jeśli masz 152 wiersze, to znaczy, że masz 8 stronicowania, ale w IList masz tylko 7. mam nadzieję, że to wszystko wyjaśni



1

Istnieją dwie główne opcje:

.NET> = 4.0 Dynamic LINQ :

  1. Dodaj za pomocą System.Linq.Dynamic; na górze.
  2. Posługiwać się: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Możesz go również uzyskać za pomocą NuGet .

Metody rozszerzeń .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);
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.