Paging con LINQ per gli oggetti


90

Come implementeresti il ​​paging in una query LINQ? In realtà per il momento sarei soddisfatto se la funzione sql TOP potesse essere imitata. Tuttavia, sono sicuro che la necessità di un supporto completo per il paging emergerà comunque presto.

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

Risposte:


231

Stai cercando i metodi di estensione Skipe Take. Skipsi sposta oltre i primi N elementi nel risultato, restituendo il resto; Takerestituisce i primi N elementi nel risultato, eliminando tutti gli elementi rimanenti.

Vedere MSDN per ulteriori informazioni su come utilizzare questi metodi: http://msdn.microsoft.com/en-us/library/bb386988.aspx

Supponendo che tu stia già prendendo in considerazione che pageNumber dovrebbe iniziare da 0 (diminuzione per 1 come suggerito nei commenti) Potresti farlo in questo modo:

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

Altrimenti come suggerito da @Alvin

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

7
Devo usare la stessa tecnica su SQL con un database enorme, prima porterà l'intera tabella in memoria e poi eliminerà gli elementi indesiderati?
user256890

1
Se sei interessato a quello che sta succedendo sotto il cofano, a proposito, la maggior parte dei driver di database LINQ fornisce un modo per ottenere informazioni di output di debug per l'SQL effettivo che viene eseguito.
David Pfeffer

Rob Conery ha scritto sul blog di una classe PagedList <T> che può aiutarti a iniziare. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
questo risulterà nel saltare la prima pagina SE pageNumber non è basato su zero (0). se pageNumber inizia con 1, quindi usa questo ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Alvin

Come sarà l'SQL risultante, quello che colpisce il database?
Faiz

53

Usare Skiped Takeè sicuramente la strada da percorrere. Se dovessi implementarlo, probabilmente scriverei il mio metodo di estensione per gestire il paging (per rendere il codice più leggibile). L'implementazione può ovviamente utilizzare Skipe 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);
  }
}

La classe definisce due metodi di estensione: uno per IEnumerablee uno per IQueryable, il che significa che è possibile utilizzarlo sia con LINQ to Objects che con LINQ to SQL (durante la scrittura della query del database, il compilatore sceglierà la IQueryableversione).

A seconda dei requisiti di paginazione, potresti anche aggiungere qualche comportamento aggiuntivo (ad esempio per gestire il negativo pageSizeo il pagevalore). Ecco un esempio di come useresti questo metodo di estensione nella tua query:

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

3
Credo che questo restituirà l'intero set di risultati e quindi filtrerà in memoria invece che sul server. Enorme riduzione delle prestazioni rispetto a un database se questo è SQL.
jvenema

1
@jvenema Hai ragione. Poiché si utilizza l' IEnumerableinterfaccia invece di IQueryablequesta, verrà inserita l'intera tabella del database, il che sarà un notevole calo delle prestazioni.
David Pfeffer

2
Ovviamente puoi facilmente aggiungere un sovraccarico per IQueryablefarlo funzionare anche con le query di database (ho modificato la risposta e aggiunta). È un po 'un peccato che non sia possibile scrivere il codice in un modo completamente generico (in Haskell questo sarebbe possibile con le classi di tipo). La domanda originale menzionava LINQ to Objects, quindi ho scritto solo un overload.
Tomas Petricek

Stavo solo pensando di implementarlo da solo. Sono un po 'sorpreso che non faccia parte dell'implementazione standard. Grazie per il codice di esempio!
Michael Richardson

1
Penso che l'esempio dovrebbe essere: public static IQueryable <T> Page <T> (... etc
David Talbot

37

Ecco il mio approccio efficiente al paging quando si utilizza LINQ to objects:

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

Questo può quindi essere utilizzato in questo modo:

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

Niente di tutto questo schifo SkipeTake che sarà altamente inefficiente se sei interessato a più pagine.


1
Funziona in Entity Framework con Azure SQL Data Warehouse, che non supporta il metodo Skip (internamente utilizzando la clausola OFFSET)
Michael Freidgeim

4
Questo doveva solo essere rubato e messo nella mia libreria comune, grazie! Ho appena rinominato il metodo Paginateper rimuovere nounvs verbambiguity.
Gabrielius

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

6

Non so se questo aiuterà qualcuno, ma l'ho trovato utile per i miei scopi:

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

Per usarlo avresti qualche query linq e passerai il risultato insieme alla dimensione della pagina in un ciclo 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
}

Quindi questo itererà su ogni autore recuperando 100 autori alla volta.


Poiché Count () enumera la raccolta, puoi anche convertirla in List () e iterare con gli indici.
Kaerber

5

EDIT - Rimosso Skip (0) in quanto non necessario

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

2
Non dovresti cambiare l'ordine dei metodi Take / Skip? Skip (0) dopo Take non ha senso. Grazie per aver fornito l'esempio in stile query.
user256890

2
No, ha ragione. Take10, Skip0 prende i primi 10 elementi. Skip0 è inutile e non dovrebbe mai essere fatto. E l'ordine di Takee Skipconta - Skip10, Take10 prende gli elementi 10-20; Take10, Skip10 non restituisce alcun elemento.
David Pfeffer

Potresti anche aver bisogno di parentesi attorno alla query prima di chiamare Take. (da ... seleziona ...). Prendi (10). Ho chiamato il costrutto selezionando una stringa. Senza parentesi, il Take ha restituito i primi 10 caratteri della stringa invece di limitare il risultato della query :)
user256890

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

Batchsize sarà ovviamente un numero intero. Ciò si avvantaggia del fatto che i numeri interi eliminano semplicemente le posizioni decimali.

Sto quasi scherzando con questa risposta, ma farà quello che vuoi, e poiché è differita, non incorrerai in una grossa penalità di prestazione se lo fai

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

Questa soluzione non è per LinqToEntities, non so nemmeno se potrebbe trasformarla in una buona query.


3

Simile alla risposta di Lukazoid, ho creato un'estensione per 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);
                    }
                }
            }

È utile se Skip o Take non sono supportati.


1

Uso questo metodo di estensione:

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

questo è quello che ho fatto. Normalmente inizi da 1 ma in IList inizi con 0. quindi se hai 152 righe significa che hai 8 pagine ma in IList hai solo 7. hop questo può chiarirti le cose



1

Ci sono due opzioni principali:

.NET> = 4.0 Dynamic LINQ :

  1. Aggiungi utilizzando System.Linq.Dynamic; in cima.
  2. Uso: var people = people.AsQueryable().OrderBy("Make ASC, Year DESC").ToList();

Puoi anche ottenerlo da NuGet .

Metodi di estensione .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);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.