DocumentDB でページング処理を行う

「データを一覧してページング表示する」という要件は エンプラ系でも コンシューマー系でも 常にある要件です。
ということで、DocumentDBでデータのページング取得を行う方法を書いておこうと思います。

1. 前提

DocumentDBアカウントには、既に以下のデータが保存されている状態を前提とします。

データベース名: ExampleDB1
コレクション名: ExampleCollection1
ドキュメント: ProductItemドキュメントが1,000件保存されている

ProductItemは以下のようなデータクラスです。

// 保存するドキュメント
using Newtonsoft.Json;

namespace PagingExample
{
  public class ProductItem
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    public string Name { get; set; }

    public int Price { get; set; }

    public int StockNumber { get; set; }
  }
}

以下のような感じで、適当なデータが入っています。

f:id:daigo-knowlbo:20170507003752p:plain

2. DocumentDB操作管理クラスを用意

DocumentDBを操作する管理クラス(DocumentManager)の実装は以下の通りです。

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Linq;
using Microsoft.Azure.Documents.Client;
using System.IO;
using System.Threading.Tasks;

namespace PagingExample
{
  public class DocumentManager
  {
    private const string EndpointUrl = "https://rddocdb.documents.azure.com:443/";
    private const string PrimaryKey = "[マスターキー]";

    private const string DatabaseID = "ExampleDB1";
    private const string CollectionID = "ExampleCollection1";

    private DocumentClient client;

    // DocumentClientオブジェクトを初期化
    public async Task<bool> InitializeDocumentClient()
    {
      this.client = new DocumentClient(new Uri(DocumentManager.EndpointUrl), DocumentManager.PrimaryKey,
        new ConnectionPolicy
        {
          ConnectionMode = ConnectionMode.Gateway,
          ConnectionProtocol = Protocol.Https
        });

      return true;
    }

    // ProductItemを 10件 ずつ取得
    public async Task<(List<ProductItem>, string)> QueryProductItems(string continuration)
    {
      List<ProductItem> result = null;

      var feedOption = new FeedOptions() {
        MaxItemCount = 10,
        RequestContinuation = continuration };

      Uri documentCollectionUri = 
        UriFactory.CreateDocumentCollectionUri(
          DocumentManager.DatabaseID, DocumentManager.CollectionID);
      var query =
        this.client.CreateDocumentQuery<ProductItem>(
          documentCollectionUri, feedOption)
          .Where( p => p.Price > 700 )
          .AsDocumentQuery();

      var ret = await query.ExecuteNextAsync<ProductItem>();
      
      result = ret.ToList();
      string retContinuration = ret.ResponseContinuation;

      return (result, retContinuration);
    }
  }
}

InitializeDocumentClient()メソッド

DocumentClientオブジェクトを new してメンバーフィールドに保持します。

QueryProductItems()メソッド

