Управление большими списками и библиотеками в SharePoint очень актуальная и востребованная тема, как для администраторов, так и для программистов.
И, если для администратор SharePoint в сети опубликовано достаточно качественного (ну, качество материала суждение субъективное😆, и я об этом немного после порассуждаю) справочного материала, например этот, то программистам немного сложнее изыскать соответствующий материал.
Более того, подходы к работе с большими данными в SharePoint Online и SharePoint 2013 (2016) разный. Об этом, кстати, совершенно справедливо сказано в этой статье.
Сегодня я планирую описать свой опыт при работе с большими данными в SharePoint 2013 с использованием Business Connectivity Services (BCS) компонентов на стороне сервера.
Да, именно в этом случае проблемы работы с большими объемами списков или библиотек наиболее актуальны, так как приходиться самостоятельно формировать запрос к источнику данных.
Известно, что в SharePoint 2013(2016) и SharePoint Online существуют пороговое ограничение получения списка или библиотеки в 5000 элементов, которое можно регулировать. В SharePoint Online возможность регулирования порогового ограничения отсутствует. В случае, если представление списка или библиотеки возвращает число элементов большее порогового ограничения, то будет сгенерировано исключение.Да, именно в этом случае проблемы работы с большими объемами списков или библиотек наиболее актуальны, так как приходиться самостоятельно формировать запрос к источнику данных.
Вот теперь подробнее о том, как следует построить решение (SPSolution) SharePoint, которое будет запрашивать большое число элементов и не создавать исключений превышения порогового значения, установленного администратором портала☺:
Мы не будем обсуждать варианты увеличения порогового знания на глобальном уровне в центре администрирования. Как известно, по умолчания это 5000 элементов.
Потому как любое увлечения порогового значения проблему решит на локальном уровне, но одновременно с этим может снизить производительность портала SharePoint в целом. Кроме этого, это единственный вариант, который доступен администраторам портала SharePoint, у программистов же возможности гораздо больше.
Первый вариант. Свойство QueryThrottleMode класса SPQuery:
Перечислитель QueryThrottleMode содержит 3 варианта использования:1. Default
2. Override
3. Strict
Для нас интересен параметр Override. Как сказано в документации - "Значение перечисления для переопределения: 1. Если пользователь является локальным администратором на сервере без ограничения полосы пропускания будет применяться в запросе. Если политика безопасности приложения web предоставляет разрешения чтение или полный доступ пользователя предел регулирования для аудиторов и администраторов будет применяться к числа элементов, включенных в запрос и регулирование не будет применяться к число подстановки, пользователей или групп и полей состояния рабочего процесса. В противном случае параметр [Microsoft.SharePoint.SPQueryThrottleOption.SPQueryThrottleOption.Default] применяется к запросу."
На практике перечислитель QueryThrottleMode используется следующим образом (снятие ограничения на уровне запросов):
//создаем ссылку на список
SPList list = web.Lists["ListName"];
//создаем запрос
SPQuery query = new SPQuery(); query.Query = @"<View Scope='RecursiveAll'> <ViewFields> <FieldRef Name='ID' /> <FieldRef Name='Title' /> </ViewFields> <Query> <Where> <Eq> <FieldRef Name='Title' /><Value Type='Text'>текст для фильтра</Value> </Eq> </Where> </Query> </View>";
//меняем перечислить запроса query.QueryThrottleMode = SPQueryThrottleOption.Override;
//создаем ссылку на массив строк SPListItemCollection results = list.GetItems(query);
//далее используем переменную results по назначению, например так
foreach (SPListItem item in results) { //здесь ваш код....👧 }
Вот и все. Следует добавить, что данный код будет работать, как следует только тогда, когда запрос выполняет пользователь с привилегированными правами. Это администраторы ферм или те, кому предоставлен полный доступ к запрашиваемым ресурсам. Не очень удобная ситуация, да 😆, тем более, эти пользователи и так имеют привилегии для просмотра ресурсов, предоставленные им методами регулирования ресурсов в центре администрирования портала. И поэтому, указанные выше метод, мягко говоря непрактичен 😄😄😄😄😄😄.
Конечно, программисты SharePoint, могут обернуть код в конструкцию SPSecurity.RunWithElevatedPrivileges и тогда все запросы будут осуществляться от имени администратора портала. Но и это, не очень умное решения, хотя бы с точки зрения производительности и безопасности запросов😂. Но не все так печально, так как у нас имеется вариант № 2 😆
Второй вариант ContentIterator:
Класс ContentIterator предоставляет методы для выполнения запросов элементов списка, списков и веб-узлов для регулирования объема передаваемых данных (т.е., чтобы избежать возникновения SPQueryThrottledException).
В справке класса приведен небольшой пример его использования. Поэтому я не буду ссылаться на код из своей практики. Они, практически идентичны. Замечу только, что путем опытных экспериментов я выяснил, что для меня класс не совсем приемлем, ну, хотя бы потому, что у меня с помощью этого класса не получилось "практично" запросить данные списка по нескольким столбцам. "Выдача" осуществляется только по одному столбцу. И этот столбец точно не может иметь тип Note. Поэтому я представлю третий вариант, который на мой взгляд, самый "практичный", по крайней мере. для меня😄😄😄😄😄😃😜
Третий вариант BatchQueryExector:
Итак, меня не устроил первый вариант и второй вариант, следовательно нужно мастерить нечто среднее между ними, позаимствовав у них все самое лучшее 😃. Я обратился за помощью к google, который привел меня к статье автор Charles Chen. Там описан класс, который и выполняет всю работу, так как это хотел я. Если коротко - класс запрашивает строки списков постранично. В общем ничего нового в подходах в работе с SQL данными, только подход перенесен на посредника😅. Я практически ничего не менял в классе, кроме метода GetItems. В моем варианте он выглядит так:
/// <summary> /// Извлекает элементы в списке в пакетах, основанных на <c>RowLimit</c> и /// вызывает обработчик для каждого элемента. /// </summary> /// <param name="handler">Метод, который вызывается для каждого элемента.</param> public void GetItems(Action<SPListItem> handler, bool Single = false) { string pagingToken = string.Empty; while (true) { _query.ListItemCollectionPosition = new SPListItemCollectionPosition(pagingToken); SPListItemCollection results = _list.GetItems(_query); foreach (SPListItem item in results) { handler(item); } if (results.ListItemCollectionPosition == null || Single) { break; // EXIT; no more pages. } pagingToken = results.ListItemCollectionPosition.PagingInfo; }
}
Я думаю, что тот, кто сравнит исходник с моим вариантом, поймет "в чем новшество" и зачем?😃😏
Здесь я приведу пример использования класса. Пример из реального решения без ретуши и изменений😃:
Первый, где нужно получить только одну строку
using ( SPSite siteNew = new SPSite ( this.SiteName ) ) { using ( SPWeb webNew = siteNew.OpenWeb ( ) ) { try { string title = ""; SPList list = webNew.Lists[MConst.MFC_WikiChita_BZnaniy];//находим список SPQuery query = QueryBZnaniy.GetQuery(Single:true, ID: id); BatchQueryExector.WithQuery(query).OverList(list).GetItems(item => { if (item != null) { title = item.Title; } },true); return title; } catch (Exception e) { perror.Visible = true; perror.CssClass = "padding-10 ui-state-error"; lerror.Text = e.ToString(); SendEmail.SendErr(webNew, e.ToString()); return "null"; } finally { if (webNew != null) webNew.Dispose(); } } }
Второй, где надо получить массив строк:
using ( SPSite siteNew = new SPSite ( this.SiteName ) ) { using ( SPWeb webNew = siteNew.OpenWeb ( ) ) { try { SPList list = webNew.Lists[MConst.MFC_WikiChita_BZnaniy];//находим список var query = QueryBZnaniy.GetQuery(); List<BZnaniy> it = new List<BZnaniy> ( ); BatchQueryExector.WithQuery(query).OverList(list).GetItems(item => { if (item != null) { SPFieldUrlValue fieldValue =new SPFieldUrlValue(item["URL"].ToString()); SPFieldUrlValue URL_BZ_Site =new SPFieldUrlValue(item["URL_BZ_Site"] != null ? item["URL_BZ_Site"].ToString() : null); string title =item["Title"] != null && !string.IsNullOrEmpty(item["Title"].ToString()) ?item["Title"].ToString().ToUpper() : "[нет данных]"; it.Add(new BZnaniy { id = item.ID, Title = title, UrlImage = fieldValue != null ? fieldValue.Url : "#", UrlSite =URL_BZ_Site != null && !string.IsNullOrEmpty(URL_BZ_Site.Url) ?URL_BZ_Site.Url : this.Page.Request.Url.AbsolutePath + "?BZnaniyId=" + item.ID; }); } }); return it; } catch ( Exception e ) { perror.Visible = true; perror.CssClass = "padding-10 ui-state-error"; lerror.Text = e.ToString ( ); SendEmail.SendErr(webNew, e.ToString()); return new List<BZnaniy> ( ); } finally { if (webNew != null) webNew.Dispose(); } } }
В реальном решении третий вариант работы с большими данными прекрасно себя зарекомендовал. Единственное замечание - пожалуйста, не забывайте индексировать столбцы списков или библиотек, которые участвуют в фильтрах запросов. Ну, это стандартные рекомендации😃. Они актуальные и разумные для всех случаев😃😃😃😃😃😃