大量のアイテムを削除する処理時間が長い場合の対処方法

みなさん、こんにちは。

今回はプログラムから大量のアイテムを削除する処理時間が長い場合の対処方法をご案内します。

SharePoint の開発を行うにあたって効率的な処理を実装する際の注意点として、多くのお客様に以下の資料をご参照いただいています。

参考資料 :

タイトル : オブジェクトの破棄

アドレス : https://msdn.microsoft.com/ja-jp/library/ee557362.aspx

タイトル : 規模の大きなフォルダーやリストの操作

アドレス : https://msdn.microsoft.com/ja-jp/library/ee557257.aspx

今回は上記の資料を補足する形として、大量のアイテムを削除する際に長時間を要する際の対処方法をご案内します。

- 背景について

SharePoint 上のアイテムを削除する際には、SPListItem クラスの Delete メソッドを使用します。

たとえば、「2011/10/25 以前に作成されたアイテムを全部削除したい!」という時、以下のように SPQuery を使用して対象のアイテムのコレクションを抽出して削除します。

----- サンプルここから -----

            SPSite site = new SPSite("https://moss");

            SPWeb web = site.OpenWeb();

            SPList list = web.GetListFromUrl("/Lists/TestList/AllItems.aspx");

            StringBuilder sb = new StringBuilder();

            sb.Append("<Where>");

            sb.Append("<Leq>");

            sb.Append("<FieldRef Name='Created'/>");

            sb.Append("<Value IncludeTimeValue='False' Type='DateTime'>");

            sb.Append("2011-10-25");

            sb.Append("</Value>");

            sb.Append("</Leq>");

            sb.Append("</Where>");

            SPQuery query = new SPQuery();

            query.Query = sb.ToString();

            query.RowLimit = 2000;

            SPListItemCollection itemCol = list.GetItems(query);

            int itemCount = itemCol.Count;

            for (int i = itemCount -1; i >=0 ; i--)

            {

                SPListItem item = itemCol[i];

                item.Delete();

            }

            web.Dispose();

            site.Dispose();

----- サンプルここまで -----

上記の処理そのものに問題ないのですが、以下の注意点があります。

SPListItemCollection 内部のアイテムの情報を参照する際に、コレクション内の全アイテムの情報をロードする処理が行われます。

SPListItemCollection の Count プロパティの値を取得する際や、インデクサを使用してSPListItemCollection 内のアイテムを参照する処理が該当します。

「1 件目のアイテムを取得する時になんか遅いな~」と感じることがある場合も多いと思いますが、この処理が原因なのです。この処理を実現するために、SPListItemCollection 内部では Dirty フラグを持っています。この Dirty フラグが true であれば、全アイテムを取得する処理が行われます。必要な処理ではあるのですが、アイテムの数が多い場合には非常にコストがかかる処理になります。

さらに、SPListItemCollection クラスを扱う際には、もう一つ注意点があります。

SPListItem は親となる SPListItemCollection の参照を保持しており、SPListItem の Delete メソッドを実施すると、親となる SPListItemCollection に対して "Dirty" フラグを立てる処理を行います。

このため、アイテムを削除した後に同じ SPListItemCollection 内の要素にアクセスすると、"Dirty" フラグが立っているため、SPListItemCollection 内のデータをすべて再取得する処理が行われます。

そのため、以下のようなループ処理で、2000 件のアイテムの削除を行う際には、

            for (int i = itemCount -1; i >=0 ; i--)

            {

                SPListItem item = itemCol[i];  // *1

                item.Delete(); // *2                    

            }

*1 で、SPListItemCollection 内のアイテムを全件取得

*2 で、SPListItemCollection 内の Dirty フラグを立てる

という処理が繰り返されるため、ループ処理の中で 2000 件分のデータを取得、次に 1999 件分のデータを取得、その次に 1998 件分のデータを取得、、、という処理を繰り返すことになるため、パフォーマンスが著しく低下します。

- 対処方法について

さて、ここからが対処方法になりますが、上記の SPListItem の Delete 処理を以下のメソッドを使用して別のバッチプロセスを使用して置き換えるという方法になります。

参考資料 :

タイトル : SPWeb.ProcessBatchData メソッド (Microsoft.SharePoint)

アドレス : https://msdn.microsoft.com/ja-jp/library/microsoft.sharepoint.spweb.processbatchdata(office.12).aspx

抜粋 : トランザクションごとに、指定されたバッチ コマンド文字列を処理し、サーバーに複数の要求を送信します。

この方法では、SPListItemCollection の Dirty フラグを更新しないため、上記のような現象は発生しません。この処理を組み込んだサンプルが、以下になります。

----- サンプルここから -----

        public void DeleteTest()

        {

            SPSite site = new SPSite("https://moss");

            SPWeb web = site.OpenWeb();

            SPList list = web.GetListFromUrl("/Lists/Blog5000List/AllItems.aspx");

            StringBuilder sb = new StringBuilder();

            sb.Append("<Where>");

            sb.Append("<Leq>");

            sb.Append("<FieldRef Name='Created'/>");

            sb.Append("<Value IncludeTimeValue='False' Type='DateTime'>");

            sb.Append("2011-10-25");

            sb.Append("</Value>");

            sb.Append("</Leq>");

            sb.Append("</Where>");

           

            SPQuery query = new SPQuery();

            query.Query = sb.ToString();

            query.RowLimit = 2000;

            SPListItemCollection itemCol = list.GetItems(query);

            int itemCount = itemCol.Count;

            foreach (SPListItem item in itemCol)

            {

                int id = item.ID;

                DeleteItemProcessBatch(web, list, id);

            }

            web.Dispose();

            site.Dispose();

        }

        public void DeleteItemProcessBatch(SPWeb web, SPList list, int itemId)

        {

            StringBuilder sb = new StringBuilder();

            sb.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

            sb.Append("<ows:Batch OnError=\"Return\">");

            sb.Append("<Method ID=\"0\">");

            sb.Append("<SetList>");

            sb.Append(list.ID.ToString());

            sb.Append("</SetList>");

            sb.Append("<SetVar Name=\"Cmd\">DELETE</SetVar>");

            sb.Append("<SetVar Name=\"ID\">");

            sb.Append(itemId.ToString());

            sb.Append("</SetVar>");

            sb.Append("</Method>");

            sb.Append("</ows:Batch>");

            web.ProcessBatchData(sb.ToString());

        }

----- サンプルここまで -----

ちなみに、どれくらい処理時間が短縮できたかというと、

SPListItem.Delete を使用した場合

20分 48 秒

SPWeb.ProcessBatchData を使用した場合

46 秒

約 27 倍の時間短縮 (当社比) になりました!

※ 環境概要 :

カスタムリストに "複数行テキスト" 型の列を 10 個追加

10 個の列それぞれに 200 文字を入力したアイテムを 2000 件登録

今回のような状況に該当する場合にはぜひご活用ください。