引数 continuration は、DocumentDBに「どのページ(というかどの位置から継続抽出するか)」の状態文字列です。1ページ目の取得時には「空文字列」を設定します。
CreateDocumentQuery()を呼び出す際の引数「FeedOptions」で、「1度に取得する件数(MaxItemCount)」と「どのページ(どこから継続取得するかを表す状態文字列(RequestContinuation )」を指定します。
AsDocumentQuery()呼び出して、ExecuteNextAsync()呼び出した結果から、10件の抽出データ(ret.ToList()) と 次のページを読み込む為の継続取得状態文字列(ret.ResponseContinuation)が得られます。
これら2つのデータをタプルで戻り値として呼び出し元に返却しています。
また、サンプルなので Price > 700 という固定的なWhere条件を付加しています。

3. DocumentManagerを呼び出してページングデータ取得

では、前述で用意した DocumentManager を利用してProductItemデータを10件ずつページング取得したいと思います。
以下が実装になります。

var manager = new DocumentManager();
var initResult = await manager.InitializeDocumentClient();

// コンソール出力用ページ番号
int pageNo = 0;

// DocumentDBに投げるcontinuration値(1ページ目は空文字列)
string continuration = "";

do
{
  (List<ProductItem> items, string continuration) queryResult = 
    await manager.QueryProductItems(continuration);

  // 次のページを取得するためのcontinuration文字列取得
  continuration = queryResult.continuration;

  // 取得したデータをコンソール出力
  Console.WriteLine(string.Format("page {0}", pageNo));
  Console.WriteLine(string.Format("continuration {0}", continuration));
  foreach (var item in queryResult.items)
  {
    Console.WriteLine(string.Format("{0} {1}", item.Name, item.Price));
  }
  Console.WriteLine("-----");
  pageNo++;
} while (!string.IsNullOrEmpty(continuration)); // continurationが空だと終端ページに達したということ

データ抽出で得られた(Responseされた)Continuation値を、次のデータ抽出(Request)で利用する形です。

4. 実行結果

実行結果は以下の通りです。

page 0
continuration {"token":"+RID:mi01AOyHIgA7AAAAAAAAAA==#RT:1#TRC:10#FPC:AgEAAAB8AD8AABhwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABLcAHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAE=","range":{"min":"","max":"FF"}}
Product_8 800
Product_9 900
Product_18 800
Product_19 900
Product_28 800
Product_29 900
Product_38 800
Product_39 900
Product_48 800
Product_49 900
-----
page 1
continuration {"token":"+RID:mi01AOyHIgBtAAAAAAAAAA==#RT:2#TRC:20#FPC:AgEAAAB2AG8AAGDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHKsAccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAE=","range":{"min":"","max":"FF"}}
Product_58 800
Product_59 900
Product_68 800
Product_69 900
Product_78 800
Product_79 900
Product_88 800
Product_89 900
Product_98 800
Product_99 900
-----
page 2
continuration {"token":"+RID:mi01AOyHIgCfAAAAAAAAAA==#RT:3#TRC:30#FPC:AgEAAABuAJ+ANcABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMAB","range":{"min":"","max":"FF"}}
Product_108 800
Product_109 900
Product_118 800
Product_119 900
Product_128 800
Product_129 900
Product_138 800
Product_139 900
Product_148 800
Product_149 900
-----
page 3
continuration {"token":"+RID:mi01AOyHIgDRAAAAAAAAAA==#RT:4#TRC:40#FPC:AgEAAABoAN8ABhxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABI8AHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMAB","range":{"min":"","max":"FF"}}
Product_158 800
Product_159 900
Product_168 800
Product_169 900
Product_178 800
Product_179 900
Product_188 800
Product_189 900
Product_198 800
Product_199 900
-----
page 4
continuration {"token":"+RID:mi01AOyHIgADAQAAAAAAAA==#RT:5#TRC:50#FPC:AgEAAABiAA8BGHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHIMAccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMABBxxwwAEHHHDAAQcccMAB","range":{"min":"","max":"FF"}}
Product_208 800
Product_209 900
Product_218 800
Product_219 900
Product_228 800
Product_229 900
Product_238 800
Product_239 900
Product_248 800
Product_249 900
-----

...省略...

-----
page 18
continuration {"token":"+RID:mi01AOyHIgC-AwAAAAAAAA==#RT:19#TRC:190#FPC:AgEAAAAKAL+DA8ABBxxwwAE=","range":{"min":"","max":"FF"}}
Product_908 800
Product_909 900
Product_918 800
Product_919 900
Product_928 800
Product_929 900
Product_938 800
Product_939 900
Product_948 800
Product_949 900
-----
page 19
continuration
Product_958 800
Product_959 900
Product_968 800
Product_969 900
Product_978 800
Product_979 900
Product_988 800
Product_989 900
Product_998 800
Product_999 900
-----
続行するには何かキーを押してください . . .

5. HTTP通信内容

DocumentDBライブラリにより、裏側で具体的にどのようなREST API リクエストとレスポンスが行われたのかをFiddlerで確認します。

f:id:daigo-knowlbo:20170507012230p:plain

上記Fiddler画面から以下のことが分かります。

  • 「/dbs/ExampleDB1/colls/ExampleCollection1/docs」へのページごとのリクエストが行われた
  • FeedOptions.MaxItemCount / FeedOptions.RequestContinuation の値がHTTPリクエストヘッダでそれぞれ「x-ms-max-item-count」「x-ms-continuation」して指定されている
  • HTTPレスポンスヘッダ「x-ms-continuation」で、次のページを読み込むための継続状態文字列が返されている

資料

公式ドキュメントでは以下を見ればいいのかな。

docs.microsoft.com

Common DocumentDB REST request headers | Microsoft Docs

Common Azure Cosmos DB REST response headers | Microsoft Docs