Підкачка за допомогою LINQ для об'єктів


94

Як би ви реалізували підкачку у запиті LINQ? Насправді на даний момент я був би задоволений, якби можна було імітувати функцію sql TOP. Однак я впевнений, що потреба у повній підтримці пейджингового зв'язку все одно з’явиться раніше.

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

Відповіді:


233

Ви шукаєте методи розширення 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, який виконується.
Девід Пфеффер,

Роб Конери писав у блозі про клас PagedList <T>, який може допомогти вам розпочати роботу. blog.wekeroad.com/blog/aspnet-mvc-pagedlistt
jrotello

49
це призведе до пропуску першої сторінки, ЯКЩО pageNumber не дорівнює нулю (0). якщо pageNumber починається з 1, тому використовуйте цей ".Skip (numberOfObjectsPerPage * (pageNumber - 1))"
Елвін

Яким буде результуючий 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цього буде використано всю таблицю бази даних, що стане основним показником продуктивності.
Девід Пфеффер,

2
Звичайно, ви можете легко додати перевантаження для IQueryableтого, щоб він також працював із запитами databse (я відредагував відповідь і додав). Трохи прикро, що ви не можете написати код повністю загальним способом (у Haskell це було б можливо з класами типів). В оригінальному запитанні згадувалось LINQ to Objects, тому я написав лише одне перевантаження.
Томаш Петрічек

Я просто думав про те, щоб реалізувати це сам. Я трохи здивований, що це не є частиною стандартної реалізації. Дякуємо за зразок коду!
Майкл Річардсон,

1
Я думаю, що прикладом має бути: public static IQueryable <T> Page <T> (... etc
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 (внутрішньо з використанням пропозиції OFFSET)
Майкл Фрейдгейм,

4
Це просто потрібно було вкрасти і помістити в мою спільну бібліотеку, дякую! Я просто перейменував метод Paginateна видалення nounпроти verbдвозначності.
Габріелій

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

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:

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

РЕДАГУВАТИ - Видалено пропуск (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 не повертає жодних елементів.
Девід Пфеффер,

Можливо, вам також знадобляться дужки навколо запиту перед викликом Take. (з ... виберіть ...). Візьміть (10). Я викликав конструкцію з вибором рядка. Без дужок Take
взяв

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

Розмір партії, очевидно, буде цілим числом. Це використовує той факт, що цілі числа просто скидають десяткові знаки.

Я наполовину жартую з цією відповіддю, але вона зробить те, що ти хочеш, і, оскільки вона відкладена, ти не будеш штрафувати, якщо ти це зробиш

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

Це рішення не для LinqToEntities, я навіть не знаю, чи може це перетворити це на хороший запит.


3

Подібно до відповіді Луказоїда, я створив розширення для 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 };
  }

це те, що я зробив. Зазвичай ви починаєте з 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);
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.