Linq to SharePoint. Создаем ContentIterator
В SharePoint 2010 для работы с большими списками есть класс Microsoft.Office.Server.Utilities.ContentIterator (Microsoft.Office.Server.dll), который позволяет итеративно обрабатывать элементы списка. Сегодня я покажу как можно реализовать аналогичный итератор при использовании Linq to SharePoint.
О самом ContentIterator'е можно прочитать у Алексея Садомова в посте Использование класса ContentIterator в разработке для Sharepoint.
Дроссельный контроль 2010
В SharePoint 2010 установлен лимит на выбор данных из списков/библиотек документов. По умолчанию лимит равен 5000 элементам. Если при выборе данных превысить его, то SharePoint выбросит исключение SPQueryThrottledException. И постраничная выборка в этом случае не учитывается. Для наглядности это можно представить в виде вот такой картинки.
В таком случае остается либо просить администратора приподнять эту планку, разрешив выбор большого количества данных либо использовать ContentIterator.
Но ContentIterator в SharePoint работает с объектами типа SPListItem, что делает невозможным применения модели данные, ориентированной на использование Linq to SharePoint.
Обход ограничения
Чтобы обойти это ограничение надо обрабатывать элементы пакетами, размер каждого из которых не будет превышать лимит. Ограничивать набор можно по полю ID. Оно есть всегда. Так же понадобятся минимальное и максимальное значения этого поля.
Мы будем перебирать элементы в цикле начиная с минимального значения поля ID, заканчивая максимальным, примерно вот так:
- var lastId = minId;
- for (var i = minId; i < maxId; i = i + throttleLimit)
- {
- // Выполняем операции над элементами
- }
throttleLimit здесь - это максимально допустимое количество элементов, извлекаемое за один проход цикла. Получить это значение для веб-приложения можно из свойства SPWebApplication.MaxItemsPerThrottledOperation .
ContentIterator
Теперь перейдем к самому итератору. Для обеспечения гибкости в работе итератор должен также иметь следующие параметры:
- Предикат для фильтрации данных. Его мы будем совмещать с ограничением по значению поля ID;
- Путь к папке из которой необходимо выбирать данные.
string.Empty
в случае, когда данные надо выбрать из корневой папки; - Флаг, указывающий на необходимость выбора данных из дочерних папок;
- Делегат для обработки элемента;
- Делегат для обработки исключения при его возникновении во время обработки элемента;
Чтобы привязать экземпляр итератора к конкретному списку и типу данных, также будем передавать итератору репозиторий, описывающий данный список и тип данных. Учитывая вышесказанное конструктор для будущего итератора будет выглядеть вот так:
- public SPLinqContentIterator(BaseRepository<TEntity, DataContext> repository,
- Expression<Func<TEntity, bool>> expression,
- string path, bool recursive,
- EntityProcessor entityProcessor,
- EntityProcessorErrorCallout entityProcessorErrorCallout)
- {
- //TODO
- }
Обработчик элемента будет возвращать флаг отмены операции для обеспечения возможности прервать итеративную обработку в любой момент:
- public delegate void EntityProcessor(TEntity entity, out bool isCancelled);
Обработчик для исключений выглядит аналогично:
- public delegate void EntityProcessorErrorCallout(TEntity entity,
- Exception exception, out bool isCancelled);
Итеративный обход данных в списке/библиотеке документов будет проходить синхронно. Реализовать асинхронный итератор у меня не получилось, т.к. пачками летели гайзенбаги.
Теперь сам обработчик:
- private static void ContentIteratorWorker(
- BaseRepository<TEntity, DataContext> repository,
- Expression<Func<TEntity, bool>> expression,
- string path, bool recursive,
- int leftId, int rightId,
- EntityProcessor entityProcessor,
- EntityProcessorErrorCallout entityProcessorErrorCallout,
- out bool cancelled)
- {
- // Флаг отмены итерации опущен по умолчанию
- cancelled = false;
- // Получаем элементы, подлежащие обработке
- var items = repository.GetEntityCollection(expression, path, recursive)
- // Накладываем ограничение по полю ID
- .Where(x => x.Id >= leftId && x.Id <= rightId)
- .ToList();
- foreach (var item in items)
- {
- // Если обработчик указан
- if (entityProcessor != null)
- {
- try
- {
- // Исполняем
- entityProcessor(item, out cancelled);
- // Сохраняем элемент
- repository.SaveEntity(item);
- }
- catch (Exception ex)
- {
- // Если указан обработчик исключения, то исполняем его
- if (entityProcessorErrorCallout != null)
- entityProcessorErrorCallout(item, ex, out cancelled);
- }
- }
- if (cancelled)
- {
- // Если флаг отмены поднят, то возвращаем управление
- return;
- }
- }
- }
Теперь можно реализовать итеративную обработку данных, продолжая при этом использовать Linq to SharePoint.
Применение
В заключении я продемонстрирую простоту применения этого итератора.
Для обработки списка Сотрудников (описание которого здесь) используя стандартный механизм SharePoint надо написать примерно вот такой код:
- using (var site = new SPSite("http://sharepoint"))
- {
- using (var web = site.OpenWeb())
- {
- var list = web.Lists["Employees"];
- var query = new SPQuery
- {
- Query = @"<Where><Eq><FieldRef Name='Sex' /><Value Type='Choice'>Male</Value></Eq></Where>"
- };
- var nativeIterator = new ContentIterator();
- nativeIterator.ProcessListItems(list, query, ProcessListItem, null);
- }
- }
-
- public static void ProcessListItem(SPListItem employee)
- {
- employee["AccessLevel"] = ((int)(employee["AccessLevel"] ?? 0)) + 1;
- employee.Update();
- }
Этот обработчик проходит по отфильтрованным элементам списка и изменяет значение поля AccessLevel. Аналогичная операция с использованием описанного здесь итератора будет выглядеть вот так:
- var repository = new EmployeeRepository("http://sharepoint", true, false);
- var iterator = new SPLinqContentIterator<Employee>(
- repository, // Репозиторий
- emp => emp.SexValue == "Male", // Предикат для фильтрации
- string.Empty, // Корневая папка
- true, // Просматривать дочерние папки
- ProcessEmployee, // Обработчик элемента
- null); // Обработчик исключений
- iterator.Begin();
-
- public static void ProcessEmployee(Employee employee, out bool cancelled)
- {
- employee.AccessLevel = employee.AccessLevel + 1;
- cancelled = false;
- }
Во втором случае код читается гораздо легче, обработчик принимает объект конкретного типа, а не SPListItem. К тому же этот итератор можно инкапсулировать в метод расширитель для типа EntityList&lst;T>, что-то вроде repository.GetEntityCollection().ForEach(...)
.
Производительность
Я проделал данные итеративные обработки для списка сотрудников, содержащего около 15К элементов, подстраивая выборку так, чтобы в результирующем наборе было разное количество элементов. И результат у меня получился вот такой:
В случае с ContentIterator'ом на обработку одного элемента уходило около 10 секунд, а в случае с SPLinqContentIterator'ом - около 3 секунд. Если доступ к данным списков SharePoint обеспечивает провайдер Linq to SharePoint, то реализовывать итеративную обработку элементов лучше, не отходя от Linq to SharePoint. Получается и быстрее и дешевле при дальнейшей поддержке.
Для случаев, когда необходимо итеративно обработать другие элементы (SPWeb, SPFolder и прочее) ContentIterator остается незаменимым.
Исходные коды
Обновленный проект можно скачать с CodePlex. Код класса SPLinqContentIterator можно посмотреть здесь.