Wednesday 21 December 2016

Управление большими списками и библиотеками в SharePoint

Управление большими списками и библиотеками в 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();
                    }
                }
            }
В реальном решении третий вариант работы с большими данными прекрасно себя зарекомендовал. Единственное замечание - пожалуйста, не забывайте индексировать столбцы списков или библиотек, которые участвуют в фильтрах запросов. Ну, это стандартные рекомендации😃. Они актуальные и разумные для всех случаев😃😃😃😃😃😃