Azure Cosmos DB入門(3)
本コンテンツは「Azure Cosmos DB入門」の(3)です
- 3 Cosmos DBプログラミング ~ DocumentDB編(前編)
- 3.4 ドキュメントの検索
- 3.5 ドキュメントの削除
- 3.6 つづく・・・
- 3.7 資料
3 Cosmos DBプログラミング ~ DocumentDB編(前編)
ここからはCosmos DBに対して具体的な操作を行うプログラムについて説明していきます。
DocumentDB編・MongoDB編・Graph(Gremlin編)・Table編という形で、アクセスAPI毎に分けて説明します。
3.1 まず、はじめに
まずは、DocumentDBにおけるクエリー操作の基本概念について説明します。
3.1.1 DocumentDBとSQL
DocumentDB(ドキュメント データモデル)に対するクエリーの実行は「SQL構文」で行われます。
SQLは昔からのRDBで馴染みのある、おそらくデータベースに関連した開発の経験があるディベロッパーならだれもが知るデータ操作のための言語です。
3.1.2 REST API(DocumentDB API) と クラスライブラリ
まず、「REST API(DocumentDB API) と クラスライブラリ」という点について説明しておきます。
Cosmos DB(DocumentDB)では、「データベースの作成」や「コレクションの作成」、また「ストアドプロシージャの作成」のような ”低レベル(データベースの構造を操作する)機能” から、「ドキュメントの検索」のような ”アプリケーションレベルの機能” までを「REST API」で提供しています。
つまり、HTTP GET / POSTによってDocumentDB(Cosmos DB)に対する大半の操作が可能になっています。
また、DocumentDB(ドキュメント データモデル)に対するクエリーにはSQLが利用されると説明しましたが、RESTのPOSTパラメータとしてSQL文がリクエストされる仕組みを取ります。
そして、DocumentDB(Cosmos DB)を利用するプログラムを作成する場合、以下の2つの方法があります。
REST API(DocumentDB REST API) を呼び出すプログラムを書く
HTTP通信の記述が可能なプログラム言語であれば、どんな環境でもDocumentDBを利用した実装を行う事が可能です。ただしこの場合、「URI文字列を構築し、POSTパラメータを作成し、必要なHTTP Headerを付加してリクエストを行い、応答のHTTPコードが200であることを確認し、応答されたJSONデータを利用(必要であればC# POCOにデシリアライズする処理を記述)」という様な煩雑な処理を行う必要があります。クラスライブラリを使用する
煩雑なHTTP通信処理をラップしたクラスライブラリを利用することになります。クラスライブラリのメソッドを呼び出すと、バックエンドで煩雑なRESTの通信を行ってくれます。
実際の開発では多くの場合クラスライブラリを利用した実装を行う事になると思います。しかし、実開発において一歩踏み込んだ実装を行っていく場合、クラスライブラリにラップされたREST通信を理解しておく必要があるケースもあります。そこでCosmos DBの学習や開発を進める上では、REST APIを知る事、またはFiddlerなどでどのような通信が行われているのかを調べる事も重要になります。
この後の説明では「言語はC#」「クラスライブラリ利用」を使用することをベースに話を進めます。
3.2 準備
3.2.1 Cosmos DB(DocumentDB)データベースアカウントの作成
Azureポータルを利用して「Cosmos DB(DocumentDB)」を作成します。
「CosmosDB入門(1)~(2)」でも、すでに触れているので、簡単に・・・
Azureポータルをブラウザで表示し「新規」を選択。検索テキストボックスに「Azure Cosmos DB」と入力。
「Azure Cosmos DB」を選択。
「作成」をクリック。
アカウント情報を入力して「作成」をクリックします。
ここでは以下の設定を行いました。
ID: cosmosdoc
API: SQL(DocumentDB)
リソースグループ: cosmosdoc
場所: 西日本
以下のCosmos DBアカウントが作成されます。
3.2.2 Visual Studio 2017プロジェクトの作成
Visual Studio 2017を起動し、メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
ここでは、プロジェクトテンプレートは「コンソールアプリケーション」、名前は「CosmosDocDBExample」としました。
次にCosmos DB(DocumentDB)にアクセスるつためにNuGetパッケージライブラリをプロジェクトに追加します。
ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックし、表示されたメニューから「NuGetパッケージの管理を選択します。
表示されたNuGetパッケージ管理画面から、左上の「参照タブ」を選択し、検索テキストボックスに「Microsoft.Azure.DocumentDB」と入力します。
一覧に Microsoft.Azure.DocumentDB が表示されるので、選択して「インストール」ボタンをクリックします。
以上で、プロジェクトの下準備が完了しました。
3.2.2 データベースとコレクションの作成
ここでは、Cosmos DB(DocumentDB)操作を管理するクラスとして DocumentDbManagerクラス を用意することとします。
プロジェクトに DocumentDbManager.cs を追加します。
(1)接続情報等の定数定義
「接続情報」及びこれから「作成するデータベース名・コレクション名」をDocumentDbManagerクラスに定数として定義します(リスト 1)。
リスト 1 // DocumentDbManagerに定数定義を追加 private const string EndpointUrl = "【URI】"; private const string PrimaryKey = "【キー】"; private const string DatabaseId = "GroupwareDB"; private const string CollectionId = "RoomReservations"; // 上記は各値をコードに埋め込んでいますが、実運用コードでは構成から読み取る等の工夫が必要
【URI】【キー】は各データベースアカウントごとに適切な値を設定します。
Azureポータルで「キー」タブを選択することで確認することができます。
データベースIDは「GroupwareDB」、コレクションIDは「RoomReservations」としました。これはサンプルとして、グループウェアにおける会議室予約のデータベースを想定します。
【コラム】プライマリキーとセカンダリキー
Azureポータル上で確認可能なデータベースアカウントに紐づいたキーは、プライマリキーとセカンダリキーの2つのキーがありました。
これらはどちらのキーを使用してもCosmos DBに接続することができます。
実運用時にはセキュリティ考慮の目的で、一定期間でキーを更新する方針が持たれることがあります。
稼働中のシステムを停止することなくキーの更新を行うためにセカンダリキーが用意されています。
例えば以下のような手順により、運用を止めることなくキーの更新が可能です。
(アクセスキーのローリング)
- システムが利用するキーをセカンダリキーに切り替える
- プライマリキーを再作成(Azureポータル上で実施可能。数十秒程度で再作成完了)
- システムが利用するキーをプライマリキーに切り替える
- セカンダリキーも再作成
(2)基本名前空間のusing定義
DocumentDB接続・操作を行う上での基本的な名前空間をusing定義しておきます(リスト 2)。
リスト 2 ... // 一般的に利用する名前空間 using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; // DocumentDB関連の名前空間 using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client;
(3)接続クライアントオブジェクトの定義
DocumentDBに接続する為の接続クライアント「Microsoft.Azure.Documents.Client.DocumentClient」をDocumentDbManagerクラスのフィールド変数「client」として定義します(リスト 3)。
また、clientはコンストラクタで初期化することとします。DocumentClientのコンストラクタには「データベースアカウントのエンドポイント」「キー」を引き渡します。
リスト 3 public class DocumentDbManager { private DocumentClient client = null; public DocumentDbManager() { this.client = new DocumentClient( new Uri(DocumentDbManager.EndpointUrl), DocumentDbManager.PrimaryKey); } ... }
(4)データベース作成メソッドの追加
DocumentDbManagerクラスにデータベースを作成するメソッド「CreateDatabase()」を定義します(リスト 4)。
リスト 4 // データベースの作成 public async Task<Database> CreateDatabase() { // データベースを作成 Database database = await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DocumentDbManager.DatabaseId }); return database; }
データベースの作成は「DocumentClient.CreateDatabaseIfNotExistsAsync()メソッド」で行うことができます。
引数は「Microsoft.Azure.Documents.Databaseオブジェクト」です。「Idプロパティ」が、作成するデータベースIDとなります。
メソッド名の通り「まだ存在しなかったらデータベースを作成(+データベース情報の返却)」を行います。データベースが既に存在したらデータベース情報の返却のみを行います。
以下が DocumentDbManager.CreateDatabase() 呼び出しコードです(リスト 5)。
(同期メソッドから非同期メソッドを呼び出しているので Wait() 呼び出しを付けています)
リスト 5 static void Main(string[] args) { var manager = new DocumentDbManager(); manager.CreateDatabase().Wait(); }
【コラム】CreateDatabaseIfNotExistsAsync()とREST API
DocumentDBへのアクセスには、REST APIとクラスライブラリが用意されていることは既に説明しました。
CreateDatabaseIfNotExistsAsync()メソッドはバックエンドでREST APIの呼び出しを行っています。
Fiddlerを使ってその様子を確認してみましょう。
以下がFiddlerのキャプチャです。2つのHTTPSリクエストが「https://cosmosdoc-japanwest.documents.azure.com」に送信されたのが分かります。
2つのHTTPS通信の詳細は以下の通りです。
① の HTTPSリクエスト GET https://cosmosdoc-japanwest.documents.azure.com/dbs/GroupwareDB HTTP/1.1 x-ms-date: Sat, 27 May 2017 10:20:14 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3dmdkOeMQIUPlqwrwmFsBv%2fWVWegqmjNfUvT%2f0D89IhPY%3d Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-02-22 Accept: application/json Host: cosmosdoc-japanwest.documents.azure.com ↓↓↓ 上記に対するHTTPSレスポンス ↓↓↓ HTTP/1.1 404 Not Found Transfer-Encoding: chunked Content-Type: application/json Content-Location: https://cosmosdoc-japanwest.documents.azure.com/dbs/GroupwareDB Server: Microsoft-HTTPAPI/2.0 x-ms-last-state-change-utc: Sat, 27 May 2017 01:07:57.846 GMT x-ms-schemaversion: 1.3 x-ms-xp-role: 1 x-ms-request-charge: 2 x-ms-serviceversion: version=1.14.23.1 x-ms-activity-id: 9f6459d5-12d8-441d-95ac-38b8b831562e x-ms-session-token: 0:467 Strict-Transport-Security: max-age=31536000 x-ms-gatewayversion: version=1.14.23.1 Date: Sat, 27 May 2017 10:20:13 GMT 136 {"code":"NotFound","message":"Message: {\"Errors\":[\"Resource Not Found\"]}\r\nActivityId: 9f6459d5-12d8-441d-95ac-38b8b831562e, Request URI: /apps/bfb8961c-dcec-4c64-b341-400abf86ebdb/services/48b5f134-d51a-4e54-8546-231b6ae8eb02/partitions/d1768b54-6a08-48ad-916b-b27cfcd326f5/replicas/131397004676902200s"} 0
② の HTTPSリクエスト POST https://cosmosdoc-japanwest.documents.azure.com/dbs HTTP/1.1 x-ms-date: Sat, 27 May 2017 10:20:14 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3dOfaltgioZC7s9CkkUg0rb%2b9WESuLoYUaH4cuMuIdpvQ%3d Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-02-22 Accept: application/json Host: cosmosdoc-japanwest.documents.azure.com Content-Length: 20 Expect: 100-continue {"id":"GroupwareDB"} ↓↓↓ 上記に対するHTTPSレスポンス ↓↓↓ HTTP/1.1 201 Created Cache-Control: no-store, no-cache Pragma: no-cache Transfer-Encoding: chunked Content-Type: application/json Server: Microsoft-HTTPAPI/2.0 Strict-Transport-Security: max-age=31536000 x-ms-last-state-change-utc: Sat, 27 May 2017 03:39:16.214 GMT etag: "00000701-0000-0000-0000-592952de0000" x-ms-resource-quota: databases=100; x-ms-resource-usage: databases=1; x-ms-schemaversion: 1.3 x-ms-quorum-acked-lsn: 467 x-ms-current-write-quorum: 3 x-ms-current-replica-set-size: 4 x-ms-xp-role: 1 x-ms-request-charge: 4.95 x-ms-serviceversion: version=1.14.23.1 x-ms-activity-id: a5ed7f76-deb4-44a5-b7e5-d229420e89d2 x-ms-session-token: 0:468 x-ms-gatewayversion: version=1.14.23.1 Date: Sat, 27 May 2017 10:20:14 GMT AA {"id":"GroupwareDB","_rid":"GA8GAA==","_self":"dbs\/GA8GAA==\/","_etag":"\"00000701-0000-0000-0000-592952de0000\"","_colls":"colls\/","_users":"users\/","_ts":1495880413} 0
1つめのHTTPSリクエストは、「https://cosmosdoc-japanwest.documents.azure.com/dbs/GroupwareDB」(/dbs/GroupwareDはデータベースを表すリソースURI)に対してGETが行われています。
つまり以下の意味を持ちます。
- 西日本リージョンの cosmosdoc データベースアカウント配下の GroupwareDB というIDのデータベース情報を取得する
結果は404 NotFoundでした。つまり対象のデータベースが存在しないという結果が応答されました。
CreateDatabaseIfNotExistsAsync()メソッドは、対象のデータベースが存在しなかったので作成処理を行います。それが2つ目のHTTPリクエストです。
「https://cosmosdoc-japanwest.documents.azure.com/dbs」に対して、パラメータ「{“id”:“GroupwareDB”}」でPOSTが行われています。
つまり以下の意味を持ちます。
- 西日本リージョンの cosmosdoc データベースアカウント配下に GroupwareDB というIDのデータベースを作成する
結果は201 Created。BODY内に作成したデータベースに関する情報をJSON形式で返却してくれました。
(5)コレクション作成メソッドの追加
DocumentDbManagerクラスにコレクションを作成するメソッド「CreateCollection()」を定義します(リスト 6)。
リスト 6 // コレクションの作成 public async Task<DocumentCollection> CreateCollection() { // パーティションキー指定あり DocumentCollection collection = new DocumentCollection(); collection.Id = DocumentDbManager.CollectionId; collection.PartitionKey.Paths.Add("/Room"); // スループットは 2500RU RequestOptions options = new RequestOptions(); options.OfferThroughput = 2500; // コレクションを作成 collection = await this.client.CreateDocumentCollectionIfNotExistsAsync( UriFactory.CreateDatabaseUri(DocumentDbManager.DatabaseId), collection, options); return collection; }
DocumentCollection
DocumentCollectionオブジェクトにより、作成するコレクションの情報を設定します。
DocumentCollection.Idは作成するコレクションのIDであり、ここでは事前に定数定義を行ったDocumentDbManager.CollectionIdを設定します。
また、パーティションキーを設定する事とします(DocumentCollection.PartitionKey.Paths.Add())。ドキュメントスキーマは後で定義しますが、「/Room」をパーティションキーとします。RequestOptions
RequestOptionsオブジェクトのOfferThroughputプロパティ設定によりスループット(つまり、予約するRU/s)を設定します。パーティションキーを設定したので設定可能な最低値である2500を設定します(パーティションキーを指定しない場合、最低値の400RUを設定することができます)。CreateDocumentCollectionIfNotExistsAsync()
用意した DocumentCollection / RequestOptions を引数としてコレクションを作成します(CreateDocumentCollectionIfNotExistsAsync())。このメソッドは、データベースの作成と同様に「存在しなかったらコレクション作成+コレクション情報返却、存在したらコレクション情報返却」を行う動作になります。
順序が逆転しますが、CreateDocumentCollectionIfNotExistsAsync()メソッドの第1引数に注目します。
第1引数は「どのデータベースに」コレクションを作成するか?の設定になります。
UriFactory.CreateDatabaseUri(【データベースID】)の結果として、データベースリソースを表すURIが返却されます。
具体的には、UriFactory.CreateDatabaseUri(“GroupwareDB”)の返却値は「dbs/GroupwareDB」となります。
次に、CreateDocumentCollectionIfNotExistsAsync()メソッドの保有者であるDocumentClientオブジェクトについて振り返りましょう。
DocumentClientは、コンストラクタでサービスエンドポイントを受け取っています。サービスエンドポイントは例えば「https://cosmosdoc.documents.azure.com:443/」のようなものになります。これにデータベースURIを結合すると以下になります。
「https://cosmosdoc.documents.azure.com:443/dbs/GroupwareDB」
データベースアカウント「cosmosdoc」、データベースを表す「dbs」、データベースID「GroupwareDB」・・つまりデータベースを表すユニークな「リソースURI」となります。この配下に「RoomReservations」コレクションを作成する、という指示につながることが理解できます。
そして、コレクション作成時のFiddlerの画面は以下の通りです。
https://cosmosdoc.documents.azure.com:443/dbs/GroupwareDB/colls/RoomReservations
データベースアカウント「cosmosdoc」、データベースを表す「dbs」、データベースID「GroupwareDB」、コレクションを表す「colls」、コレクションID「ReservationRoom」・・つまりコレクションを表すユニークな「リソースURI」となります。
【コラム】Resource URI と UriFactoryクラス
DocumentDBにおいてデータベースやコレクション、ドキュメントといったリソースはすべてURIで表される仕組みをとります。
URI Path | 説明 |
---|---|
/dbs | データベースアカウント配下のデータベース |
/dbs/{databaseId} | データベースアカウント配下のデータベースID=databaseIdのデータベース |
/dbs{databaseId}/colls | databaseIdデータベース配下のコレクション |
/dbs{databaseId}/colls/{collectionId} | databaseIdデータベース配下のcollectionIdコレクション |
/dbs{databaseId}/colls/{collectionId}/docs | collectionIdコレクション配下のドキュメント |
/dbs{databaseId}/colls/{collectionId}/docs/{docId} | collectionIdコレクション配下のdocIdドキュメント |
/dbs{databaseId}/colls/{collectionId}/docs/{docId}/attachments/{attachmentsId} | docIdドキュメント配下のattachmentsId1添付 |
/dbs{databaseId}/colls/{collectionId}/sprocs/{procId} | collectionIdコレクション配下のprocIdストアドプロシージャ |
/dbs{databaseId}/colls/{collectionId}/triggers/{triggerId} | collectionIdコレクション配下のtriggerIdトリガー |
/dbs{databaseId}/colls/{collectionId}/udfs/{udfId} | collectionIdコレクション配下のudfIdユーザー定義関数 |
/dbs/{databaseId}/users | databaseIdデータベース配下のユーザー |
/dbs/{databaseId}/users/{userId} | databaseIdデータベース配下のユーザーID=userIdのユーザー |
/dbs/{databaseId}/users/{userId}/permissions | userIdユーザーのパーミッション |
/dbs/{databaseId}/users/{userId}/permissions/{permissionId} | userIdユーザーのpermissionIdパーミッション |
各リソースのURIを作成するヘルパーメソッドが「Microsoft.Azure.Documents.Client.UriFactoryクラス」で用意されています。
- UriFactory.CreateDatabaseUri()
- UriFactory.CreateCollectionUri()
- UriFactory.CreateDocumentUri()
- UriFactory.CreateStoredProcedureUri()
- UriFactory.CreateTriggerUri()
- UriFactory.CreateUserDefinedFunctionUri()
- UriFactory.CreateUserUri()
- UriFactory.CreatePermissionUri()
- UriFactory.CreateAttachmentUri()
- UriFactory.CreateConflictUri()
- UriFactory.CreateDocumentCollectionUri()
これらのヘルパーメソッドはこの後も頻繁に使用します。
3.2.3 ドキュメントモデルの作成
RoomReservationsコレクションに保存するドキュメントを表すC#モデルクラスを用意します。
DocumentDB上ではドキュメントはJSON形式で保持されますが、C#コード上ではモデルクラスにマッピングするとコーディングを行う上で便利です。
以下のように RoomReservationInfo.cs をプロジェクトに追加します。
RoomReservationInfoクラスの実装は以下の通り(リスト 7)。
リスト 7 using System; using System.Collections.Generic; using Newtonsoft.Json; namespace CosmosDocDBExample { public class RoomReservationInfo { [JsonProperty(PropertyName = "id")] public string Id { get; set; } /// <summary> /// 会議室名を取得または設定します。 /// </summary> public string Room { get; set; } /// <summary> /// 会議名を取得または設定します。 /// </summary> public string Title { get; set; } /// <summary> /// 予約者IDを取得または設定します。 /// </summary> public string ReservedUserId { get; set; } /// <summary> /// 予約者名を取得または設定します。 /// </summary> public string ReservedUserName { get; set; } /// <summary> /// 開始日時を取得または設定します。 /// </summary> public DateTime Start { get; set; } /// <summary> /// 終了日時を取得または設定します。 /// </summary> public DateTime End { get; set; } /// <summary> /// 参加メンバーを取得または設定します。 /// </summary> public List<AssignMember> AssignMembers { get; set; } } public class AssignMember { public string UserId { get; set; } public string UserName { get; set; } } }
RoomReservationInfoクラスは、会議予約を表すものとし、AssignMembersが会議への参加者を表すものとします。
IDプロパティについてJSONでは id となるように定義しています(JsonProperty属性)。
DocumnetDBのドキュメントにおいて「id」というキーワードは、ドキュメントを一意に識別するための特別なキーワードです。そのため、C#クラス定義上は Id となっているプロパティを、DocumentDBに保存する際にJSON形式では id となるようにしています。
ドキュメントの一意識別子
ドキュメントは、コレクション内で一意となる識別子を持つ必要があります。
パーティションキーを「指定している場合」と「指定していない場合」でルールが異なります。
パーティションキーが指定されている場合
コレクション内のドキュメントは「パーティションキー項目とid項目」によって一意である必要があります。パーティションキーが指定されていない場合
コレクション内のドキュメントは「id項目」によって一意である必要があります。
3.3 ドキュメントの作成・更新
ドキュメントを作成および更新する処理を追加します。
DocumentDbManagerクラスにRoomReservationInfoドキュメントを作成(INSERT)するメソッド「CreateDocument()」を定義します(リスト 8)。
リスト 8 public async Task<Document> CreateDocument( RoomReservationInfo roomReservationInfo) { var document = await this.client.CreateDocumentAsync( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId), roomReservationInfo); return document; }
ドキュメントの作成は「DocumentClient.CreateDocumentAsunc()」で行うことが出来ます。
DocumentDbManager.CreateDocument()を呼び出すコードが以下になります(リスト 9)。
ひとまずここではソースとなるRoomReservationInfoオブジェクトを固定でべた書きしています。
リスト 9 // ドキュメントソースオブジェクトを用意 var assignMembers = new List<AssignMember>(); assignMembers.Add(new AssignMember() { UserId = "tanaka", UserName = "田中和夫" }); assignMembers.Add(new AssignMember() { UserId = "sakamoto", UserName = "坂本寛子" }); RoomReservationInfo item = new RoomReservationInfo() { Id = "00001", Room = "第1会議室", Title = "Cosmos DB移行についての打ち合わせ", ReservedUserId = "daigo", ReservedUserName = "醍醐竜一", Start = new DateTime(2017, 5, 30, 10, 0, 0), End = new DateTime(2017, 5, 30, 11, 0, 0), AssignMembers = assignMembers }; // ドキュメント作成メソッド呼び出し var manager = new DocumentDbManager(); manager.CreateDocument(item).Wait();
上記コードで作成されたドキュメントを、Azureポータルの「クエリ エクスプローラ」で確認した画面が以下です。
3.3.1 idの明示的な指定と暗黙的な自動採番
上記 リスト 9 のドキュメント作成コードでは id (RoomReservationInfo.Id) の値を明示的に指定しました。
識別子である id 値は未指定としてドキュメントを作成することも可能です。
未指定の場合、自動的に「GUID文字列」が id 値として設定されます。
idを指定しなかった場合に作成されるドキュメントの例は以下の通りです(リスト 10)。
リスト 10 [ { "id": "00838cf1-1c6e-49cd-aee5-b7d2f1e96677", "Room": "第1会議室", "Title": "Cosmos DB移行についての打ち合わせ", "ReservedUserId": "daigo", "ReservedUserName": "醍醐竜一", "Start": "2017-05-30T10:00:00", "End": "2017-05-30T11:00:00", "AssignMembers": [ { "UserId": "tanaka", "UserName": "田中和夫" }, { "UserId": "sakamoto", "UserName": "坂本寛子" } ], "_rid": "UyJwAK9QBwABAAAAAAAACA==", "_self": "dbs/UyJwAA==/colls/UyJwAK9QBwA=/docs/UyJwAK9QBwABAAAAAAAACA==/", "_etag": "\"00002305-0000-0000-0000-5929769d0000\"", "_attachments": "attachments/", "_ts": 1495889564 } ]
3.3.2 CreateDocumentAsync()とUpsertDocumentAsync()
上記 リスト 8 の実装では、ドキュメントを作成するメソッドとして「CreateDocumentAsync()」を使用しました。
これ以外に「UpsertDocumentAsync()」というメソッドがあります。メソッド名から想像できる通り、ドキュメントが存在しなかったら INSERT、ドキュメントが存在したら UPDATE を行います。
ドキュメントの存在有無とは・・・
* パーティションキー指定ありの場合:
コレクション内に「id と パーティションキー」が同一のドキュメントがあるか?
* パーティションキー指定なしの場合:
コレクション内に「id」が同一のドキュメントがあるか?
です。
UpsertDocumentAsync()を利用するように修正したCreateDocument()の実装は以下の通りです(リスト 11)。
メソッド名も、実装の実態に合わせ「SaveDocument()」としました。
リスト 11 //public async Task<Document> CreateDocument(RoomReservationInfo roomReservationInfo) public async Task<Document> SaveDocument(RoomReservationInfo roomReservationInfo) { var document = await this.client.UpsertDocumentAsync( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId), roomReservationInfo); return document; }
例として以下のコードを実行します。
リスト 12 var manager = new DocumentDbManager(); var assignMembers = new List<AssignMember>(); assignMembers.Add(new AssignMember() { UserId = "tanaka", UserName = "田中和夫" }); assignMembers.Add(new AssignMember() { UserId = "sakamoto", UserName = "坂本寛子" }); RoomReservationInfo item = new RoomReservationInfo() { Id = "00001", Room = "第1会議室", Title = "Cosmos DB移行についての打ち合わせ", ReservedUserId = "daigo", ReservedUserName = "醍醐竜一", Start = new DateTime(2017, 5, 30, 10, 0, 0), End = new DateTime(2017, 5, 30, 11, 0, 0), AssignMembers = assignMembers }; manager.SaveDocument(item).Wait(); // item.Room = "第2会議室"; manager.SaveDocument(item).Wait(); // item.Room = "第1会議室"; item.Title = "タイトルを変更!"; manager.SaveDocument(item).Wait();
上記 リスト 12 実行後のコレクション内ドキュメントは、以下の2件になります(リスト 13)。
リスト 13 [ { "id": "00001", "Room": "第1会議室", "Title": "タイトルを変更!", "ReservedUserId": "daigo", "ReservedUserName": "醍醐竜一", "Start": "2017-05-30T10:00:00", "End": "2017-05-30T11:00:00", "AssignMembers": [ { "UserId": "tanaka", "UserName": "田中和夫" }, { "UserId": "sakamoto", "UserName": "坂本寛子" } ], "_rid": "bel5AIFBHAABAAAAAAAACA==", "_self": "dbs/bel5AA==/colls/bel5AIFBHAA=/docs/bel5AIFBHAABAAAAAAAACA==/", "_etag": "\"0000106c-0000-0000-0000-5929795c0000\"", "_attachments": "attachments/", "_ts": 1495890267 }, { "id": "00001", "Room": "第2会議室", "Title": "Cosmos DB移行についての打ち合わせ", "ReservedUserId": "daigo", "ReservedUserName": "醍醐竜一", "Start": "2017-05-30T10:00:00", "End": "2017-05-30T11:00:00", "AssignMembers": [ { "UserId": "tanaka", "UserName": "田中和夫" }, { "UserId": "sakamoto", "UserName": "坂本寛子" } ], "_rid": "bel5AIFBHAABAAAAAAAADA==", "_self": "dbs/bel5AA==/colls/bel5AIFBHAA=/docs/bel5AIFBHAABAAAAAAAADA==/", "_etag": "\"00008c20-0000-0000-0000-5929795c0000\"", "_attachments": "attachments/", "_ts": 1495890268 } ]
↓↓↓2017.06.03追記↓↓↓
楽観的同時実行制御(optimistic concurrency control)による更新について
楽観的同時実行制御によるデータデータ更新を行う場合について補足します。
詳細にはまだ触れていませんが、ドキュメントには etag というシステムにより自動採番される値が割り当てられています。etagは更新を行う毎に自動的に更新されます。etagを使用することで、楽観的同時実行制御を行うことができます。
実装イメージは以下になります。
RoomReservationInfoクラスに etag を追加します。ドキュメントの取得時に、その時点での etag 値を保持するためです。
DocumentDbManagerクラスに ReplaceDocument() メソッドを追加します。client.ReplaceDocumentAsync()メソッドを呼び出しますが、この時に「RequestOptions.AccessCondition」によりetagの値を指定します。AccessConditionType.IfMatchは楽観的同時実行制御を実施する事を意味します。
// RoomReservationInfoクラス定義 public class RoomReservationInfo { .. 省略 // etagを追加 public string _etag { get; set; } } // DocumentDbManagerクラスメソッド定義 public async Task<Document> ReplaceDocument( RoomReservationInfo roomReservationInfo) { var doc = await this.client.ReplaceDocumentAsync( UriFactory.CreateDocumentUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId, roomReservationInfo.Id), roomReservationInfo, new RequestOptions() { AccessCondition = new AccessCondition() { Condition = roomReservationInfo._etag, Type = AccessConditionType.IfMatch } }); return doc; } // 呼び出しイメージ // ①ドキュメント取得 var reservation = manager.FindById("第1会議室", "0000000001"); // ②データ修正 reservation.Title = "変更!" + DateTime.Now.ToString("yyyyMMddHms"); // ③DB更新 var doc = manager.ReplaceDocument(reservation).Result; // ①と③の間に別プロセスによりデータ変更が行われていたらetagによる同時実行制御により更新が失敗する
↑↑↑2017.06.03追記↑↑↑
3.3.3 ドキュメントの一括投入 ~ DocumentDB Data Migration Tool
以降の検索の説明等ではもっと大量のドキュメントが登録されている状態が好ましいです。
その為に、データ量を増やしておきたいと思います。
せっかく用意した DocumentDbManager.CreateDocument() メソッドを繰り返し呼び出してもよいのですが、一括でドキュメントを投入可能
な「DocumentDB Data Migration Tool」を使うことにします。
DocumentDB Data Migration Toolは「DocumentDB→DocumentDB」や「jsonファイル→DocumentDB」「DocumentDB→jsonファイル」「MongoDB→DocumentDB」「DynamoDB→DocumentDB」等々のデータマイグレーションを行ってくれるツールです。
ここでは、事前に用意した300件のRoomReservationInfoを定義したJSONファイル(exampledata.json)から、DocumentDBへのデータ投入を行います。
その前に、作成済みの2件のドキュメントを削除しておきましょう。
削除の方法はいくつもありますが、Azureポータルで「ドキュメント エクスプローラ」を利用すると、ドキュメントを1件づつですが削除することができます(ここでは2つなのでOK)。
「DocumentDB Data Migration Tool」は以下からダウンロードすることができます。
Download Azure DocumentDB Data Migration Tool from Official Microsoft Download Center
(1) DocumentDB Data Migration Toolを起動
ダウンロードしたzipファイルを解凍し「dtui.exe」を実行します。
「Next」をクリックします。
(2)データソースを選択
データソースの形式は「JSON file(s)」を選択し、「Add Files」ボタンをクリックして対象のJSONファイルを選択します。
「Next」ボタンをクリックします。
(3)エクスポート先情報を入力
エクスポート先情報を入力します。
「Verify」ボタンで値の妥当性を確認してから、「Next」ボタンをクリックします。
エクスポート先:
DocumnetDB - Sequential import(partitioned collection)Connection String:
Azureポータルの「キー タブ」のプライマリ接続文字列をコピーし、「Database=【データベース名】」を付与した文字列を設定します。
例)AccountEndpoint=https://cosmosdoc.documents.azure.com:443/;AccountKey=[キー];Database=GroupwareDB
(4)ログ出力先等の設定
エラーログの出力先や更新のインターバルを指定して「Next」ボタンをクリックします。
(5)インポート実行
設定内容を確認して、間違えがなければ「Import」ボタンをクリックします。
(6)インポート完了
インポートが完了します。
Azureポータル上からもデータがインポートされたことを確認することができます。
3.4 ドキュメントの検索
ここまでで、「GroupwareDBデータベース の RoomReservationsコレクション に RoomReservationInfoドキュメント が300件」存在する状態になりました。
いくつかのパターンで検索を行ってみたいと思います。
前述でDocumentDBへのクエリーはSQL言語で行われると説明しました。
クラスライブラリ(Microsoft.Azure.DocumentDB)を使用した場合、「SQLを直接記述する方法」と「LINQ構文を使用する方法」があります。
3.4.1 LINQによる検索
LINQを使用した場合、ライブラリ内部及びCosmos DB(DocumentDB)では、以下の図ような振る舞いが行われます。つまり、最終的にはSQL構文で実行されることになります(Entity Frameworkを使った際のSQL Server接続と同様です)。
では、具体的な実装を。
(1)シンプルな条件検索
1つ目のクエリーサンプルは、抽出条件として「Room」を指定するサンプルです。
DocumentDbManagerに以下のメソッドを追加します(リスト 14)。
リスト 14 public List<RoomReservationInfo> FindByRoom(string room) { var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId)). Where(r => r.Room == room); var result = query.ToList(); return result; }
呼び出し側の実装は、以下の通りです(リスト 15)。
リスト 15 var manager = new DocumentDbManager(); List<RoomReservationInfo> roomReservationIngos = manager.FindByRoom("スタンディングテーブル"); // 検索結果出力 Console.WriteLine(string.Format("{0}件", roomReservationIngos.Count)); foreach (var info in roomReservationIngos) { Console.WriteLine( string.Format("{0} / {1} / {2} ~ {3}", info.Room, info.Title, info.Start, info.End)); }
実行結果の出力は以下の通りです。
97件 スタンディングテーブル / アーキテクチャ社内勉強会(06/02回) / 2017/06/02 13:00:00 ~ 2017/06/02 14:30:00 スタンディングテーブル / 営業会議(06/02回) / 2017/06/02 17:00:00 ~ 2017/06/02 19:00:00 ...省略 スタンディングテーブル / ○○様オンサイトサポートについて / 2017/09/07 9:00:00 ~ 2017/09/07 10:00:00 スタンディングテーブル / 進捗会議(08/10回) / 2017/08/10 10:00:00 ~ 2017/08/10 11:00:00 続行するには何かキーを押してください . . .
加えて、検索処理のHTTPS通信をFiddlerによって監視した結果を以下に示します。
「RoomReservationsコレクション情報の取得」と「ドキュメントの取得」の2つのHTTPSリクエストが行われています。
2つめのHTTPSリクエストは以下の通りです。
POST https://cosmosdoc-japanwest.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations/docs HTTP/1.1 x-ms-continuation: x-ms-documentdb-isquery: True x-ms-documentdb-query-enablecrosspartition: False x-ms-documentdb-query-iscontinuationexpected: False x-ms-documentdb-populatequerymetrics: False x-ms-date: Sat, 27 May 2017 16:43:35 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3dcS4wmQgHbflDa7VXTOrqKrlUowyeQNK6h9D9lJj8Yug%3d Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-02-22 Accept: application/json Content-Type: application/query+json Host: cosmosdoc-japanwest.documents.azure.com Content-Length: 98 Expect: 100-continue {"query":"SELECT * FROM root WHERE (root[\"Room\"] = \"スタンディングテ\\u30fcブル\") "}
BODYのJSONデータとして「SQL」が設定されています。
FROM句の「root」は、コレクションに属するデータを表す予約語です。POSTリクエストのURI「https://cosmosdoc-japanwest.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations/docs」によって、クエリーの対象コレクションが明示されているため、rootという表現が可能になります。
WHERE句もLINQメソッド構文で指定した内容がSQLに展開されていることが分かります。
(2)予約者による条件検索(クロスパーティション検索)
次に予約者による検索を実装します。
DocumentDbManagerに以下のメソッドを追加します(リスト 16)。
リスト 16 public List<RoomReservationInfo> FindByReservedUserId(string userId) { FeedOptions feedOptions = new FeedOptions() { EnableCrossPartitionQuery = true }; var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri(DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId), feedOptions) .Where(r => r.ReservedUserId == userId); var result = query.ToList(); return result; }
FeedOptionsオブジェクトを作成し、CreateDocumentQuery()メソッドの第2引数に設定しています。また、FeedOptions.EnableCrossPartitionQueryプロパティを true に設定しています。
これは非常に重要な設定です。
本サンプルの RoomReservationsコレクション には、パーティションキーとして「/Room」を設定しました。検索条件に「Room」が含まれていない場合、「クロスパーティション検索」というものになります。CreateDocumentQuery()呼び出し時に、明示的に FeedOptions.EnableCrossPartitionQueryプロパティ の値を true に設定していないと例外が発生してしまいます。
「クロスパーティション検索」は、検索条件に パーティションキーの値 を指定したものと比較して負荷の高い(消費RUの高い)クエリーになります。
この点に関しては、設定するパーティションキーの設計段階からの検討が必要になります。
呼び出し側の実装は、以下の通りです。
リスト 17 List<RoomReservationInfo> roomReservationInfos = manager.FindByReservedUserId("daigo"); Console.WriteLine(string.Format("{0}件", roomReservationInfos.Count)); foreach (var info in roomReservationInfos) { Console.WriteLine( string.Format("{0} / {1} / {2} ~ {3}", info.Room, info.Title, info.Start, info.End)); }
【コラム】クロスパーティション検索時のHTTPSリクエスト
リスト 16 のクロスパーティション検索が実行された場合のHTTPSリクエストの様子をFiddlerで取得したのが以下です。
「/dbs/GroupwareDB/colles/RoomReservations/docs」へのリクエストが10回行われていることが分かります。
次に、以下のロジックを実行してみます。
コレクションのパーティションキーレンジ情報の取得を行う処理になります。
// パーティションキーレンジ情報の取得 var pkRanges = await this.client.ReadPartitionKeyRangeFeedAsync( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId ) ); var list = pkRanges.ToList();
上記が実行された際のFiddlerによるHTTPS通信の様子が以下です。
上記から分かることは、RoomReservationsコレクションは「10」のパーティションに分割されドキュメントが保持されている、ということです。
”同一パーティションキー値を持つドキュメント”は”同一のパーティション”に保持されます。逆に、パーティションキー値が異なるドキュメントは、異なるパーティションに保持される可能性があります。
パーティションキーを指定したクエリーでは、単一のパーティションに対するクエリーで素早くデータを取得できますが、パーティションキー指定のないクエリー、つまり複数のパーティションにまたがる可能性のあるクエリーでは「各パーティションへの問い合わせ」が必要になります。
これにより、クロスパーティション検索は多くの RU/s を消費することになります。
(3)結果件数とMaxItemCount
検索時のオプション設定「FeedOptions」には「MaxItemCountプロパティ」というものがあります。
DocumentDBへの1度の問い合わせで取得するアイテム(ドキュメント)の最大件数の指定になります。
DocumentDbManagerに以下のメソッドを追加します(リスト 18)。
リスト 18 public List<RoomReservationInfo> FindByRoom2(string room) { FeedOptions feedOptions = new FeedOptions() { MaxItemCount = 5 }; var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId), feedOptions). Where(r => r.Room == room); var result = query.ToList(); return result; }
呼び出し側の実装は、以下の通りです(リスト 19)。
リスト 19 List<RoomReservationInfo> roomReservationInfos = manager.FindByRoom2("第1会議室");
上記コードによる検索で発生したHTTPSリクエストを監視したFiddler画面は以下の通りです。
5件づつの取得処理を繰り返し呼び出している様子を確認することができます。
MaxItemCountを超える検索結果があった場合、クラスライブラリが自動的に裏側で必要な数のHTTPSリクエストを発行します。
(4)スカラー値検索
スカラー値検索の例として、検索条件に合致する件数を取得する処理を実装します。
DocumentDbManagerに以下のメソッドを追加します(リスト 20)。
クエリーオブジェクトに対して「Count()」LINQメソッドを呼び出すことで、件数の取得を行うことができます。
リスト 20 public int CountByRoom(string room) { var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId)). Where(r => r.Room == room); var result = query.Count(); return result; }
(5)子要素による検索
次に、RoomReservationInfo.AssignMember要素の値による検索を行いたいと思います。
DocumentDbManagerに以下のメソッドを追加します(リスト 21)。
リスト 21 public List<RoomReservationInfo> FindByAssignMember(string room, AssignMember member) { var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId)) .Where(r => r.Room == room && r.AssignMembers.Contains(member)); var result = query.ToList(); return result; }
呼び出し側の実装は、以下の通りです(リスト 22)。
リスト 22 List<RoomReservationInfo> roomReservationInfos = manager.FindByAssignMember( "第1会議室", new AssignMember() { UserId = "daigo", UserName = "醍醐竜一" }); Console.WriteLine(string.Format("{0}件", roomReservationInfos.Count)); foreach (var info in roomReservationInfos) { Console.WriteLine( string.Format("{0} / {1} / {2} ~ {3}", info.Room, info.Title, info.Start, info.End)); }
3.4.2 SQL指定による検索
LINQ構文ではなくSQLを直接指定した検索を行うことも可能です。
このあたりの感覚もEntity Frameworkと同じですね。
DocumentDbManagerに以下のメソッドを追加します(リスト 23)。
リスト 23 public RoomReservationInfo FindById(string room, string id) { var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId), new SqlQuerySpec( string.Format( "SELECT * FROM root WHERE root.Room = '{0}' AND root.id = '{1}'", room, id))); var list = query.ToList(); return list.Count > 0 ? list[0] : null; }
DocumentClient.CreateDocumentQuery()メソッドの引数として「SqlQuerySpecオブジェクト」を引き渡します。SqlQuerySpecにはSQL文をそのまま文字列として指定可能です。
SQLのパラメータ化
上記FindById()ではSQL文字列にパラメータ部分も埋め込みを行いました。パラメータ部に対して、明確にパラメータオブジェクト(SqlParameter)を適用することも可能です(リスト 24)。
リスト 24 public RoomReservationInfo FindByIdWithParam(string room, string id) { SqlQuerySpec sqlQuerySpec = new SqlQuerySpec(); sqlQuerySpec.QueryText = "SELECT * FROM root WHERE root.Room = @room AND root.id = @id"; sqlQuerySpec.Parameters.Add(new SqlParameter("@room", room)); sqlQuerySpec.Parameters.Add(new SqlParameter("@id", id)); var query = this.client.CreateDocumentQuery<RoomReservationInfo>( UriFactory.CreateDocumentCollectionUri(DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId), sqlQuerySpec); var list = query.ToList(); return list.Count > 0 ? list[0] : null; }
3.5 ドキュメントの削除
ドキュメントの削除は「DocumentClient.DeleteDocumentAsync()メソッド」で行うことが出来ます。
DocumentDbManagerに以下のメソッドを追加します(リスト 25)。
コレクションにパーティションキーを設定している場合は、RequestOptions.PartitionKeyに削除対象ドキュメントのパーティションキー値を設定します。
リスト 25 public async Task<Document> DeleteById(string room, string id) { RequestOptions requestOptions = new RequestOptions(); requestOptions.PartitionKey = new PartitionKey(room); var document = await this.client.DeleteDocumentAsync( UriFactory.CreateDocumentUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId, id), requestOptions); return document; }
リスト 26 // パーティションキーの設定がないコレクションの場合は // RequestOptions.PartitionKey指定なしの以下の実装が可能 public async Task<Document> DeleteById(string id) { var document = await this.client.DeleteDocumentAsync( UriFactory.CreateDocumentUri( DocumentDbManager.DatabaseId, DocumentDbManager.CollectionId, id)); return document; }
3.6 つづく・・・
DocumentDB編をこの1つの投稿で終わらせようと思っていたのですが、思いのほか長くなってきましたので、「Azure Cosmos DB入門(4)」に分割継続させることにしました。
3.7 資料
本記事のサンプルは以下からダウンロード可能です。
Azure Cosmos DB入門(2)
本コンテンツは「Azure Cosmos DB入門」の(2)です。
2 Cosmos DBの主要概念
前回の「Azure CosmosDB入門(1)」では、Cosmos DBの「特徴」と「Hello Cosmos DB」と題した簡単なサンプルについて説明しました。
早く具体的なデータベース操作に入りたいところですが、その前にCosmos DBを利用する上で押さえておきたい主要な概念について説明します。
ここでは、各技術要素の説明に関して「主要概念」の範囲にとどめます。
RU(Request Unit)やパーティショニングなどは、より複雑な内容を理解する必要がありますが、それは「Azure CosmosDB入門(4)」で説明する事とし、ここではCosmos DBプログラミングを始めるための概要を理解することを目的とします。
2.1 データモデルとアクセスAPI
Cosmos DBでは、以下のような多様なデータモデルをサポートします。
- ドキュメント
- テーブル
- グラフ
各データモデルに対しては、それぞれに対応したアクセス用のAPIが提供されています。
「アクセス用API」と「データモデル」は密接な関係を持ちます。AzureにCosmos DBアカウントを作成する際は、初期作成パラメータとして「アクセス用API」を選択します。すると「アクセス用API」に対応した「データモデル」でデータベースアカウントが作成されます。
以下が「アクセス用API」の一覧と、それに対応する「データモデル」です。
- SQL(DocumentDB)用API : ドキュメント データモデル
- MongoDB用API : ドキュメント データモデル
- Gremlin用API : グラフ データモデル
- Table(Key-Value)用API : Key-Value データモデル
2.1.1 atom-record-sequence(ARS)
マルチデータモデルによるデータ保持を行うCosmos DBですが、内部では「atom-record-sequence(ARS)」というデータ保持の仕方をしています。
公式ドキュメントでは以下のように説明されています。
- atomは、string/bool/numberのようなプリミティブ型の小さなセットから構成されます
- recordは、atomから構成される構造体です
- sequenceは、atom / record / sequenceから構成される配列です
つまり、以下のようなイメージでしょうか。
2.1.2 AzureポータルでCosmos DBを作成する
アクセス用API / データモデルの種類は、Cosmos DBをAzureに作成する際に決定します。
また、Azure Cosmos DBを作成すると「データベースアカウント」が作成されます。これはAzure Cosmos DB全体においてユニークなIDを持つアカウントとなります。
1つのデータベースアカウントにおいて、複数のデータモデルのデータを混在させることはできません。
既に「1.2 Hello Cosmos DB」で、Azureポータルを利用してCosmos DBデータベースを作成する手順については説明しました。
以下は、それぞれのアクセスAPI(=データモデル)のCosmos DBデータベースアカウント作成のイメージです。
(1) DocumentDBデータモデル
(2) MongoDBデータモデル
(3) Graphデータモデル
(4) Tableデータモデル
2.2 各種データモデルのデータ構造
「ドキュメント」「グラフ」「テーブル」の各データモデルにおける「データの構造」について見ていきます。
2.2.1 「ドキュメント」モデル
「ドキュメント」データモデルでは以下のような構造のデータを保持します。
※厳密な構造上はコレクション配下にストアドプロシージャなども含まれますが、それらは適時後述します。
(1) データベースアカウント
「データベースアカウント」がルート要素となります(Azureポータルで作成したリソースのルートです。Azureポータルで「Cosmos DBを作成する」=「Cosmos DBデータベースアカウントを作成する」となります)。
(2) データベース
データベースアカウントの下に「データベース」をN個作成することができます。
(3) コレクション
データベースの下に「コレクション」をN個作成することができます。
(4) ドキュメント
コレクションの下の「ドキュメント」が、データとしての1項目であり、JSON形式のデータで表現されます。
SQL Serverにマッチさせて考えると以下のようになります。
- 「データベースアカウント」=SQL Serverの「データベースインスタンス」
- 「データベース」=SQL Serverの「データベース」
- 「コレクション」=SQL Serverの「テーブル」
- 「ドキュメント」=SQL Serverの「レコード」
JSONドキュメントデータはスキーマレスであり、異なる形式のデータを同一コレクション内に混在させることができます。
ただし、全く関係のない種類のデータを混在させるというのではなく、同一の概念を表すが、データ毎に詳細は異なるデータ群を同一コレクションに保存することが多いと思います。
以下はショッピングサイトを想定したケースで、商品である「書籍」と「CD」を表したJSONドキュメントです。「商品」という共通概念のデータを表しますが、書籍はページ数(PageCount)という独自の属性があり、CDには「トラック数(TrackCount)」という独自の属性があります。
書籍を表すJSON { "Id": "001", "Title": "0から始めるCosmos DB", "Price": 2800, "PageCount": 360 }
CDを表すJSON { "Id": "102", "Title": "毎日10分TOEIC Listening", "Price": 1600, "TrackCount": 30 }
2.2.2 「グラフ」 モデル(2017/5現在Preview版)
これはDocumentDBがCosmos DBとなったタイミング(2017/5/10)でサポートされた新たなデータモデルです。
グラフデータベース自体は既に世の中には存在し、Neo4Jなどが代表的なものとして存在していました。
Cosmos DBでも同様のグラフデータモデルをサポートし、クエリー言語として「Gremlin」をサポートします。
Vertex と Edge
グラフデータベースの概念の詳細にはここでは深くは触れませんが簡単に・・・
グラフデータベースは一般的に、「twitterやfacebookの友達の繋がり」を表す例で説明されることが多いです。それ以外にも「交通機関の駅の繋がりからの最短経路検索」「物流における配送経路の決定」「関連する商品をグラフ管理することにより顧客への商品レコメンド抽出」等々で活用の用途があります。
Vertexは人や物を表す「頂点」であり、それからEdgeはVertex間の関係性を表す「辺・端線」となります。
Vertexの直接的な繋がり、複数のVertexを経由した間接的繋がり、またEdgeは繋がりの種類・方向を表すことができます。
2.2.3 「テーブル」モデル(2017/5現在Preview版)
「テーブル」データモデルは、その名前の通りテーブル構造でデータを保持します。ただしRDBのテーブルと異なり、スキーマレスのテーブル構造となります。
以下の図のように「1つのテーブル」に異なるスキーマ(異なる列構造)のデータを格納することができます。
「Azure入門」と「解剖C#」は書籍データであり「PageCount列」があります。
「美女と野獣 bluelay」はブルーレイいコンテンツなので「PageCount列」は無く「Duration列」があります。
「コカ・コーラ」については「PageCount列」も「Duration列」も存在しません。
Azure Storage Tableとの関係
テーブルは従来の「Azure Storage Table」のCosmos DB版との位置付けです。
APIもAzure Storage Table APIと互換性のあるインターフェイスが用意されています。
「Cosmos DBのテーブル」と「Azure Storage Table」の使い分けは以下のようになります。
「Cosmos DB Table」:
グローバルディストリビューション、低レイテンシー等々のCosmos DBの持つアベイラビリティ、スケーラビリティのメリットが得られる。「Azure Storage Table」:
速度は遅いがストレージあたりの低料金によるコストメリットが得られる。
2.3 パーティショニング
Cosmos DBのバックエンドでは、固定化されたサイズのSSDベースのストレージが動作しており、ラッチフリーを実現したBW-Treeのデータベースシステムが動作しています。
また、Cosmos DBは「パーティション」という単位でデータを分割して保持します。パーティションはSSDストレージに関連付けられて保持されることになります。
コレクション群を保持する論理リソースを「コンテナ」と呼び、コンテナはパーティションやサーバーを跨いで保持されます。
コレクションに対して用意されるパーティション数は、Cosmos DBにより自動的に適時決定されます(ストレージサイズや予約されたスループット(RU)により決定)。外から実際の動作を確認する限り、コレクションのStorage Capacityが10GB設定の場合には 1パーティション 構成、250GB設定の場合には 25パーティション 構成が取られるようです。
Storage Capacity=250GB(以上)の設定を行う場合、コレクションに対して「パーティションキー」というものを設定する必要があります。
「パーティションキー」は、ドキュメント内の データ要素 を指定します。
パーティションキーが同一のドキュメント(データ)は、同一のパーティションに保存されます。
例えば以下のようなドキュメントに対して /cityName をパーティションキーとした場合、cityNameがTokyoのドキュメントは必ず同一パーティションに保存されます。
{ 'id': 1, 'userName': 'ryuichi', 'cityName': 'Tokyo' ...省略... }
パーティションについては、設計上の注意点と、良くないパーティション設計を行った場合の問題点があります。
- 同一パーティションキー値のデータにおいてのみトランザクション処理が可能
- パーティションキーはデータがなるべく均等に分散されるように設定する
データの偏りが発生した場合、対象のパーティションにのみ負荷がかかり、予約設定したスループット(RU)のパフォーマンスがでなくなる。
さらに詳細なパーティションについての情報は「Azure CosmosDB入門(4)」で説明します。
2.3.1 Azureポータルでパーティションキーを設定
Data Explorerでコレクションを追加する際に「Partition key」を設定することができます。
2.3.2 プログラムでパーティションキーを設定
以下がプログラムでパーティションキーを設定するコードスニペットです。
client = new DocumentClient( new Uri("https://cosmosdb.documents.azure.com:443/"), "[キー]"); await client.CreateDatabaseIfNotExistsAsync(new Database { Id = "Database1" }); var collection = new DocumentCollection { Id = "Collection1" }; collection.PartitionKey.Paths.Add("/cityName"); await client.CreateDocumentCollectionIfNotExistsAsync( UriFactory.CreateDatabaseUri("Database1"), collection);
DocumentCollectionオブジェクトのPathsプロパティに「/cityName」を追加しています。
コレクション作成時(CreateDocumentCollectionIfNotExistsAsync())の引数で渡されるDocumentCollectionオブジェクトを通じて、指定のパーティションキーが適用されます。
2.4 RU(Request Unit)
RU(Request Unit)はCosmos DBでは非常に重要な概念です。
RUは「Cosmos DBに対する操作(クエリーや書き込み)を行うのに要する計算量」の単位になります。
2.4.1 1RUとは?
RUは「処理を行い為の計算量」の単位ですが、では 1RU とはどれくらいの処理量でしょうか。
公式ドキュメントにおいて 1RU は以下とされています。
The baseline of 1 request unit for a 1KB item corresponds to a simple GET by self link or id of the item.
つまり、
「1KBのデータを、セルフリンクもしくはIDでシンプルに取得する操作に要するRUが約1」
ということです。
大きなデータの取得、複雑な抽出条件の指定等を行うと、そのデータ抽出操作を行うのに必要なRU値が増えます。
2.4.2 RUはコレクションに割り当てる
RUは「コレクション」に対して 100RU/秒 単位で割り当てます。
RUは「扱うデータサイズ・量」「抽出条件の複雑度」「リクエストのトラフィック量」により必要量が決まります。また、RUのサイズがCosmos DBの課金における大きな要素となりますので、割り当てサイズの決定は非常に重要になります。
2.4.3 RUという概念のすばらしさ
従来のハードウェアリソースを意識した、つまり、CPUの割り当て数・メモリの割り当て量を基準としたスケール概念の場合、データ量の増大・抽出条件の複雑化が発生すると、結果取得のパフォーマンス(応答時間)の遅延につながります。
これに対してCosmos DBでは、CPUやメモリのような物理リソースではなく、RUという抽象単位の割り当てを行う考え方をとります。RU/秒は1秒間にこなせる計算量であり、応答時間については「read <10ms、write <15ms」がSLAで保障され続けます。
つまり、Cosmos DBでは「コレクション毎に、1秒間に処理できるRU」を予約設定します。予約したRUを使い切ろうが、使わなかろうが、予約したRU分が課金対象となります。そして、「read <10ms、write <15ms」のパフォーマンスを発揮するのに要するCPUやメモリはAzureバックエンドの話であり、Cosmos DB利用者は意識する必要がないし、見ることすらできません。バックエンドのハードウェインフラを意識することなく、一定の契約パフォーマンスが常に得られる、まさに PaaS の素晴らしさといえるでしょう。
2.4.4 予約RUの設定方法
コレクションに対するRUの設定は、Azureポータルやプログラムから行うことができます。
(1) Azureポータルによる設定
Azureポータルでは、コレクションを作成する際にRUの設定を行うことができます。
項目としては「Throughput」となります。
以下の画面キャプチャに示すように、Storage Capacityが「Fixed(10GB)」の場合には 400~10,000 RU の範囲で、Storage Capacityが「Unlimited」の場合には 2,500~100,000 RU の範囲で設定が可能です。
初期作成後もData Explorerでスループットの変更は柔軟に行うことができます。
(2) プログラムによる設定
1000RUのスループットを定義したDocumentDBコレクションを作成するコードスニペットは以下の通りです。
// 接続用クライアントオブジェクトの作成 this.client = new DocumentClient( new Uri(EndpointUrl), PrimaryKey); // データベースの作成 await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DatabaseId }); // コレクションの作成 var collection = new DocumentCollection { Id = CollectionId }; var requestOptions = new RequestOptions() { OfferThroughput = 1000 }; await this.client.CreateDocumentCollectionIfNotExistsAsync( UriFactory.CreateDatabaseUri(DatabaseId), collection, requestOptions);
RequestOptionsオブジェクトの「OfferThroughputプロパティ」に、必要なスループット(RU)を指定してコレクションの作成メソッドを呼び出します。
10,000RUを超える値を設定する場合は、パーティションキーを設定する必要があります。設定しなかった場合、例外が発生します。
2.4.5 消費RUの確認方法
Cosmos DBにおいては RU/秒 を予約設定して運用します。
つまり、アプリケーション実装におけるCosmos DBへの読み込み・書き込みがどれだけのRUを消費するかは、RUの予約数を決定するうえで非常に重要です。
前述のようにCosmos DB公式ドキュメントにおいても 1RU の目安は以下とされています。
「1KBのデータを1件取得する(一貫性レベルはSession)」操作に要するRUが約「1」
実際に実装したアプリケーション上の操作におけるRUの消費量は以下の方法で確認することが出来ます。
(1) Cosmos DBへの操作を行った際のResponseヘッダ「x-ms-request-charge」で確認
DocumentDB API(接続オプションをTCPではなくHTTPSとしている場合)・REST APIによる操作を行っている場合にはHTTPS通信を監視するFiddlerなどを使って確認することができます。 以下は「WHERE Age > 20」という条件付きであるコレクションのドキュメント検索を行った際のHTTPS通信内容です。
右下のペインにおいてHTTP Response Headerを確認しています。
「x-ms-request-charge」項目が今回の検索を行うのに消費したRUであり 3.72 という値が返却されています。
(2) Azureポータルのクエリーエクスプローラで確認
Azureポータルにはクエリーエクスプローラ機能が用意されています。
以下の画面のようにコレクションに対して任意のSQLを実行すると「請求の要求」という項目で消費RUが表示されます。
(3) GetLastRequestStatisticsコマンド(MongoDB)で確認
GetLastRequestStatisticsコマンドを呼び出すことで直前の処理の消費RUを確認することができます。
C#上から呼び出す例は以下です(詳細については「Azure CosmosDB入門(3)」で説明します)。
MongoClient client = new MongoClient(settings); var database = client.GetDatabase(dbName); ...任意のMongoDBアクセス処理 var result = database.RunCommand<BsonDocument>(new CommandDocument { { "getLastRequestStatistics", 1 } }); Console.WriteLine( "消費RU={0}", result["RequestCharge"].RawValue);
2.4.6 RUの話は深い・・・(後半に続く)
記述の通りRUはCosmos DBでは非常に重要な要素です。
Cosmos DBを運用する際のコストに対して非常に大きな比重を持ちますし、運用においてのシステムとして利用するスケーラビリティのバリューでもあります。
また、まだ触れていない「インデックス」「一貫性レベル」の設定状態によっても変動があります。
そして、以下のような要素についての解説がまだまだ必要なのですが、これはまた後半で説明する事とします。
- RU/秒 と RU/分
- RU超過時のレスポンスコード429
- etc…
RUについての理解はひとまずここまでにし、もう少し具体的なCosmos DBプログラミングの理解を進めた後で、さらに深堀したほうが理解しやすいと考える為です。
Azure Cosmos DB入門(1)
本コンテンツは「Azure Cosmos DB入門」の(1)です。
1 Cosmos DBとは
Cosmos DBはAzureで提供されるデータベースサービスの1つです。データベースの種類としては、いわゆる「NoSQL」データベースとなります。
公式ドキュメントにおいては、まず以下のように説明されています。
「グローバル分散可能なマルチモデルデータベースサービス」
1.1 特徴
Cosmos DBの代表的な特徴は以下ような点があげられます。
- グローバル分散可能
- 柔軟なスケーリングが可能
- 超高速な応答速度
- マルチデータモデル
- 多様な一貫性モデル
- 高いレベルのSLA
上記項目について、それぞれ順番に見ていきましょう。
1.1.1 グローバル分散可能
Azureは世界30ヶ所以上のリージョンでサービスが提供されています。Cosmos DBは、これらのリージョンに対してレプリケーションが可能です。
つまり、世界中に分散してデータを保持することができます。
世界規模のサービスを提供する場合、データベース(Cosmos DB)とアプリケーション(例えばAppService)を各リージョンにレプリカ&配置することで、エンドユーザーが最寄りのリージョンに対してのアクセスでサービスを利用するようにすることができます。つまり、地理的に遠いリージョンへのアクセスによるネットワーク遅延を回避することが可能になります。勿論、そのうえでレプリケーションされたCosmos DBはバックエンドで自動的にリージョン間のデータ同期が行われます。また、特定リージョンのダウン時には自動的なフェールオーバーが行われます。
1.1.2 柔軟なスケーリングが可能
スループットのスケーリングもCosmos DBの大きな特徴になります。
クラウドサービスの特徴として自由自在にスケール調整が可能なことがあげられます。
Cosmos DBは、更に、オンライン状態のまま5秒以内にパフォーマンスのスケール変更が可能となっています。スケーリングの調整は、Azureポータル上からもプログラム上からも簡単に可能となります。
また、Cosmos DBにおけるスループットは「RU(Request Unit)」という単位になっており、これが課金対象の大きな要素となります。
ストレージ容量についてもペタバイト単位までスケール可能となっています。
そしてCosmos DBは無制限にスケーリングすることが可能です。
1.1.3 超高速な応答速度
Cosmos DBに限らず、AWSのDynamoDBにしても、それからオンプレ運用可能なApache CASSANDRAにしても、NoSQLデータベースの特徴として「超高速である」ということは周知の事実でしょう。
対比されるデータベースシステムとしてRDBがありますが、NoSQLでは「テーブル間リレーションという概念を持たない」「限られたトランザクション機能とする」等の機能実装上の方針の違いから「高速な動作」を手に入れています。
Cosmos DBでは、同一リージョン内における読み取り・書き込みのレイテンシーに対して以下の表に示すようなミリ秒単位の速度を保証しています。
Read(1KB) | Indexed Write(1KB) | |
---|---|---|
50% | < 2ms | < 6ms |
99% | < 10ms | < 15ms |
※「同一リージョン内」でのレイテンシーとは、つまり「Japan WestにApp Serviceをパブリッシュ&同じくJapan WestにCosmos DBを作成」というケースにおける App ServiceからCosmos DBへのデータアクセスにおけるレイテンシーを指します。
1.1.4 マルチデータモデル
これもCosmos DBの大きな特徴になります。
従来のNoSQLでは、CASSANDRAは「Key-Value」、MongoDBは「ドキュメント」・・・のようにデータベースプロダクト(サービス)と保持するデータモデル形式は固定化されていました。
しかし、Cosmos DBでは以下のような異なるデータモデルを保持することができます。
- 「ドキュメント」
- 「Graph」
- 「Key-Value」
またそれらのデータモデルに合わせて、アクセス用のAPIもそれぞれ用意されています。
1.1.5 多様な一貫性モデル
従来の一般的なNoSQLデータベースでは、データの一貫性に関して以下の2つのモデルを提供していました(AWS DynamoDBの該当)。
- 「Strong」 : 強固な一貫性を提供する(ただし速度性能を犠牲にする)
- 「Eventual」 : (一貫性を維持するためにタイムラグを要するが)最終的にデータの一貫性を提供する(速度性能重視)
Cosmos DBでは、上記に加えて「Bounded-staleness」「Session」「Consistent Prefix」という3つのモードが用意されています。(合計5つの一貫性モードが用意されています)
1.1.6 高いレベルのSLA
Cosmos DBでは以下について99.9%という非常に高いSLA保証を行っています。
- 可用性保証 99.9%
- スループット保証 99.9%
- レイテンシー保証 99.9%(99%が10ms未満のレイテンシーである事)
- 一貫性保証 読取り要求の100%が、要求された一貫性レベルの一貫性を保証
SLAの詳細は以下に公開されています。
https://azure.microsoft.com/en-us/support/legal/sla/cosmos-db/v1_0/
1.1.7 Cosmos DBは自分たちが普通に使うものか?
上記で紹介した6つの特徴から「Cosmos DBは"なんかすごい"」という事が感じ取れると思います。と同時に「世界規模の分散」や「無制限なスケーリング」などを必要とするような、例えばfacebookだったりtwitterだったりのようなシステムを自分たちは作りはしないし、これって使う必要あるの?という思いを持つ方も多いことでしょう。
しかし、Cosmos DBがレイテンシーに関して「読み込み <10ms、書き込み <15ms」の保証をしているデータベースシステムである、という点だけでも使う価値を見出せるのではないかと思います。
多くの既存の、そして現役の開発の現場(これは、特定企業内のBtoBなシステム、不特定多数の企業を対象としたBtoBのクラウドサービス、不特定多数の個人を対象としたBtoCのサービス、を含む)においても、RDBによるデータアクセスがボトルネックとなるケース、その為のパフォーマンスチューニングを追加で講じる必要に直面するケースは多いのではないでしょうか。
「RDBをCosmos DBにすべて置き換える」という選択肢もありますし、「RDBとCosmos DBのハイブリット運用」という選択肢もあるでしょう。
また、Cosmos DBはACIDなトランザクション処理をサポートしていますが、従来のRDBよりも制約があり設計上の工夫が必要である点があります。その様なことから、マスタデータをRDBで、読み取り効率を上げたい部分に関してはRDB → Cosmos DBへのデータ連携を走らせ、Cosmos DBからのリードパフォーマンスの優位性を享受する、ということも考えられます。
それから、データ要件によってはRDBのテーブル構造がマッチしないケースもあります。例えば私が所属する会社の製品にワークフロー(稟議書)を扱うものがあります。稟議書は「申請者から何人もの承認者を経るパス」を持ちます。パスは単純なケースもあれば、稟議書の内容によって複雑な分岐をする場合もあります。下図のようなパス(フロー)をデータベースに保持する場合、テーブル構造よりもグラフ構造で保存した方が自然な形で保存することができますし、速度も圧倒的に早いでしょう。
※私自身もNoSQLをガンガンシステムに適用しているわけではないので、この辺りは業界全体としても経験を蓄積し、並行してさらなる技術の進歩が起こりながらベストプラクティスを求めていくことになると思います。
1.2 Hello Cosmos DB
ではHello Worldならぬ、Hello Cosmos DBとして、Cosmos DBを利用するファーストステップを行ってみようと思います。
1.2.1 AzureポータルでCosmos DBを作成
ブラウザで「https://portal.azure.com」を開きます。
サイドメニューから「新規」を選択します。
フィルターに「Azure Cosmos DB」と入力します。
「Azure Cosmos DB」を選択します。
「作成」ボタンをクリックします。
任意の「ID」(ここでは cosmosdb)を入力します。
「API」は「SQL(DocumentDB)」を選択します。
「リソースグループ」の任意の値(ここでは cosmosdb)を入力します。
以上で「cosmosdb」というデータベースアカウントのCosmos DB(DocumentDBデータモデル)が作成されました。
1.2.2 Azureポータルでデータを操作
実際の運用アプリケーションでは、データの作成・検索・更新等の操作はプログラムから行われますが、ここではまず、Azureポータル上からCosmos DBのデータ操作を行いその雰囲気を感じ取ります。
(1) データの入れ物の準備(データベースとコレクション)
作成したCosmos DBをAzureポータルで表示した画面が以下です。
「Data Explorer(Preview)」を選択すると、以下の画面が表示されます。
SQL ServerのManagement Studioのようなノリでデータ操作を行うことができます。
「New Collection」をクリックすると以下の画面が表示されます。
このデータベースアカウント「cosmosdb」に対して、「データベース」→「コレクション」を作成します。コレクションは「データ項目」を入れる入れ物になります(詳細は後述)。
ここでは以下の内容にてデータベースとコレクションを作成します。
* Database id = ExampleDb1
* Collection id = ExampleCol1
* Storage capacity = Fixed(10GB)
* Throughput = 400
* Partition key = なし
データを格納する入れ物が完成しました(コレクションを作成すると「Throughput = 400」というパフォーマンスを提供するためのリソースがAzure上に確保された状態となります。そ の為、課金の対象となりますので、作成→放置→課金継続・・・にはご注意ください)。
(2) データ(ドキュメント)の作成
データベース・コレクションの作成を行った後の「Data Explorer(Preview)」の表示は以下の通りです。
左側のツリーから「Documents」を選択し、「New Document」を選択します。
JSONエディタが表示されるので、適当にデータを編集してSaveボタンをクリックします。
※本チュートリアルでは、Cosmos DBを作成する際に、APIとして「SQL(DocumentDB)」を選択したためデータモデルが「ドキュメント(つまりJSONデータモデル)」となっています。
作成されたデータは以下の通りです。自ら編集した物以外の項目「rid」「self」「etag」「attachments」「_ts」が付加されていますが、これらはCosmos DBが内部的に自動で付加したものです。
この後の上記手順を繰り返し、適当なデータ5件を作成しました。
作成したデータ一覧 [ { "id": "1", "FirstName": "Ryuichi", "LastName": "Daigo", "FavoriteDB": "Cosmos DB", "Age": 17, "_rid": "2cRoAIH5HAACAAAAAAAAAA==", "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAACAAAAAAAAAA==/", "_etag": "\"0f00fff9-0000-0000-0000-591fcf700000\"", "_attachments": "attachments/", "_ts": 1495256943 }, { "id": "2", "FirstName": "Hiroaki", "LastName": "Sakamoto", "FavoriteDB": "SQL Server", "Age": 25, "_rid": "2cRoAIH5HAADAAAAAAAAAA==", "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAADAAAAAAAAAA==/", "_etag": "\"0f0006fa-0000-0000-0000-591fd0400000\"", "_attachments": "attachments/", "_ts": 1495257151 }, { "id": "3", "FirstName": "Susumu", "LastName": "Akiyama", "FavoriteDB": "Neo4j", "Age": 36, "_rid": "2cRoAIH5HAAEAAAAAAAAAA==", "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAAEAAAAAAAAAA==/", "_etag": "\"0f0007fa-0000-0000-0000-591fd05b0000\"", "_attachments": "attachments/", "_ts": 1495257178 }, { "id": "4", "FirstName": "Hiroko", "LastName": "Takada", "FavoriteDB": "Oracle", "Age": 32, "_rid": "2cRoAIH5HAAFAAAAAAAAAA==", "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAAFAAAAAAAAAA==/", "_etag": "\"0f0008fa-0000-0000-0000-591fd07d0000\"", "_attachments": "attachments/", "_ts": 1495257212 }, { "id": "5", "FirstName": "Yoko", "LastName": "Kitahara", "FavoriteDB": "Mongo DB", "Age": 18, "_rid": "2cRoAIH5HAAGAAAAAAAAAA==", "_self": "dbs/2cRoAA==/colls/2cRoAIH5HAA=/docs/2cRoAIH5HAAGAAAAAAAAAA==/", "_etag": "\"0f000efa-0000-0000-0000-591fd0aa0000\"", "_attachments": "attachments/", "_ts": 1495257257 } ]
(3) データの検索
引き続き「Data Explorer(Preview)」を使用します。
「Edit Filter」ボタンをクリックします。
フィルター入力テキストボックスが有効になるので、以下のように抽出条件を設定して「Apply Filter」ボタンをクリックします。抽出条件はSQLライクに記述可能です。
where c.Age > 20
条件に該当する3件に絞り込みが行われました。
同様の機能を持ったツールとして「ドキュメント エクスプローラー」「クエリ エクスプローラー」というものがAzureポータル上で提供されています。
(4) 後片付け
コレクションを作成したままにしておくと課金対象となってしまうので削除しておきましょう。
きれいにコレクションの上のデータベースごと削除することとします。
「Data Explorer(Preview)」でデータベース(ExampleDb1)の右側の「・・・」をクリックするとポップアップメニューが表示されます。「Delete Database」を選択します。
確認画面が表示されるので、削除対象のデータベース名を入力して「OK」ボタンをクリックします。
1.2.3 プログラムでデータを操作
次に、前述の「Azureポータルでデータを操作」で行った操作をC#プログラムから行います。
(1) 接続情報の確認
Azureポータルで「キー」を選択します。「読み取り/書き込みキー」タブを選択します。
「URI」と「プライマリキー」を控えておきましょう。
(2) Visual Studio 2017プロジェクトを作成
メニュー「ファイル→新規作成→プロジェクト」を選択し、表示された「新しいプロジェクト」に以下の内容を入力します。
- プロジェクトテンプレート=「Windows クラシックデスクトップ→コンソールアプリ(.NET Framework)」
- 名前=HelloCosmosDb
(3) CosmosDB接続ライブラリ(NuGetパッケージ)を追加
ソリューションエクスプローラからHelloCosmosDbプロジェクトをマウス右ボタンクリックして、表示されたポップアップメニューから「NuGet パッケージの管理」をクリックします。
表示されたNuGetパッケージ管理画面の参照タブで「Microsoft.Azure.DocumentDB」を選択して「インストール」ボタンをクリックします。
「ライセンスへの同意」ダイアログが表示されますが、そのまま「同意する」をクリックします。
(4) データの入れ物の準備(データベースとコレクション)
話を単純にする為に、コンソールアプリケーションのProgram.csにコードを追記していくこととします。
また、以下、各処理のコードスニペットを紹介し、最後に全体の実装を掲載する事とします。
Cosmos DB関連の名前空間を適時usingします。
using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client;
接続情報と作成するデータベース名・コレクション名を定数定義しておきます。
private const string EndpointUrl = "https://cosmosdb.documents.azure.com:443/"; private const string PrimaryKey = "キー"; private const string DatabaseId = "ExampleDb1"; private const string CollectionId = "ExampleCol1";
データベースとコレクションを作成するコードは以下の通りです。
private DocumentClient client; ... public async Task<bool> CreateDatabaseCollection() { // 接続用クライアントオブジェクトの作成 this.client = new DocumentClient( new Uri(EndpointUrl), PrimaryKey); // データベースの作成 await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DatabaseId }); // コレクションの作成 var collection = new DocumentCollection { Id = CollectionId }; await this.client.CreateDocumentCollectionIfNotExistsAsync( UriFactory.CreateDatabaseUri(DatabaseId), collection); return true; }
まずは、接続クライアントオブジェクト「DocumentClient」を作成します。作成したDocumentClientオブジェクトは、後で別の処理でも流用することを考慮し、clientフィールド変数に保持することにしました。
DocumentClientは接続オブジェクトであるため、エンドポイントおよび接続用キーをコンストラクタで渡すことになります。
データベースの作成には「CreateDatabaseIfNotExistsAsync()メソッド」を呼び出します。引数でデータベースIDを指定します。メソッド名から想像がつくように「存在しなかったら作成、存在したらそのまま処理が正常終了」します。
コレクションの作成には「CreateDocumentCollectionIfNotExistsAsync()メソッド」を呼び出します。第1引数に対して「UriFactory.CreateDatabaseUri()メソッド」呼び出しの戻り値を渡しています。第2引数のDocumentCollectionオブジェクトによりコレクションIDを指定しています。
(5) データ(ドキュメント)の作成
前述と同様に以下のようなスキーマのデータ(ドキュメント)を作成します。
{ "id": "1", "FirstName": "Ryuichi", "LastName": "Daigo", "FavoriteDB": "Cosmos DB", "Age": 17, }
上記スキーマに合致したモデルクラス Person を用意します。
namespace HelloCosmosDb { public class Person { public string Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string FavoriteDB { get; set; } public int Age { get; set; } }
Personオブジェクトを5件の固定データで作成する実装は以下の通りです。
DocumentClientオブジェクト(this.client)の「UpsertDocumentAsync()」メソッドを呼び出します。Upsertの名前から分かるように「キーが同じドキュメントが既に存在すれば更新を、存在しなければ作成」処理が行われます。
第1引数は、どのコレクションに対してドキュメントを作成するかを表します。
第2引数は、作成するオブジェクトそのものを指定します。
public async Task<bool> CreateDocuments() { string[] id = { "1", "2", "3", "4", "5" }; string[] firstName = { "Ryuichi", "Hiroaki", "Susumu", "Hiroko", "Yoko" }; string[] lastName = { "Daigo", "Sakamoto", "Akiyama", "Takada", "Kitahara" }; string[] favoriteDB = { "Cosmos DB", "SQL Server", "Neo4J", "Oracle", "MongoDB" }; int[] age = { 17, 25, 36, 32, 18 }; for(int i=0; i<5; i++) { var person = new Person() { Id =id[i], FirstName=firstName[i], LastName=lastName[i], FavoriteDB = favoriteDB[i], Age = age[i] }; var newDocument = await this.client.UpsertDocumentAsync( UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), person); } return true; }
(6) データの検索
ここまでで、cosmosdbデータベースアカウント → ExampleDb1データベース → ExampleCol1 上に5件のドキュメントが作成された状態です。
Ageが20より多きドキュメントを検索してコンソール出力します。
public void SearchDocuments() { Uri collectionUrl = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId); var query = this.client.CreateDocumentQuery<Person>(collectionUrl) .Where(c => c.Age > 20); foreach (var person in query) { Console.WriteLine(person); Console.WriteLine("-----"); } }
DocumentClientオブジェクト(this.client)の「CreateDocumentQuery()」メソッドでクエリーオブジェクトを作成することができます。Linqメソッドk構文でWhere句を記述することができます。
(7) ソース全体と実行結果
上記に説明した実装およびコードスニペットをまとめた実装の全体コードは以下になります。
//Person.cs using System; namespace HelloCosmosDb { public class Person { public string Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string FavoriteDB { get; set; } public int Age { get; set; } public override string ToString() { return string.Format( "ID:{0}\r\nFirstName:{1}\r\nLastName:{2}\r\nFavoriteDB:{3}\r\nAge:{4}", this.Id, this.FirstName, this.LastName, this.FavoriteDB, this.Age); } } }
// Program.cs using System; using System.Linq; using System.Threading.Tasks; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Client; namespace HelloCosmosDb { class Program { private const string EndpointUrl = "https://cosmosdb.documents.azure.com:443/"; private const string PrimaryKey = "キー"; private const string DatabaseId = "ExampleDb1"; private const string CollectionId = "ExampleCol1"; private DocumentClient client; static void Main(string[] args) { Console.WriteLine(UriFactory.CreateDatabaseUri(DatabaseId)); var program = new Program(); program.CreateDatabaseCollection().Wait(); program.CreateDocuments().Wait(); program.SearchDocuments(); } public async Task<bool> CreateDatabaseCollection() { // 接続用クライアントオブジェクトの作成 this.client = new DocumentClient( new Uri(EndpointUrl), PrimaryKey); // データベースの作成 await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DatabaseId }); // コレクションの作成 var collection = new DocumentCollection { Id = CollectionId }; await this.client.CreateDocumentCollectionIfNotExistsAsync( UriFactory.CreateDatabaseUri(DatabaseId), collection); return true; } public async Task<bool> CreateDocuments() { string[] id = { "1", "2", "3", "4", "5" }; string[] firstName = { "Ryuichi", "Hiroaki", "Susumu", "Hiroko", "Yoko" }; string[] lastName = { "Daigo", "Sakamoto", "Akiyama", "Takada", "Kitahara" }; string[] favoriteDB = { "Cosmos DB", "SQL Server", "Neo4J", "Oracle", "MongoDB" }; int[] age = { 17, 25, 36, 32, 18 }; for(int i=0; i<5; i++) { var person = new Person() { Id =id[i], FirstName=firstName[i], LastName=lastName[i], FavoriteDB = favoriteDB[i], Age = age[i] }; var newDocument = await this.client.UpsertDocumentAsync( UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), person); } return true; } public void SearchDocuments() { Uri collectionUrl = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId); var query = this.client.CreateDocumentQuery<Person>(collectionUrl) .Where(c => c.Age > 20); foreach (var person in query) { Console.WriteLine(person); Console.WriteLine("-----"); } } } }
実行結果は以下の通りです。
// 実行結果 ID:600bb1b7-fdfc-479c-b295-8fc3f589b45a FirstName:Hiroaki LastName:Sakamoto FavoriteDB:SQL Server Age:25 ----- ID:a2b5fd96-de4f-4283-bd10-78bfd5056505 FirstName:Susumu LastName:Akiyama FavoriteDB:Neo4J Age:36 ----- ID:82e9770d-7a9f-4b0e-adbb-849051912154 FirstName:Hiroko LastName:Takada FavoriteDB:Oracle Age:32 ----- 続行するには何かキーを押してください . . .
Azure Cosmos DB入門(目次)
「Azure Cosmos DB入門」目次
DocumentDB が Azure Cosmos DB としてリニューアルされたので、改めてこのサービスの全体像を整理したコンテンツ(ブログ)を「Cosmos DB入門」として書こうと思います。
目次は以下の通り。順次コンテンツを追加していく予定です。
※ 2017/07/09に(1)~(8)のコンテンツが揃いました。今後もCosmos DBのトピックをブログに継続してエントリーしていきます。
- Azure CosmosDB入門(1)
- 1 Cosmos DBとは
- 1.1 特徴
- 1.1.1 グローバル分散可能
- 1.1.2 柔軟なスケーリングが可能
- 1.1.3 超高速な応答速度
- 1.1.4 マルチデータモデル
- 1.1.5 多様な一貫性モデル
- 1.1.6 高いレベルのSLA
- 1.1.7 Cosmos DBは自分たちが普通に使うものか?
- 1.2 Hello Cosmos DB
- 1.2.1 AzureポータルでCosmos DBを作成
- 1.2.2 Azureポータルでデータを操作
- 1.2.3 プログラムでデータを操作
- 1.1 特徴
- 1 Cosmos DBとは
- Azure CosmosDB入門(2)
- 2 Cosmos DBの主要概念
- 2.1 データモデルとアクセスAPI
- 2.1.1 atom-record-sequence(ARS)
- 2.1.2 AzureポータルでCosmos DBを作成する
- 2.2 各種データモデル
- 2.2.1 「ドキュメント」モデル
- 2.2.2 「グラフ」 モデル(2017/5現在Preview版)
- 2.2.3 「テーブル」モデル(2017/5現在Preview版)
- 2.3 パーティショニング
- 2.4 RU(Request Unit)
- 2.4.1 1RUとは?
- 2.4.2 RUはコレクションに割り当てる
- 2.4.3 RUという概念のすばらしさ
- 2.4.4 予約RUの設定方法
- 2.4.5 消費RUの確認方法
- 2.4.6 RUの話は深い・・・(後半に続く)
- 2.1 データモデルとアクセスAPI
- 2 Cosmos DBの主要概念
- Azure CosmosDB入門(3)
- 3 Cosmos DBプログラミング ~ DocumentDB編(前編)
- 3.1 まず、はじめに
- 3.2 準備
- 3.3 ドキュメントの作成・更新
- 3.4 ドキュメントの検索
- 3.5 ドキュメントの削除
- 3.6 つづく・・・
- 3.7 資料
- 3 Cosmos DBプログラミング ~ DocumentDB編(前編)
- Azure CosmosDB入門(4)
- Azure CosmosDB入門(5)
- 5 Cosmos DBプログラミング ~ MongoDB編
- 5.1 はじめに
- 5.1.1 Cosmos DB(MongoDBデータモデル)とMongoDB
- 5.2 準備
- 5.2.1 Cosmos DB(MongoDB)データベースアカウントの作成
- 5.2.2 Visual Studio 2017プロジェクトの作成
- 5.3 データベースとコレクションの作成
- 5.3.1 Azureポータルで作成
- 5.3.2 プログラムで作成
- 5.3.3 mongoコマンドで作成
- 5.4 ドキュメントの作成
- 5.4.1 ドキュメントの一括投入 ~ Studio 3T for MongoDB(旧Mongo chef)
- 5.5 ドキュメントの検索
- 5.5.1 Mongoクエリー言語による検索
- 5.5.2 LINQによる検索
- 5.6 ドキュメントの更新
- 5.7 ドキュメントの削除
- 5.8 資料
- 5.9 つづく・・・
- 5.1 はじめに
- 5 Cosmos DBプログラミング ~ MongoDB編
- Azure CosmosDB入門(6)
- 6 Cosmos DBプログラミング ~ Gremlin編
- 6.1 はじめに
- 6.1.1 グラフ データモデルとは
- 6.1.2 Gremlinとは
- 6.2 準備
- 6.2.1 Cosmos DB(Gremlin)データベースアカウントの作成
- 6.2.2 Visual Studio 2017プロジェクトの作成
- 6.3 データベースとコレクションの作成
- 6.3.1 プログラムで作成
- 6.4 グラフデータの操作
- 6.4.1 Vertexの追加
- 6.4.2 Edgeの追加
- 6.4.3 Vertexの検索
- 6.4.4 Edgeの検索
- 6.4.5 Vertex~Edge~Vertexを辿る(1)
- 6.4.6 Vertex~Edge~Vertexを辿る(2)
- 6.5 まとめ
- 6.6 資料
- 6.1 はじめに
- 6 Cosmos DBプログラミング ~ Gremlin編
- Azure CosmosDB入門(7)
- 7 Cosmos DBプログラミング ~ Table編
- 7.1 はじめに
- Cosmos DB(Table) と Azure Table Storage
- アクセス用クラスライブラリ
- 7.2 Cosmos DB(Table)
- 7.2.1 データベース構造
- 7.2.2 Cosmos DB固有情報の設定
- 7.3 準備
- 7.3.1 Cosmos DB(Table)データベースアカウントの作成
- 7.3.2 Visual Studio 2017プロジェクトの作成
- 7.4 テーブルの作成
- 7.4.1 Azureポータルでテーブルを作成
- 7.4.2 コードでテーブルを作成
- 7.5 テーブルの操作
- 7.5.1 エンティティ型の定義
- 7.5.2 エンティティの追加
- 7.5.3 エンティティの一括追加
- 7.5.4 エンティティの検索
- 7.5.5 エンティティの更新
- 7.5.6 エンティティの削除
- 7.6 まとめ
- 7.7 資料
- 7.1 はじめに
- 7 Cosmos DBプログラミング ~ Table編
- Azure CosmosDB入門(8)
- 8 Cosmos DBをもっと知りたい
- 8.1 一貫性レベル(Consistency Level)
- 8.1.1 Cosmos DBの提供する一貫性レベル(Consistency Level)
- 8.1.2 既定の一貫性レベル
- 8.2 RU(Request Unit)詳細
- 8.2.1 RUの設定方法
- 8.2.2 消費RUの確認方法
- 8.2.3 RU/s超過時のレスポンスコード429
- 8.2.4 リトライ動作の指定
- 8.2.5 一貫性レベル(Consistency Level)による消費RUの違い
- 8.2.6 RU/m (RU/分) の設定
- 8.3 グローバルレプリケーション
- 8.4 障害復旧およびフェールオーバー
- 8.4.1 自動フェールオーバー
- 8.4.2 手動フェールオーバー
- 8.5 まとめ
※2017/07/09 目次更新
- 8.1 一貫性レベル(Consistency Level)
- 8 Cosmos DBをもっと知りたい
はじめに
先日 2017/5/10-12 に開催された Build 2017 において多数の新しい技術の発表がなされました。
その中の1つとして Azure Cosmos DB も発表されました。
Azure Cosmos DBは従来 DocumentDB として公開されていたサービスに置き換わるものです。厳密には「Cosmos DB が DocumentDB の機能を内包している」という関係性を持ちます。
Azure Cosmos DBは、2010年に「Project Florence」として開始されました。大規模アプリケーション開発の課題に対処するために、マイクロソフト社内向けに開発されていたサービスです。しかし、グローバルに分散されたアプリケーションの構築は、マイクロソフト以外の課題でもあるとの認識から2015年に「Azure DocumentDB」としてサービスリリースされました。
DocumentDBに多数の機能を追加し、そして2017/5/10に Azure Cosmos DB としてサービスリリースが行われました。
※これまでのDocumentDB情報
Cosmos DBは、従来のDocumentDBの知識やコードはそのまま利用可能です。大幅な機能追加によるリニューアルであり、従来のDocumentDBの機能や仕様がひっくり返されている部分はありません。
つまり、ネット上に公開されているDocumentDBの情報は(完全にマッチしないまでも)引き続きCosmos DBにおいて有効な情報です。
DocumentDBライブラリの「1クエリ」は「1HTTPリクエスト」ではない
豆知識的なメモ ブログです。
私自身、DocumentDBを学ぶ上で「あー、そうなんだぁ。ライブラリがいい意味でも、そうでない意味でも、面倒見てくれてるんだなぁ」と思った点について書いておきます。
※本記事では「FeedOptions.MaxItemCount」と「DocumentDBのREST API呼び出しにおけるHTTPヘッダー「x-ms-max-item-count」」について記述しています。
前提条件
以下のモデルクラスデータが 1,000件 、DocumentDBに保存されているものとします。
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; } }
また、ドキュメントは以下のデータベース・コレクションに保存されているものとします。
データベース名: ExampleDB1
コレクション名: ExampleCollection1
ライブラリを使って全件クエリー!
プロジェクトに対してNugetから「Microsoft.Azure.Document」を追加すると以下のようなコードで 1,000件のProductItemオブジェクト を取得することができます。
// リスト1 string EndpointUrl = "https://rddocdb.documents.azure.com:443/"; string PrimaryKey = "[プライマリキー]"; string DatabaseID = "ExampleDB1"; string CollectionID = "ExampleCollection1"; // DocumentClientオブジェクト初期化 DocumentClient client = new DocumentClient(new Uri(EndpointUrl), PrimaryKey); // 全件取得 Uri documentCollectionUri = UriFactory.CreateDocumentCollectionUri(DatabaseID, CollectionID); var query = client.CreateDocumentQuery<ProductItem>(documentCollectionUri); // result変数に1,000件のデータが取得される var result = query.ToList();
実際に発行されたHTTP
上記コードでは、最後の「result = query.ToList();」を実行したタイミングで DocumentDB へのクエリー要求が行われます。
処理完了後に result 変数には1,000件の ProductItemオブジェクト が取得されます。
FiddlerでHTTP通信を監視すると、以下のようなHTTP通信が行われていることを確認することができます。
10回の /docs へのリクエストが行われています。
また、各リクエスト時の リクエストチャージ(x-ms-request-charge)は 38.1 でした。全部で38.1×10のRUを消費。
つまり、ライブラリを利用するコードとしては「query.ToList()」の1文が、10回のHTTPリクエストに分割された、ということです。
x-ms-max-item-countの影響
全1,000件のデータが10回のHTTPリクエストに分割された理由、それはDocumentClient.CreateDocumentQuery()メソッド呼び出し時のFeedOptions引数に影響します。
リスト1ではCreateDocumentQuery()の第2引数を指定しませんでしたが、CreateDocumentQuery()には第2引数にFeedOptionsオブジェクトを指定することができます。
FeedOptions.MaxItemCountの値が1リクエストで取得する項目の最大件数になります(DocumentDBのREST API呼び出しにおけるHTTPヘッダー「x-ms-max-item-count」値)。
また、FeedOptions.MaxItemCountのデフォルト値は「100」となっています。
つまり、これを省略したリスト1では「1リクエストで取得する最大件数=100」で動作を行いました。
10回のリクエストの途中のHTTPヘッダーを見ると以下のようになっています。
上記キャプチャでは、HTTPレスポンスに「x-ms-continuation: {“token”:“qd4DAJ67FQDIAAAAAAAAAA==”,“range”:{“min”:“”,“max”:“FF”}}」が記述されています。
つまり、HTTPリクエスト・レスポンス間での特定クエリーに対するページング処理が挟み込まれています(10件ずつのページング)。
FeedOptions.MaxItemCountを指定する
では、以下のリスト2のように FeedOptions.MaxItemCount= 1000 を指定してみます。
// リスト2 string EndpointUrl = "https://rddocdb.documents.azure.com:443/"; string PrimaryKey = "[プライマリキー]"; string DatabaseID = "ExampleDB1"; string CollectionID = "ExampleCollection1"; // 1度の最大取得項目数は1,000 var feedOption = new FeedOptions() { MaxItemCount = 1000 }; // DocumentClientオブジェクト初期化 DocumentClient client = new DocumentClient(new Uri(EndpointUrl), PrimaryKey); // 全件取得 Uri documentCollectionUri = UriFactory.CreateDocumentCollectionUri(DatabaseID, CollectionID); var query = client.CreateDocumentQuery<ProductItem>(documentCollectionUri, feedOption); var result = query.ToList();
リスト2を実行すると、以下のように1度のHTTPリクエストで1,000件のProductItemオブジェクトを取得することができます。
ちなみにこの時の消費RUは 380.96 となりました。
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; } } }
以下のような感じで、適当なデータが入っています。
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で確認します。
上記Fiddler画面から以下のことが分かります。
- 「/dbs/ExampleDB1/colls/ExampleCollection1/docs」へのページごとのリクエストが行われた
- FeedOptions.MaxItemCount / FeedOptions.RequestContinuation の値がHTTPリクエストヘッダでそれぞれ「x-ms-max-item-count」「x-ms-continuation」して指定されている
- HTTPレスポンスヘッダ「x-ms-continuation」で、次のページを読み込むための継続状態文字列が返されている
資料
公式ドキュメントでは以下を見ればいいのかな。
Common DocumentDB REST request headers | Microsoft Docs
Common Azure Cosmos DB REST response headers | Microsoft Docs
DocumentDB のインデックス設定によるRUの変動について
DocumentDBのインデックスについて調べてのでブログに記しておこうと思います。
DocumentDBではデフォルト設定でも結構いい感じに動くようにインデックス設定が行われているのですが、カスタマイズ可能であり、これを最適化するとRUや(インデックス用)データサイズの節約につながります。
※RU = RequestUnit。DocumentDBにおける「単位時間(秒)あたりの処理数の抽象単位」。課金に大きく影響する数値です。
1. デフォルトのインデックス設定
DocumentDBでは、インデックスはコレクションに対して設定可能です。
またコレクションを作成すると、デフォルトでは、以下のようなインデックスポリシーが設定されます。
{ "indexingMode": "consistent", "automatic": true, "includedPaths": [ { "path": "/*", "indexes": [ { "kind": "Range", "dataType": "Number", "precision": -1 }, { "kind": "Hash", "dataType": "String", "precision": 3 } ] } ], "excludedPaths": [] }
これは、Azureポータル上で「対象DocumentDBアカウント → DataExploere → 対象データベース → 対象コレクション → Scale & Settings」で確認することができます。
「includedPaths」はインデックス適用対象のパス、「excludedPaths」はインデックス所帯対象のパス、という意味になります。
「path: “/*"」とは、「格納されたJSONデータ要素のすべて(配下の階層を含めてすべて)に対して」という意味を持ちます。
そして ”/*" に対して、「Range」「Hash」の2種類のインデックスが設定されています。
それぞれのインデックス種類の意味は、Azure Cosmos DB indexing policies | Microsoft Docsに記述されており、以下が対象箇所の引用になります。
Hash:
Hash を /prop/? (または/) に使用して、以下のクエリを効率的に処理することができます。
SELECT FROM collection c WHERE c.prop = “value”
Hash を /props/[]/? (または / または /props/) に使用して、以下のクエリを効率的に処理することができます。
SELECT tag FROM collection c JOIN tag IN c.props WHERE tag = 5Range:
Range を /prop/? (または/) に使用して、以下のクエリを効率的に処理することができます。
SELECT FROM collection c WHERE c.prop = “value”
SELECT FROM collection c WHERE c.prop > 5
SELECT FROM collection c ORDER BY c.prop
インデックスの種類にはもう1つ「Spatial」があります。これはGEOクエリーを行う為のインデックスとなります。
つまり、コレクションに対するデフォルトのインデックス設定は、一般的な「文字列比較」や「数値比較」による抽出、また並べ替えのためのインデックスが、すべてのドキュメント要素に適用されることになります。
2. 下準備
さて、本投稿では「インデックスの最適化によるRUの変動」を調べるのですが、そのためのデータを準備したいと思います。
以下のPhotoItemクラスをコレクションに保存するドキュメントエンティティとします。
public class PhotoItem { [JsonProperty(PropertyName = "id")] public string Id { get; set; } // タイトル public string Title { get; set; } // 「いいね」数 public int LikeCount { get; set; } // 写真のURI public string PhotoUri { get; set; } // カテゴリ public string Category { get; set; } }
データベース・コレクションは以下のDocumentManagerクラスのInitializeDocumentClient()メソッドで作成します。
データベース名は ExampleDB1、コレクション名は ExampleCollection1、パーティションキーとして Category を設定します。
CreateExampleDocument(PhotoItem photoItem)メソッドは、PhotoItemドキュメントを作成します。
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; // データベースとコレクションを作成 public async Task<bool> InitializeDocumentClient() { this.client = new DocumentClient(new Uri(DocumentManager.EndpointUrl), DocumentManager.PrimaryKey, new ConnectionPolicy { ConnectionMode = ConnectionMode.Gateway, ConnectionProtocol = Protocol.Https }); // ExampleDB1 データベースを作成 await this.client.CreateDatabaseIfNotExistsAsync( new Database { Id = DocumentManager.DatabaseID }); // ExampleCollection1 コレクションを作成(パーティションキーはCategory) Collection<string> partitionPaths = new Collection<string>(); partitionPaths.Add("/Category"); var collection = new DocumentCollection { Id = DocumentManager.CollectionID, PartitionKey = new PartitionKeyDefinition() { Paths = partitionPaths } }; collection = await this.client.CreateDocumentCollectionIfNotExistsAsync( UriFactory.CreateDatabaseUri(DocumentManager.DatabaseID), collection); return true; } // ドキュメントを作成 public async Task<Document> CreateExampleDocument(PhotoItem photoItem) { // ドキュメント作成 var doc = await this.client.CreateDocumentAsync( UriFactory.CreateDocumentCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID), photoItem); return doc; } ...省略 }
以下のコードで DocumentManagerクラス を呼び出してデータベース・コレクション・10,000件のドキュメントを作成します。
// DocumentManagerクラスを呼び出してデータベースを初期化 Random random = new Random(); var manager = new DocumentManager(); var initResult = await manager.InitializeDocumentClient(); string[] categories = { "sky", "sea", "mountain", "people" }; for (int i = 0; i < 10000; i++) { PhotoItem item = new PhotoItem() { Id = i.ToString(), Title = string.Format("SampleTitle{0}", i.ToString()), LikeCount = random.Next(1000), PhotoUri = string.Format("https://xxxxxxx/{0}", i.ToString()), Category = categories[i%4] }; var ret = await manager.CreateExampleDocument(item); System.Threading.Thread.Sleep(50); // 429しないように緩やかに追加する Console.WriteLine(i); }
3. デフォルト状態での書き込みと読み込み
デフォルトのインデックス状態で書き込みと読み込みを確認してみます。
書き込み
10件のドキュメントを書き込んでみます。
Random random = new Random(); var manager = new DocumentManager(); var initResult = await manager.InitializeDocumentClient(); { string[] categories = { "sky", "sea", "mountain", "people" }; for (int id = 10000; id < 10010; id++) { PhotoItem item = new PhotoItem() { Id = id.ToString(), Title = string.Format("SampleTitle{0}", id.ToString()), LikeCount = random.Next(1000), PhotoUri = string.Format("https://xxxxxxx/{0}", id.ToString()), Category = categories[id % 4] }; var ret = await manager.CreateExampleDocument(item); } }
上記ソースにより送信されたHTTPリクエストは以下のようになります(ここでは1件のみ抽出。続けて実施した10件の書き込みのRUは同一であった為)。
POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1 x-ms-documentdb-partitionkey: ["sea"] x-ms-date: Thu, 04 May 2017 16:46:29 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3deHQp5YAx4oaQAkFvxYn0vh9sMbLFqPliTss6TqfrT1Y%3d x-ms-session-token: 0:10043 Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-01-19 Accept: application/json Host: rddocdb-japaneast.documents.azure.com Content-Length: 109 Expect: 100-continue {"id":"10001","Title":"SampleTitle10001","LikeCount":469,"PhotoUri":"https://xxxxxxx/10001","Category":"sea"}
そして、HTTPレスポンスは以下の通りです。
HTTP/1.1 201 Created Cache-Control: no-store, no-cache Pragma: no-cache Transfer-Encoding: chunked Content-Type: application/json Server: Microsoft-HTTPAPI/2.0 Strict-Transport-Security: max-age=31536000 x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:05.215 GMT etag: "00003133-0000-0000-0000-590b5ae50000" x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760; x-ms-resource-usage: documentSize=6;documentsSize=3912;documentsCount=10031;collectionSize=6560; x-ms-schemaversion: 1.3 x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1 x-ms-content-path: FvRbAKerSQA= x-ms-quorum-acked-lsn: 10043 x-ms-current-write-quorum: 3 x-ms-current-replica-set-size: 4 x-ms-xp-role: 1 x-ms-request-charge: 6.1 x-ms-serviceversion: version=1.13.76.2 x-ms-activity-id: 6f53bfbf-d4b1-4ad4-9bf1-b4ce7367636b x-ms-session-token: 0:10044 x-ms-gatewayversion: version=1.13.76.2 Date: Thu, 04 May 2017 16:46:28 GMT ...省略...
消費RUは・・・「x-ms-request-charge: 6.1」。
1件の書き込みRUは 6.1 でした。
読み込み
ドキュメントを読み込みを行います。
DocumentManagerクラスに検索用メソッドQueryPhotoItems()を追加します。
QueryPhotoItems()はかなり固定的な実装で、LikeCountが100より大きく、Categoryが sky のドキュメントを50件取得します。
public class DocumentManager { ...省略 public async Task<List<PhotoItem>> QueryPhotoItems() { List<PhotoItem> result = null; var feedOption = new FeedOptions() { MaxItemCount = 50 }; var query = this.client.CreateDocumentQuery<PhotoItem>( UriFactory.CreateDocumentCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID), feedOption) .Where(i => i.LikeCount > 100 && i.Category == "sky") .Take(50); result = query.ToList(); return result; } }
DocumentManager.QueryPhotoItems()を呼び出す実装は以下の通り。
var manager = new DocumentManager(); var initResult = await manager.InitializeDocumentClient(); // LikeCountが>100のドキュメントを50件検索 var result = await manager.QueryPhotoItems();
上記で実行されるHTTPリクエストは以下の通りです。
POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1 x-ms-continuation: x-ms-documentdb-isquery: True x-ms-max-item-count: 50 x-ms-documentdb-query-enablecrosspartition: False x-ms-documentdb-query-iscontinuationexpected: False x-ms-date: Thu, 04 May 2017 16:59:15 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3dKF%2fbc5aVHlTffYtoHF4k5qzALl727%2bNdHlERf3plj7U%3d Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-01-19 Accept: application/json Content-Type: application/query+json Host: rddocdb-japaneast.documents.azure.com Content-Length: 109 Expect: 100-continue {"query":"SELECT TOP 50 * FROM root WHERE ((root[\"LikeCount\"] > 100) AND (root[\"Category\"] = \"sky\")) "}
HTTPレスポンスは以下の通りです。
HTTP/1.1 200 Ok Cache-Control: no-store, no-cache Pragma: no-cache Transfer-Encoding: chunked Content-Type: application/json Server: Microsoft-HTTPAPI/2.0 Strict-Transport-Security: max-age=31536000 x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:13.755 GMT x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760; x-ms-resource-usage: documentSize=6;documentsSize=4298;documentsCount=10040;collectionSize=6964; x-ms-item-count: 50 x-ms-schemaversion: 1.3 x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1 x-ms-content-path: FvRbAKerSQA= x-ms-xp-role: 2 x-ms-request-charge: 20.32 x-ms-serviceversion: version=1.13.76.2 x-ms-activity-id: 2f1f663c-09e2-4913-b20d-8ef5ac3d63c2 x-ms-session-token: 0:10052 x-ms-gatewayversion: version=1.13.76.2 Date: Thu, 04 May 2017 16:59:15 GMT ...省略...
x-ms-request-charge: 20.32。
消費RUは 20.32 でした。
4. LikeCount/Categoryのみのインデックス設定で書き込みと読み込み
インデックスを以下のように変更します。
LikeCount / Categoryのみにインデックスを適用するようにカスタマイズします。
{ "indexingMode": "consistent", "automatic": true, "includedPaths": [ { "path": "/LikeCount/*", "indexes": [ { "kind": "Range", "dataType": "Number", "precision": -1 } ] }, { "path": "/Category/*", "indexes": [ { "kind": "Hash", "dataType": "String", "precision": 3 } ] } ], "excludedPaths": [ { "path": "/*" } ] }
設定はAzureポータルで「設定 → インデックス作成ポリシー」から変更することができます。
書き込み
先程と同様に10件のドキュメントを書き込んでみます。
HTTPリクエストは以下の通りです。
POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1 x-ms-documentdb-partitionkey: ["mountain"] x-ms-date: Thu, 04 May 2017 17:03:09 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3deHdnuMnm8pSUdfK4T0ItZPjQzGvmyzqBOEExfbDCuUw%3d Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-01-19 Accept: application/json Host: rddocdb-japaneast.documents.azure.com Content-Length: 114 Expect: 100-continue {"id":"10010","Title":"SampleTitle10010","LikeCount":782,"PhotoUri":"https://xxxxxxx/10010","Category":"mountain"}
HTTPレスポンスは以下の通りです。
HTTP/1.1 201 Created Cache-Control: no-store, no-cache Pragma: no-cache Transfer-Encoding: chunked Content-Type: application/json Server: Microsoft-HTTPAPI/2.0 Strict-Transport-Security: max-age=31536000 x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:05.215 GMT etag: "00004e33-0000-0000-0000-590b5ecd0000" x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760; x-ms-resource-usage: documentSize=6;documentsSize=4317;documentsCount=10040;collectionSize=6965; x-ms-schemaversion: 1.3 x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1 x-ms-content-path: FvRbAKerSQA= x-ms-quorum-acked-lsn: 10053 x-ms-current-write-quorum: 3 x-ms-current-replica-set-size: 4 x-ms-xp-role: 1 x-ms-request-charge: 5.52 x-ms-serviceversion: version=1.13.76.2 x-ms-activity-id: fedec436-d43f-479c-b8c7-8ce095d9dd07 x-ms-session-token: 0:10054 x-ms-gatewayversion: version=1.13.76.2 Date: Thu, 04 May 2017 17:03:08 GMT ...省略...
消費RUは・・・「x-ms-request-charge: 5.52」。
1件の書き込みRUは 5.52 でした。
デフォルトインデックス状態の 6.1 よりも小さな値になりました。
読み込み
ドキュメントを読み込みを行います。
先程と同様の読み込みを行います。
HTTPリクエストは以下の通りです。
POST https://rddocdb-japaneast.documents.azure.com/dbs/ExampleDB1/colls/ExampleCollection1/docs HTTP/1.1 x-ms-continuation: x-ms-documentdb-isquery: True x-ms-max-item-count: 50 x-ms-documentdb-query-enablecrosspartition: False x-ms-documentdb-query-iscontinuationexpected: False x-ms-date: Thu, 04 May 2017 17:10:13 GMT authorization: type%3dmaster%26ver%3d1.0%26sig%3dvO5eIEtwI7FZ3%2bD8d8ezEIvH3Hp9EuY1EJ8ZR0Rm2WE%3d Cache-Control: no-cache x-ms-consistency-level: Session User-Agent: documentdb-dotnet-sdk/1.13.2 Host/32-bit MicrosoftWindowsNT/6.2.9200.0 x-ms-version: 2017-01-19 Accept: application/json Content-Type: application/query+json Host: rddocdb-japaneast.documents.azure.com Content-Length: 109 Expect: 100-continue {"query":"SELECT TOP 50 * FROM root WHERE ((root[\"LikeCount\"] > 100) AND (root[\"Category\"] = \"sky\")) "}
HTTPレスポンスは以下の通りです。
HTTP/1.1 200 Ok Cache-Control: no-store, no-cache Pragma: no-cache Transfer-Encoding: chunked Content-Type: application/json Server: Microsoft-HTTPAPI/2.0 Strict-Transport-Security: max-age=31536000 x-ms-last-state-change-utc: Thu, 04 May 2017 08:40:13.755 GMT x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760; x-ms-resource-usage: documentSize=6;documentsSize=3929;documentsCount=10050;collectionSize=6597; x-ms-item-count: 50 x-ms-schemaversion: 1.3 x-ms-alt-content-path: dbs/ExampleDB1/colls/ExampleCollection1 x-ms-content-path: FvRbAKerSQA= x-ms-xp-role: 2 x-ms-request-charge: 20.32 x-ms-serviceversion: version=1.13.76.2 x-ms-activity-id: 2be70cc0-d86c-40ce-8f72-a2542c954ad9 x-ms-session-token: 0:10063 x-ms-gatewayversion: version=1.13.76.2 Date: Thu, 04 May 2017 17:10:13 GMT ...省略...
消費RUは 20.32 でした。
検索条件(LikeCount・Category)が、デフォルトインデックス時も、その後のカスタムインデックス時も共に対象となっている為に、読み込み時の消費RUが変化しませんでした。
4. 読み込みインデックスの効きを確認する
前述の確認では デフォルトインデックス時・カスタムインデックス時 共にインデックス対象の項目で検索してしまっていました。
検索条件を以下のようにした場合はどうでしょうか?
var query = this.client.CreateDocumentQuery<PhotoItem>( UriFactory.CreateDocumentCollectionUri(DocumentManager.DatabaseID, DocumentManager.CollectionID), feedOption) .Where(i => i.LikeCount > minLikeCount && i.Category == "sky" && i.Title == "SampleTitle0") .Take(50);
Title項目も検索対象としました(このクエリー条件は現実のビジネス的にはちょっと変な条件指定ですが・・・技術的検証としてとらえてください)。
Titleは「デフォルトインデックス時にはインデックス対象」「カスタムインデックス時にはインデックス対象外」となる項目です。
実施結果は以下のようになります。
デフォルトインデックス時(インデックス対象) : x-ms-request-charge: 5.96
カスタムインデックス時(インデックス対象外) : x-ms-request-charge: 204.98
RUに大きな違いが発生しました。
インデックス外の項目での検索の負荷の高さを確認することができます。
まとめ
DocumentDBではデフォルト状態で「すべての項目に対して Hash と Range のインデックスが設定されている」。
GEOクエリーを行わなければ、何もしないでもある程度いい感じに動いてくれる。
インデックスの最適化で消費RUの最適化が図れる。ただし、適切に実施しないと、消費RUに大きな差が生まれるので注意しなければならない。
といった感じでしょうか。