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 資料
本記事のサンプルは以下からダウンロード可能です。