Paging mit LINQ für Objekte


90

Wie würden Sie Paging in einer LINQ-Abfrage implementieren? Eigentlich wäre ich vorerst zufrieden, wenn die SQL-TOP-Funktion nachgeahmt werden könnte. Ich bin mir jedoch sicher, dass der Bedarf an vollständiger Paging-Unterstützung ohnehin schon früher auftritt.

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

Antworten:


230

Sie suchen nach den Skipund TakeErweiterungsmethoden. Skipbewegt sich an den ersten N Elementen im Ergebnis vorbei und gibt den Rest zurück; TakeGibt die ersten N Elemente im Ergebnis zurück und löscht alle verbleibenden Elemente.

Weitere Informationen zur Verwendung dieser Methoden finden Sie unter MSDN: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Angenommen, Sie berücksichtigen bereits, dass die Seitennummer bei 0 beginnen sollte (Verringerung pro 1, wie in den Kommentaren vorgeschlagen). Sie könnten dies folgendermaßen tun:

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

Ansonsten wie von @Alvin vorgeschlagen

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

7
Sollte ich dieselbe Technik über SQL mit einer riesigen Datenbank verwenden, wird dann zuerst die gesamte Tabelle in den Speicher genommen und dann die unerwünschte weggeworfen?
user256890

1
Wenn Sie daran interessiert sind, was unter der Haube vor sich geht, bieten die meisten LINQ-Datenbanktreiber übrigens eine Möglichkeit, Debug-Ausgabeinformationen für das tatsächlich ausgeführte SQL abzurufen.
David Pfeffer

Rob Conery hat über eine PagedList <T> -Klasse gebloggt, die Ihnen beim Einstieg helfen kann. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
Dies führt dazu, dass die erste Seite übersprungen wird, wenn pageNumber nicht auf Null (0) basiert. Wenn pageNumber mit 1 beginnt, verwenden Sie daher dieses ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin

Wie wird das resultierende SQL aussehen, das auf die Datenbank trifft?
Faiz

53

Verwenden Skipund Takeist definitiv der richtige Weg. Wenn ich dies implementieren würde, würde ich wahrscheinlich meine eigene Erweiterungsmethode schreiben, um Paging zu handhaben (um den Code besser lesbar zu machen). Die Implementierung kann natürlich verwenden Skipund 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);
  }
}

Die Klasse definiert zwei Erweiterungsmethoden - eine für IEnumerableund eine für IQueryable, was bedeutet, dass Sie sie sowohl mit LINQ to Objects als auch mit LINQ to SQL verwenden können (beim Schreiben einer Datenbankabfrage wählt der Compiler die IQueryableVersion aus).

Abhängig von Ihren Paging-Anforderungen können Sie auch zusätzliches Verhalten hinzufügen (z. B. um mit Negativ pageSizeoder pageWert umzugehen). Hier ist ein Beispiel, wie Sie diese Erweiterungsmethode in Ihrer Abfrage verwenden würden:

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

3
Ich glaube, dies wird die gesamte Ergebnismenge zurückgeben und dann im Speicher anstatt auf dem Server filtern. Riesige Leistung beeinträchtigt eine Datenbank, wenn dies SQL ist.
Jvenema

1
@jvenema Du hast recht. Da hierbei IEnumerableeher die Schnittstelle als IQueryablediese verwendet wird, wird die gesamte Datenbanktabelle abgerufen, was einen großen Leistungseinbruch darstellt.
David Pfeffer

2
Sie können natürlich leicht eine Überladung hinzufügen IQueryable, damit es auch mit Datenbankabfragen funktioniert (ich habe die Antwort bearbeitet und hinzugefügt). Es ist ein bisschen bedauerlich, dass Sie den Code nicht vollständig generisch schreiben können (in Haskell wäre dies mit Typklassen möglich). In der ursprünglichen Frage wurde LINQ to Objects erwähnt, daher habe ich nur eine Überladung geschrieben.
Tomas Petricek

Ich habe nur darüber nachgedacht, dies selbst umzusetzen. Ich bin ein bisschen überrascht, dass es nicht Teil der Standardimplementierung ist. Danke für den Beispielcode!
Michael Richardson

1
Ich denke, das Beispiel sollte sein: public static IQueryable <T> Page <T> (... etc
David Talbot

37

Hier ist mein performanter Ansatz zum Paging bei der Verwendung von LINQ für Objekte:

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

Dies kann dann wie folgt verwendet werden:

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

Nichts von diesem Müll Skipund Takedas wird sehr ineffizient sein, wenn Sie an mehreren Seiten interessiert sind.


1
Es funktioniert in Entity Framework mit Azure SQL Data Warehouse, das die Skip-Methode nicht unterstützt (intern unter Verwendung der OFFSET-Klausel)
Michael Freidgeim

4
Dies musste nur gestohlen und in meine gemeinsame Bibliothek aufgenommen werden, danke! Ich umbenannt nur um das Verfahren Paginatezu entfernen nounvs verbMehrdeutigkeit.
Gabrielius

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

6

Ich weiß nicht, ob dies jemandem helfen wird, aber ich fand es nützlich für meine Zwecke:

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

Um dies zu verwenden, hätten Sie eine Linq-Abfrage und würden das Ergebnis zusammen mit der Seitengröße an eine foreach-Schleife übergeben:

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
}

Dies wird also über jeden Autor iterieren und jeweils 100 Autoren abrufen.


Da Count () die Auflistung auflistet, können Sie sie genauso gut in List () konvertieren und mit Indizes iterieren.
Kaerber

5

BEARBEITEN - Überspringen (0) entfernt, da dies nicht erforderlich ist

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

2
Sollten Sie die Reihenfolge der Take / Skip-Methoden nicht ändern? Überspringen (0) nach Take macht keinen Sinn. Vielen Dank, dass Sie Ihr Beispiel im Abfragestil angegeben haben.
user256890

2
Nein, er hat recht. Take10, Skip0 nimmt die ersten 10 Elemente. Skip0 ist sinnlos und sollte niemals gemacht werden. Und die Reihenfolge Takeund die SkipAngelegenheiten - Skip10, Take10 nehmen die Elemente 10-20 an; Take10, Skip10 gibt keine Elemente zurück.
David Pfeffer

Möglicherweise benötigen Sie auch Klammern um die Abfrage, bevor Sie Take aufrufen. (von ... auswählen ...). Nimm (10). Ich habe das Konstrukt mit der Auswahl eines Strings aufgerufen. Ohne Klammern gab der Take die ersten 10 Zeichen der Zeichenfolge zurück, anstatt das Abfrageergebnis einzuschränken :)
user256890

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

Batchsize wird offensichtlich eine ganze Zahl sein. Dies nutzt die Tatsache aus, dass Ganzzahlen einfach Dezimalstellen löschen.

Ich scherze nur halb mit dieser Antwort, aber sie wird tun, was Sie wollen, und weil sie aufgeschoben wird, werden Sie keine große Leistungsstrafe erleiden, wenn Sie dies tun

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

Diese Lösung ist nicht für LinqToEntities, ich weiß nicht einmal, ob sie daraus eine gute Abfrage machen könnte.


3

Ähnlich wie bei Lukazoid habe ich eine Erweiterung für IQueryable erstellt.

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

Es ist nützlich, wenn Skip oder Take nicht unterstützt werden.


1

Ich benutze diese Erweiterungsmethode:

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

das habe ich getan Normalerweise beginnen Sie bei 1, aber in IList beginnen Sie mit 0. Wenn Sie also 152 Zeilen haben, bedeutet dies, dass Sie 8 Seiten haben, in IList jedoch nur 7. Hüpfen Sie, dies kann Ihnen die Sache klar machen



1

Es gibt zwei Hauptoptionen:

.NET> = 4.0 Dynamic LINQ :

  1. Hinzufügen mit System.Linq.Dynamic; oben.
  2. Verwenden: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Sie können es auch von NuGet erhalten .

.NET <4.0- Erweiterungsmethoden :

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);
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.