Azure FunctionsからCosmos DBに出力バインドする(2)~.csコンパイル編

1. はじめに

前回のエントリーに引き続きの投稿になります。
Azure Functions 出力バインドを利用し Cosmos DB にデータ出力を行います。

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

前回はAzureポータルのみでFunctionsの作成を行いました。
また、ソースコードの実装は .csx(C#スクリプト) をポータル上で編集しました。

今回は .csファイル で実装を行い、コンパイルアセンブリをAzure Functionsに発行することにします。
.csxでの実装に比べ、実行時コンパイル処理が省かれるので、起動時の動作が早くなります。

本エントリーのみでも理解できる内容として記述しますが、前回のエントリーと重複する部分は手短に記述します。ということで、前回のエントリーを見ていただけるとより理解しやすいかと思います。

ryuichi111std.hatenablog.com

2. 開発環境

コンパイル実装をするため、本エントリーでは以下の環境を利用します。

※ 2017/7/20現在、Stableな Visual Studio 2017 では 「Function Tools for Visual Studio 2017」がサポートされていません。

3. 前提条件

以下のAzure環境を前提条件とします。

3.1 Cosmos DB アカウント

以下のような 出力先 Cosmos DB アカウント を用意している前提とします。

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

API(データモデル)は「SQL (DocumentDB)」です。

3.2 Function App

前回エントリーで作成した以下の「Function App」を用意している前提とします。

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

「アプリ名」:RdExampleFunctionApp
「リソースグループ」:RdExampleFunctionApp
ホスティングプラン」:従量課金プラン
「場所」:西日本
「Storage」:新規作成 rdexamplefunctionapp

4. ② VS2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装

VSを使用した実装に移りますが、「4.1 基本的なAzure Functionsプロジェクトの作成から発行まで」と、その上に「4.2 osmos DB出力バインドの追加からパブリッシュまで」の2段階で進めたいと思います。

4.1 プロジェクトの作成から基本パブリッシュまで

まず第1段階として、基本的なAzure Functionsプロジェクトの作成から発行までを行います。

(1) Visual Studio 2017 Preview(15.3)の起動

Visual Studio 2017 Preview(15.3)を起動します。
「Azure Function Tools for Visual Studio 2017」のインストールを済ませておいてください。

(2) Azure Functionsプロジェクトの作成

「ファイル → 新規作成 → プロジェクト」を選択します。
「新しいプロジェクト」ウィンドウで、テンプレートカテゴリーから「Cloud」を選択、プロジェクトテンプレートとして「Azure Functions」を選択します。
プロジェクト名は、ここでは「CosmosDbBindExampleFunction」としました。

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

以下のようなプロジェクトが作成されます。

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

(3) Functionソース(.cs)の追加

新規作成したプロジェクトには、まだ1つもFunction実装がありません。
Functionの実装ソース(.cs)を追加します。
ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「追加 → 新しい項目」を選択します。

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

表示された「新しい項目の追加」ウィンドウから「Azure Function」を選択し、名前を入力します。ここではデフォルトのまま「Function1.cs」としました。

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

「New Azure Function」ウィンドウが表示されます。

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

ここでは、設定内容は以下とします。

  • トリガーの種類: HttpTrigger
  • AccessRights: Anonymous(今回は簡易に匿名で利用できるようにします)
  • FunctionName: HttpTriggerCSharp

自動生成された Function1.cs は以下の通りです。

// 自動生成された Function1.cs 

using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace CosmosDbBindExampleFunction
{
  public static class Function1
  {
    [FunctionName("HttpTriggerCSharp")]
    public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
    {
      log.Info("C# HTTP trigger function processed a request.");

      // parse query parameter
      string name = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
        .Value;

      // Get request body
      dynamic data = await req.Content.ReadAsAsync<object>();

      // Set name to query string or body data
      name = name ?? data?.name;

      return name == null
        ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
        : req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
    }
  }
}

(4) Nugetパッケージの更新

自動生成されたプロジェクトおよびfunction1.csは、2017/7/20現在の確認では、そのままではビルドが通りません。
Nugetパッケージの更新を行う必要があります。
ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「NuGetパッケージの管理」を選択します。

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

「更新プログラム」を選択、「プレリリースを含める」にチェック、「すべてのパッケージを選択」にチェックし、「更新」ボタンをクリックします。

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

(5) ビルド&発行

ソリューションエクスプローラでプロジェクト名「CosmosDbBindExampleFunction」をマウス右ボタンクリック。表示されたメニューから「発行」を選択します。

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

発行のUIが表示されるので「既存のものを選択」を選択して「発行」ボタンをクリックします(今回はAzureポータル上に既に RdExampleFunctionApp というFunction Appを作成済みの為)。

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

発行対象のFunction Appである「RdExampleFunctionApp」を選択して、「OK」ボタンをクリックします。

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

発行が完了したらAzureポータルでFunctionを確認します。
HttpTriggerCSharp関数が発行されて事を確認することができます。

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

4.2 Cosmos DB出力バインドの追加からパブリッシュまで

次に第2段階として、Cosmos DB出力バインドの追加を行います。

(1) DocumentDB Extensionの追加

Cosmos DBへの出力バインドを実装するために、以下のNuGetパッケージを追加します。

  • Microsoft.Azure.WebJobs.Extensions.DocumentDB

NuGetパッケージの管理画面から「Microsoft.Azure.WebJobs.Extensions.DocumentDB」を検索&選択して「インストール」を行います。

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

(2) Function1.csの実装を修正

Function実装を Cosmos DB出力バインド を行うように修正します。

// Cosmos DB出力バインド実装を加えた Function1.cs

using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

namespace FunctionApp2
{
  public static class Function2
  {
    [FunctionName("HttpTriggerCSharp")]
    public static HttpResponseMessage Run(
      [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
      HttpRequestMessage req,
      [DocumentDB("CommunicationDB", "MessageCol",
            CreateIfNotExists = true,
            ConnectionStringSetting = "cosmosdb_DOCUMENTDB")]
      out object messageDocument,
      TraceWriter log)
    {
      string yourName = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "yourName", true) == 0)
        .Value;

      string message = req.GetQueryNameValuePairs()
        .FirstOrDefault(q => string.Compare(q.Key, "message", true) == 0)
        .Value;

      // 出力パラメータmessageDocumentに設定された値がCosmos DBに出力される
      messageDocument = new
      {
        yourName = yourName,
        message = message
      };

      if (!string.IsNullOrEmpty(yourName) && !string.IsNullOrEmpty(message))
      {
        return req.CreateResponse(HttpStatusCode.OK);
      }
      else
      {
        return req.CreateResponse(HttpStatusCode.BadRequest);
      }
    }
  }
}

上記実装のうち重要なポイントを2つ以下に記述します。

①メインの Run() メソッドの引数

前回の.csxファイル形式での実装では、引数のバインドをAzureポータルのGUI上で行いました。
AzureポータルGUI上での操作は、実際には function.json ファイルに反映されます。つまり、GUI操作はエディタとしての機能であり、本質的には function.json への記述で設定を行いました。
コンパイルベースのFunction定義では、メソッド引数への属性設定により同様の設定を行います(つまり、引数属性設定により入出力バインドの設定を行うことができます)。

  • 第1引数

    [HttpTrigger(AuthorizationLevel.Anonymous, “get”, “post”, Route = null)] HttpRequestMessage req

入力のHttpTrigger定義となります。匿名ユーザーのアクセスを許可し、GET / POST を受け入れます。ルーティング定義はnullとしています。

  • 第2引数

    [DocumentDB(“CommunicationDB”, “MessageCol”, CreateIfNotExists = true, ConnectionStringSetting = “cosmosdb_DOCUMENTDB”)] out object messageDocument

こちらがCosmos DB出力バインドの定義になります。
CommunicationDBデータベース、MessageColコレクションへの出力としています。
CreateIfNotExists=trueは、本Functionの実行時にCosmos DB側のデータベース・コレクションが存在しなかった場合に自動生成するかどうかの設定です。ここでは、trueなので存在しなかったら作成する意味となります。
ConnectionStringSetting 値は、出力先のCosmos DBへの接続文字列を表します。ただし、接続文字列自体ではなく、接続文字列を設定したアプリケーション設定キーとなります。
アプリケーション設定キーは、Azureポータルで設定・確認することができます。
RdExampleFunctionAppを選択し、「アプリケーション設定」をクリックします。

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

「アプリ設定」項目に「キーと値」の設定が行えるので、ここに「cosmosdb_DOCUMENTDB」キーを追加し、値としてCosmos DBアカウントへの接続文字列を追加します(前回の.csx形式によるブログエントリーを実施している場合、既に当該キーは追加されていると思います)。

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

②Cosmos DB ドキュメントの作成

出力バインド定義が行われた引数 messageDocument をnewで生成することで、作成するCosmos DB(DocumentDB)ドキュメントを指定することができます。

messageDocument = new
{
yourName = yourName,
message = message
};

(3) 発行

VSから発行を行います。
※ 既に前述で発行方法は説明したので、ここでは割愛します。

(4) テスト実行

今回発行したFunctionはHttpTriggerで起動します。
匿名ユーザーによるリクエストも許可している為、以下のURLによりキックすることができます。

https://rdexamplefunctionapp.azurewebsites.net/api/HttpTriggerCSharp?yourName=ryuichi.daigo&message=hello azure functions

ブラウザで上記URLをリクエストします。

AzureポータルからCosmos DBのデータエクスプローラを確認すると、以下のようにドキュメントが作成されたことを確認することができます。

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

5 まとめ

Azure Functionsは、超基本でいうとあくまで「Function(関数)」なのですが、今回紹介したBindを含め、やはり色々な機能を持っています。最近ではDurable Functionsなんてのも出てきましたし。
そんな進化し続けるAzure Functionsですが、Visual StudioのToolkitのリリースはやや遅めな印象を持っています。とはいえ、そろそろ正式版も出るんじゃないかなあ・・・とも思うので、そのあたりの開発インフラ事情が更新されたら、本エントリーも改めて修正していきたいと思います。

Azure FunctionsからCosmos DBに出力バインドする(1)~.csx編

Azure Functionsには、その実行に関して「トリガー」「バインド」という概念が存在します。

  • 「トリガー」
    ファンクションの実行のきっかけとなるもの。「1つのファンクション」に「1つのトリガー」が定義されます。
  • 「バインド」
    そのファンクションで処理する入力方向および出力方向のデータを表します。

※Azure Functionsのトリガーとバインドの本質的な解説は以下のあたりを参考に・・・

docs.microsoft.com

1. 今回の実装概要

今回はAzure Functionsの出力バインドとして Cosmos DB を利用する方法についてまとめます。

実装するものは以下のようなイメージになります。

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

まず、用意する Azure Functions は、HTTPリクエストをトリガーとして実行されます。
FunctionsはCosmos DBを出力バインドとします。

つまり、Azure FunctionsをホストするURLに対してHTTPリクエストが行われると、Functionが実行され、結果としてCosmos DBに対してドキュメントが作成される(出力される)、という動きになります。

前提条件

今回は出力バインドとして Cosmos DB を用います。
ということで、前提条件として以下のような Cosmos DB アカウントを用意していることを前提とします。

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

API(データモデル)は「SQL (DocumentDB)」です。

2. 2つの実装方式

Azure Functionsは実運用にも耐えうる状態と考えられますが、開発ツールの統合という意味ではまだまだ過渡期的な状況です(2017/7/19現在)。
今回は以下の2つの方法でAzure Functionsを実装します。

  • ① Azureポータルのみを使ったcsxによる実装
    Azureポータル上の操作のみで実装します。実装ファイルはcsx(C#Script)で行います。

  • Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装
    実装はcsで行います。つまりコンパイルイメージをAzure Functionsにアップします。

共通の初期設定

まず、「1 Azureポータルのみを使ったcsxによる実装」「2 Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装」の両方に共通の設定を行います。

「Function App」の作成

Azureポータルにログインします。

(1) Function Appの検索

「+新規」をクリックし、検索ボックスに「Function App」と入力します。
候補一覧に表示された「Function App」を選択します。

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

(2) 作成

「作成」ボタンをクリックします。

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

(3) パラメータの入力

アプリ名等々を適時入力して「作成」ボタンをクリックします。
ここでは以下の内容としました。
「アプリ名」:RdExampleFunctionApp
「リソースグループ」:RdExampleFunctionApp
ホスティングプラン」:従量課金プラン
「場所」:西日本
「Storage」:新規作成 rdexamplefunctionapp

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

(4) 作成されたリソースの確認

(3)の後、しばらく待っているとAzure Functionsを構成するリソースが作成されます。
作成したリソースグループ「RdExampleFunctionApp」は以下となります。

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

「App Serviceプラン」「ストレージ アカウント」「App Service」の3つのリソースが作成されました。
Azure Functionsは基本的にApp Serviceであり、また、その動作の際にはAzure Storageを利用します。その為、上記のような3つのリソースが生成されました。

3. 「① Azureポータルのみを使った.csxによる実装」

では、Functionsの作成に移ります。

3.1 Function Appの選択

リソースグループから(もしくはリソースの一覧から)「RdExampleFunctionApp」を選択します。

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

3.2 関数の追加

関数の「+」マークをクリックし、右側のペインから「カスタム関数を作成する」をクリックします。

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

カスタム関数作成画面が表示されます。

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

以下の作成パラメータを入力します。
テンプレート:「HttpTrigger - C#
関数名の指定:「HttpTrigger1」
承認レベル:「anonymous」 ※認証なしのHTTPリクエストを受け入れるトリガーとします。

「作成」ボタンをクリックすると関数(Azure Function)が作成されます。

3.3 関数の実行

自動生成された HttpTrigger1 ファンクションを実行してみます。

実行ボタンをクリックします。

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

画面下側の「ログ」ぺインに、コード「log.Info(…)」で実装されたログ文字列が表示されるのを確認することができます。
ひとまず、デフォルトで生成されるファンクションの実行をすることができました。

3.4 Cosmos DB出力バインドの設定

次にCosmos DB出力バインドの設定を行います。
左側のファンクション メニューから「統合」を選択します。

(1) 出力の作成

更に、右側に表示された設定画面から、出力→「+新しい出力」をクリックします。

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

(2) DocumentDB形式の選択

一覧から「Azure DocumentDB ドキュメント」を選択し「選択」ボタンをクリックします。

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

(3) 出力バインドの詳細設定

Azure DocumentDB(Cosmos DB)出力バインド設定を行います。
設定項目は以下の通りです。

  • ドキュメントパラメーター名:ファンクションの引数名。この名前で受け取った引数(変数)でCosmos DBのドキュメントを操作します。
  • データベース名:操作対象のデータベース名を設定します。
  • コレクション名:操作対象のコレクション名を設定します。
  • DocumentDBのデータベースとコレクションが作成されるようにしますか?:データベース・コレクションが未作成状態の時に、自動生成するかどうかのフラグを設定します。

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

続けて「DocumentDBアカウント接続」→「新規」をクリックします。

以下の画面のように、Cosmos DBアカウント一覧が表示されるので、操作対象のCosmos DBアカウント「cosmosdb」を選択します。

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

以上で、Cosmos DBへの出力バインドの設定が完了しました。
以下が設定完了後の画面です。

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


※2017/7/20追記
上記、Azureポータル上でのバインド設定は結果として function.json に反映されます(以下の画面)。

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

AzureポータルGUI操作によるバインド設定は、function.jsonを編集する為のエディタであり、本質的な設定記述場所は function.json となります。


3.5 関数(ファンクション)実装の修正

次に、関数(ファンクション)の実装を修正します。
左側のメニュー(?)から「HttpTrigger1」を選択し、run.csxのエディタ画面を表示します。

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

実装を以下に修正して、「保存」ボタンをクリックします。

using System.Net;

public static HttpResponseMessage Run(HttpRequestMessage req, out object outputDocument, TraceWriter log)
{
  string yourName = req.GetQueryNameValuePairs()
    .FirstOrDefault(q => string.Compare(q.Key, "yourName", true) == 0)
    .Value;

  string message = req.GetQueryNameValuePairs()
    .FirstOrDefault(q => string.Compare(q.Key, "message", true) == 0)
    .Value;

  outputDocument = new {
    yourName = yourName,
    message = message
  };


  if (!string.IsNullOrEmpty(yourName) && !string.IsNullOrEmpty(message)) {
    return req.CreateResponse(HttpStatusCode.OK);
  }
  else {
    return req.CreateResponse(HttpStatusCode.BadRequest);
  }
}

実装内容のポイントは、以下の通り。

  • クエリーパラメータとして「yourName」「message」の2つのパラメータを文字列で受けとります。
  • 受け取ったパラメータからCosmos DBのドキュメントを生成します。
  • 引数「outputDocument」は、出力バインド設定「1.4 (3)」の「ドキュメントパラメーター名」と一致している必要があります。

3.6 テスト実行

右側の「テスト」をクリックし、HTTPメソッドを「GET」に設定します。
「クエリ」には「パラメータの追加」で「yourName」および「message」を追加します。
これらは既に実装した関数(ファンクション)の仕様に合わせた呼び出し方法となります。
「実行」ボタンをクリックします。

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

3.7 テスト結果の確認

AzureポータルでCosmos DB(cosmosdbアカウント)の「データエクスプローラ」を表示します。
以下の画面のように ドキュメント が生成されていることを確認することができます。

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

4. まとめ(&つづく・・・)

ということで、思ったより長くなったので「(2) Visual Studio 2017 Preview(Ver.15.3)を使ったcsコンパイルによる実装」は、あらためエントリーを分けて投稿したいと思いますm(_ _)m。

おまけ

AzureポータルGUIで設定した Cosmos DB出力バインド による接続文字列設定は、以下の場所に保存されています。
作成した Azure Functions「RdEzampleFunctionApp」を選択し、「アプリケーション設定」を選択します。

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

「アプリ設定」の下に構成設定として設定値が保存されています。

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

Azure Cosmos DB入門(8)

本コンテンツは「Azure Cosmos DB入門」の(8)です

ryuichi111std.hatenablog.com

8 Cosmos DBをもっと知りたい

これまでの「Azure Cosmos DB入門(1)~(7)」では、Azure Cosmos DBの概要から各データモデル(APIモデル)毎のプログラミング手法について説明してきました。
今回は「Azure Cosmos DB入門」の最終回として、Cosmos DBにおけるいくつかのトピックにスポットを当てて、少しだけ技術的に踏み込んでみたいと思います。

8.1 一貫性レベル(Consistency Level)

データベースにおいては「データの一貫性」は非常に重要な要素です。
書き込んだデータは、当然の如く「書き込んだ値」にならなければなりません。
しかし、話はそれほど単純ではなく、このあたりの考え方には「CAP定理」というものが関係してきます。

CAP定理

CAP定理とは・・・その頭文字である「Consistency(一貫性 )・Availability(可用性)・Partition-tolerance(分断耐性 )」の3つの要素は同時に保証することは出来ない、という定理です。
C / A / Pのうち2つは保証することができるが、3つを同時に保証することはできないというものです。
一般的に、RDBにおいてはCとAを保証する設計となっています。分散型NoSQLデータベースにおいては、AとPを保証する設計となっています。
つまり、分散型NoSQLデータベースシステムである Cosmos DB も AとP の保証を優先しています。
ただし、2者を取り1者を完全に捨てるわけではなく、バランスとして2つを優先し1つの優先度を落とす、というのが実際のところの落としどころとなります。
CAP定理と、それに対する RDB / 分散型NoSQL のアプローチの考え方については、ちょっとした1冊の本としても成り立つくらいの内容なので、ここでは省略させていただきます。
ググるとたくさん情報が出てくるので、そちらをご参照ください。

多くの分散型NoSQLのアプローチ

多くの分散型NoSQLデータベースでは「Eventual」および「Strong」という一貫性レベルを提供することが多いです。
この二者択一に限らず、いくつかの細かな一貫性レベルの指定が可能なデータベースは存在しますが、分散型NoSQLデータベースにおける代表的な一貫性レベルは、この2つになります。

Eventualとは「結果整合性」の一貫性レベルを表します。更新されたデータは、最終的に一貫した値に収束するという考え方です。
下図のように、更新されたデータは一定の時間を経ると収束し、一貫性を保ちます。逆に言うと、収束するまでの間は、データは最新のデータを読み取れることもあれば、少し古いデータが読み取られることもあります。

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

一方 Strongでは、必ず最新のデータを読み取ることができます。

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

処理としては必ず最新のデータが読み取れた方が、シンプルであり、その方がうれしいでしょう。ただし、分散型NoSQLデータベースにおいては、Strong一貫性レベルによる読み取りは負荷の高い処理になります。RDBとは異なるアプローチにより、スケーラビリティ・アベイラビリティ・ローレイテンシーを目指す分散型NoSQLにおいては Strong は良い選択肢ではありません。
SNSのタイムライン表示などでは結果整合性を採用しても大きな害は発生しません。しかし、銀行システムの預金口座の表示などで、リロードしたタイミングで通帳の残高状況が変わってしまうのは大きな問題があります。
それぞれの業務要件を満たすために、NoSQLデータベース機能による一貫性の管理、および、アプリケーションロジックによる業務要件の担保、といった設計上の考慮が必要になるのが従来のRDBとCosmos DBのような分散型NoSQLの違いでもあります。

※ Cosmos DBにおいてStrong一貫性レベルを使用した場合、クエリー レイテンシーSLA保証されている為、SLA範囲を超える速度の劣化ではなく 消費RUの増加 という影響が現れます。
※ 一貫性という言葉に関してAzureポータルでは「整合性」と記述している箇所があります。これ以降、「一貫性」「整合性」の2つの言葉は同意とします。

8.1.1 Cosmos DBの提供する一貫性レベル(Consistency Level)

Cosmos DBにおいては 5つ の一貫性レベルが提供されます。
強い一貫性レベルから順に記述したのが、以下のリストになります。

  • Strong
  • Bounded Staleness
  • Session
  • Consistent Prefix
  • Eventual

(1) Strong(強固)

Linearizabile(線形化可能性)を保証する最も強い一貫性レベルです。
あるプロセスにより書き込みが行われると、別のどのプロセスから読み取っても最新のデータを読み取ることができます。
消費RUは高くなります。
また、Strong一貫性レベルでは、グローバルレプリケーションを行うことが出来ません(単一リージョンによる運用に限定されます)。

(2) Bounded Staleness(有界整合性制約)

Bounded Stalenessは K個のバージョンの書き込み もしくは t時間の間隔 だけ読み取りが遅れる可能性があります(つまり古いデータを読み取る可能性があります)。
Azureポータル上での設定画面は以下になります(データベースアカウント → 既定の整合性)。

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

最大ラグ操作(K個)および最大ラグ時間(t間隔)の設定可能範囲はグローバルレプリケーションの設定有無により異なります。

グローバルレプリケーション 最大ラグ操作数(K) 最大ラグ時間(t)
あり(レプリカあり) 100,000~1,000,000 5分~1日
なし(レプリカなし) 10~1,000,000 5秒~1日

やはりグローバルレプリケーションの有無によりその設定可能数値範囲に大きな相違があることが分かります。
また、消費RUは高くなります(Strongと同等)。

(3) Session(セッション)

読み取るデータは最新である保証はありません。ただし、最終的に書き込まれたデータは反映され一貫性と保った状態に収束します。
一貫性レベルとしては弱いですが、この後説明する「Consistent Prefix」「Eventual」と異なり、クライアント セッションにフォーカスした一貫性レベルとなります。
セッションにおいて monotonic reads / monotonic writes / read your own writes (RYW) が保証されています。
* monotonic reads
当該セッションから読み取れるデータは、読み取る毎にさらに古いデータにはならない。
* monotonic writes
当該セッションからの同一データへの書き込みは、書き込み順序が保証される。
* read your own writes (RYW)
当該セッションからのデータ書き込みは、(当該セッションからの)読み取りに反映される(書き込んだデータを読み取れる。古いデータが読み込まれることはない。)。

また、消費RUはStrong / Bounded Stalenessより低く、Eventualよりも高くなります。

(4) Consistent Prefix(一貫性のあるプレフィックス)

読み取るデータは最新である保証はありません。ただし、最終的に書き込まれたデータは反映され一貫性と保った状態に収束します。
レプリカに反映される書き込み順序は保証されます。
A→B→Cというデータ更新が行われた場合、クライアントから見ると、A、A→B、A→B→Cと書き込みが行われた処理データは読み込まれますが、A→CとかB→A→Cと順序が入れ替わって書き込まれる様子が見えることはありません。

(5) Eventual(最終的)

最も弱い一貫性レベルです。
読み取るデータは最新である保証はありません。ただし、最終的に書き込まれたデータは反映され一貫性を保った状態に収束します。
データを繰り返し読み取る際、1度目に取得したデータより更に古い世代のデータが読み取られることもあります。
読み取りと書き込みの待機時間は最も早くなります。
読み取り操作時の消費RUは最も低くなります。

8.1.2 既定の一貫性レベル

Cosmos DBでは「データベースアカウント」に対して、既定の一貫性レベルを設定することができます。
Azureポータル上で設定することができ、「データベースアカウント → 既定の整合性」メニューで変更することができます。

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

初期状態では、既定値は「セッション」になっています。

読み取り操作を行う場合、明示的な指定を行わない限り、データベースアカウントに対する既定の一貫性レベルが適用されます。
また、「読み取り要求を行う毎」に個別に一貫性レベルを明示的に指定することができます。
以下が例となります(リスト1)。

リスト1 一貫性レベルの明示的な指定(Session一貫性レベルで読み取る例)

DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    new ConnectionPolicy() { // 実運用時はDirect/Tcp推奨
      ConnectionMode = ConnectionMode.Gateway,
      ConnectionProtocol = Protocol.Https},
    ConsistencyLevel.Session);  // ← 一貫性レベルを明示的に指定

// クエリー実行
var query =
    client.CreateDocumentQuery<RoomReservationInfo>(
        UriFactory.CreateDocumentCollectionUri("GroupwareDB", "ReservationCollections")).
        Where(r => r.Room == "大会議室");
var result = query.ToList();

リスト1実行時に発行されたHTTP RequestをFiddlerで確認した結果は以下です。
HTTP Headerに「x-ms-consistency-level: Session」が追加されています。

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: Sun, 09 Jul 2017 03:16:15 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dS7DKHRXurZXz6v6VqkMU8QceYxyDHoWQjAIOOn2N9ao%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ブル\") "}

操作毎の明示的な一貫性レベル指定に関する注意事項として、データベースアカウントに対して設定した既定の一貫性レベルより高いレベルの一貫性レベルを指定することはできません。

8.2 RU(Request Unit)詳細

「Azure Cosmos DB入門(2)」においてもRU(Request Unit)の概要について触れました。
それを踏まえて、また+αの概念も含めて、RUについてのポイントをまとめると、以下のようになります。

  • RUはCosmos DBにおけるクエリーや書き込みなどの「処理の計算量を表す抽象単位」である
    (1RU = 一貫性レベルSessionにおいてキー指定で1KBのデータを1件取得する操作に要する処理量)
  • クエリー処理を行うと、その複雑度・データ量等により消費されるRUが変動する
    (複雑なクエリーほど多くのRUを消費する)
  • RUはコレクションに対して割り当てる
  • 1秒あたり利用可能なRUをコレクションに対して割り当てる
    (Storage Capacity:Fixed(10GB)設定時=400~10,000RU/s。Unlimited設定時=2,500~100,000RU/s)
  • RU/second を超える処理量が一時的に必要になった時のために「RU/m(RU/分)」の設定が可能
  • RUはCosmos DBにおける課金対象の大部分を占める

8.2.1 RUの設定方法

コレクションに割り当てるRU/sの値は、Azureポータルおよびプログラムから行うことができます。
「2.4.4 予約RUの設定方法」ですでに説明したのでそちらを参照してください。

Azure Cosmos DB入門(2) - ryuichi111stdの技術日記

8.2.2 消費RUの確認方法

クエリーや更新の結果消費したRUの量の確認方法は、「2.4.5 消費RUの確認方法」ですでに説明したのでそちらを参照してください。

Azure Cosmos DB入門(2) - ryuichi111stdの技術日記

8.2.3 RU/s超過時のレスポンスコード429

RU/sは事前予約制です。
Cosmos DBは使用した分を精算する方式ではなく、事前にどれだけの処理能力(キャパシティ)を用意するかを RU/s の値として設定します。
RU/sは消費しても、しなくても利用料金として精算請求が行われます。
コレクションの作成と同時にRUの割り当てを行います。つまりコレクションを作成した時点で、格納するデータが空であっても、設定したRU分の課金が開始されます。

運用コストを考慮すると「必要最低限の RU/s値 をコレクションに対して割り当てたい」ということになります。
しかし、クライアントからのトラフィックに対して RU/s値 が十分でなかった、つまり足りなかった場合、レスポンスコード429というエラーが発生します。

レスポンスコード429の確認

では実際に レスポンスコード429 の発生を確認してみます。
以下のような、クエリーをforループで10回繰り返す処理を実行してみます(リスト2)。
処理対象のコレクションは、最小構成のRU/s=400としています。

リスト2 負荷をかけて400RU/sを超過させる

DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    new ConnectionPolicy() {
      ConnectionMode = ConnectionMode.Gateway,
      ConnectionProtocol = Protocol.Https});

// クエリーを10回繰り返す
for(int i = 0; i < 10; i++)  {
  var query =
    this.client.CreateDocumentQuery<RoomReservationInfo>(
      UriFactory.CreateDocumentCollectionUri("GroupwareDB", "RoomReservations"))
      .Where(r => r.Room == "スタンディングテーブル");
  var result = query.ToList();
}

リスト2の実行結果のHTTPS通信をFiddlerで監視した結果は以下の画面です。

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

1度 429 が発生していることを確認できます。

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

レスポンスボディのJSONには、確かに「Requesr rate is large」とのエラーメッセージを確認することができます。
また、レスポンスヘッダに「x-ms-retry-after-ms: 488」という項目が確認できます。
これは 488ms 待ってから再度リクエストを行ってください、という意味を持ちます。
1秒毎にRUのキャパシティが復活するために、指定のリトライ待機時間を取った上で再度のリトライを行うことを促しています。
forループで10回のクエリーを実行しました。そして1回の429(RU超過)エラーが発生しました。その上で、200で成功したクエリー(~/RoomReservations/docs)は10回確認できます。
つまり、429エラーが発生した際は、ライブラリ側でリトライ処理が行われていることが分かります。
仮に、REST APIを自ら実行する方式でCosmos DBを操作した場合には、429の発生の認識およびリトライを自前で実装する必要があります。

8.2.4 リトライ動作の指定

ライブラリが行うリトライ処理の動作については、ユーザープログラム側で制御することができます。
DocumentClientクラスのコンストラクタ引数「ConnectionPolicyクラス」を利用します。
「ConnectionPolicy.RetryOptionsプロパティ」によってリトライ設定を制御することができ、これは「Microsoft.Azure.Documents.Client.RetryOptions」型となります。
以下の2つがRetryOptionsクラスの主要プロパティです。

  • RetryOptions.MaxRetryAttemptsOnThrottledRequestsプロパティ
    RU超過(429)発生に対して、何回までリトライを繰り返すかを設定します。設定したリトライ回数を超えると例外が発生します。

  • RetryOptions.MaxRetryWaitTimeInSecondsプロパティ
    最初の要求からの累積待ち時間の上限を設定します(単位は秒)。この上限値を超える待ち時間が発生すると例外が発生します。

以下がその利用例です(リスト3)。

リスト3 リトライ制御の明示的指定方法

//リトライ回数5回、累積待ち時間60秒の接続ポリシーを作成
ConnectionPolicy cp = new ConnectionPolicy();
cp.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 5;
cp.RetryOptions.MaxRetryWaitTimeInSeconds = 60; 

// ConnectionPolicyを指定してDocumentClientを作成
DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    cp);

//接続ポリシーが適用されたクエリー処理が実行される
var query =
  client.CreateDocumentQuery<RoomReservationInfo>(
    UriFactory.CreateDocumentCollectionUri("GroupwareDB", "ReservationCollections")).
    Where(r => r.Room == "大会議室");
var result = query.ToList();

8.2.5 一貫性レベル(Consistency Level)による消費RUの違い

一貫性レベルの設定が消費RUに影響します。
公式ドキュメントに記された情報からは、以下のような関係性であると考えられます。

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

※図の吹き出しの通り Session と Consistenc Prefix の消費RUの上下関係は未確認です(詳しい方がおられましたら、コメント or RyuichiDaigo@そら (@ryuichi111std) | Twitter までご指摘いただけたら嬉しいです)。

実際の値を確認するために、先程のリスト1を用いて、ConsistencyLevel値をそれぞれに変更してみました。
テストした結果の消費RU値は以下の通りです(絶対的数値の大きさは取得件数・データ量が影響しているので、各一貫性レベル間の相対値の比較が確認対象です)。

Strong Bounded Staleness Session Consistent Prefix Eventual
70.74 70.74 35.37 35.37 35.37

Strong / Bounded Stalenessは高い値。
Session / Consistent Prefix / Eventualが同一値となりました。Eventualが低くなってほしかったのですが、このクエリー処理においては同様のRU消費量のようでした。

※ 上記は、Strongを試すために単一リージョン構成での実行結果です。ただし、グローバルレプリケーション構成でも、同様の結果が得られました(勿論Strong以外)。

8.2.6 RU/m (RU/分) の設定

前述の通り、設定したRU/sの許容量を超えると レスポンスコード 429 のエラーが発生してしまいます。
ライブラリによるリトライ処理が行われ、クエリー処理は継続されますが、当然処理の遅延につながってしまいます。さらにリトライで追いつかない程のキャパシティ不足となった場合、例外が発生してしまいます(つまり、アプリケーションとしてはユーザーに対してタイムアウトエラーを伝えなければなりません)。

アプリケーションでは、時間帯によるトラフィックの変動や、何らかの原因による一時的なトラフィックの増加が発生することがあります。
RU/sの概念のみでこのような状況に対応しようとした場合、最大負荷状態に合わせたRU/s設定を行う必要があります。しかし、一時的な高負荷時間帯以外の時間帯では、過剰なRU/s設定となってしまいコストが増してしまいます。

そこで RU/m(RU/分) という機能が用意されています。
RU/sと同様に、コレクションに対して RU/m の設定を行います。
Azureポータルでの設定画面は以下になります(データベースアカウント → スケール)。

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

設定対象のコレクションをドロップダウンで選択し、「RU/分」項目を「オン」に設定して「保存」ボタンをクリックします。

RU/mの設定値

RU/m(RU/分)は「オン / オフ」の設定です。
RU/mは具体的な数値を設定しません。RU/sに対して10倍の値が自動的に設定されます。
コレクションに対するRU/s設定が 400 であれば、RU/mは 4,000 となります。
また、5,000RU/sまでのコレクションに対してのみ RU/m を有効化することができます。
5,001RU/s以上のスループットを設定したコレクションには RU/m を有効化することはできません。

RU/mの消費例

RU/m(RU/分)がオンに設定されている場合、あるタイミングにおいて、1秒あたりの予約RU/sを超えてしまったとします。その場合、RU/sとは別枠のRU/mが消費されデータベース操作が行われます。

以下に具体的な RU/sとRU/mの消費と回復 の例を説明します。
前提条件は「RU/s = 1,000 、 RU/mオン」とします。

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

①開始点から0.2秒後に200RUを消費する処理が実行されました。
→RU/sの残が800となりました。
②0.4秒後に300RUを消費する処理が実行されました。
→RU/sの残が500となりました。
③0.6秒後に600RUを消費する処理が実行されました。
→RU/sの残が0となりました。RU/sが不足したのでRU/mが消費され、残が9,900となりました。 ④0.9秒後に500RUを消費する処理が実行されました。
→RU/sの残が0なので、RU/mが消費され、RU/mの残が9,400となりました。
⑤1秒後にRU/sが回復しました。
→RU/sの残が1000、RU/mの残はそのまま9,400となりました。
⑥1.2秒後に350RUを消費する処理が実行されました。
→RU/sの残が650となりました。
⑦1.5秒後に800RUを消費する処理が実行されました。
→RU/sの残が0となりました。RU/mが消費され、残が9,250となりました。
⑧1.8秒後に1,500RUを消費する処理が実行されました。
→RU/sの残が0なので、RU/mが消費され、RU/mの残が7,750となりました。
⑨60秒後、RU/mは回復し10,000となります。

8.3 グローバルレプリケーション

Cosmos DBは分散型NoSQLデータベースです。世界各国のAzureリージョンに対してグローバルレプリケーションすることが可能です。
2017/7/9現在、Azureの世界各所 24個所 のリージョンにCosmos DBをレプリケーションすることが可能です。
グローバルレプリケーションを行うことのメリットは、主に以下の2点が挙げられます。

  • 耐障害性の向上
    地理的に離れたリージョンにデータベースをレプリケーションすることで、Disaster Recoveryであったり、リージョン障害時の継続稼働を可能にします。

  • 性能向上
    ユーザーが世界各国に散らばっているようなシステムにおいて、利用者に近いリージョンでデータベースを動作させることで、性能向上を図ります。(ネットワーク遅延の抑制)

8.3.1 書き込みリージョンと読み込みリージョン

Cosmos DBでは、複数のリージョンにデータベースをレプリケーションすることが可能です。
この場合、書き込みリージョンは1つ、それ以外は読み取りリージョンとなります。
つまりレプリケーションを行うことによる地理的性能向上は、読み取りに対して有効なもので、書き込みに関しては、世界で1つの書き込みリージョンに対して行うことになります。
書き込みリージョンに対して書き込まれたデータが、Cosmos DBのバックエンド処理によりレプリケーション構成された各リージョンにコピーされる仕組みとなります。

8.3.2 レプリケーションの作成

レプリケーションは、Azureポータルでマウス操作のみで簡単に行うことができます。
レプリケーションは簡単に作成できますが、レプリケーションしたリージョン毎に 割り当てRU の課金が行われますので注意してください(料金プランについては今後変更の可能性もあるので、公式情報をご確認ください)。
例えば 1,000RU/s のコレクション1つを含むCosmos DBアカウントにおいて、レプリケーションを1つ追加した場合、1,000RU/s × 2リージョン = 2,000RU/sの料金 が発生します。

Azureポータルでレプリケーションを作成する手順は、以下の通りです。

(1) Azureポータルを表示

Azureポータルにログインし、対象のCosmos DBアカウントを選択。さらに「データをグローバルにレプリケートする」メニューを選択します。
世界地図とリージョンを表すマークが表示されます。

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

(2) レプリケーションするリージョンを選択

世界地図上のマークの意味は以下の通りです。

いくつかのリージョンを選択して「保存」ボタンをクリックすると、レプリケーションが開始されます。
小さなデータベースでも、1つレプリケーションを増やすのに数分はかかると思います。ただし、レプリケーション構成中も、データベースは停止することなく、アクティブに動作し続けています。
また公式ドキュメントの記述では「100TBまでのデータであれば30分」でレプリケーション可能であるとのことです。

8.3.3 アプリケーション配置 と Cosmos DB レプリケーション

Cosmos DBはデータベースシステムなので、ユーザーに対して単体で提供するものではありません。
1つの代表的な(そしてもっとも単純な)利用想定は、以下のような形です。

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

Azure App ServiceでWebアプリケーションをホストします。
バックエンドのデータベースとしてCosmos DBを利用します。

そして、ユーザーが世界各国に散らばっていた場合、負荷分散と地理的なネットワーク遅延の問題回避のために以下のような構成をとります。

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

Azure App Serviceを世界のリージョンに分散配置すると共に、Cosmos DBも併せてApp Serviceと同一リージョン(もしくは構成によっては近くのリージョン)に配置(レプリケーション)します。

8.3.4 読み取りリージョンの明示的指定

グローバル レプリケーション設定は、Azureポータル上の操作で簡単に行えました。
しかし、プログラム側でも「どのリージョンから読み取るか」という指定をする必要があります。これを行わないと、データベース自体はグローバルレプリケーションされているが、実際にアプリケーションがアクセスするのは、マスターの書き込みリージョンのみになってしまいます。

具体的なプログラムでの設定方法は以下の通りです。

DocumentClientオブジェクトを生成する際に指定可能な、ConnectionPolicyオブジェクトの「PreferredLocationsプロパティ」を利用します。
ConnectionPolicy.PreferredLocationsプロパティは、「Collection型」で複数のリージョンを優先順位順に設定することができます。
以下のリスト4は「WestEurope」「BrazilSouth」の優先順位を指定してクエリーを行います。
WestEuropeが読み取りできない状態であった場合、BrazilSouthkから読み取りが行われます。WestEurope / BrazilSouthのどちらのリージョンからも読み取れない状況であった場合は、書き込みリージョン(マスターリージョン)からの読み取りが行われます。

リスト4 リージョンを指定して読み取り

// リージョン名はstring型で指定しますが「LocationNames列挙体」が用意されています。
ConnectionPolicy connectionPolicy = new ConnectionPolicy();
connectionPolicy.PreferredLocations.Add(LocationNames.WestEurope);
connectionPolicy.PreferredLocations.Add(LocationNames.BrazilSouth);
connectionPolicy.ConnectionMode = ConnectionMode.Gateway;
connectionPolicy.ConnectionProtocol = Protocol.Https;

DocumentClient client = 
  new DocumentClient(
    new Uri("https://[your account].documents.azure.com:443/"),
    "[your key]",
    connectionPolicy);

var query =
  this.client.CreateDocumentQuery<RoomReservationInfo>(
    UriFactory.CreateDocumentCollectionUri("GroupwareDB", "RoomReservations"))
    .Where(r => r.Room == "大会議室");
var result = query.ToList();

読み取りリージョンを確認する

論理的なお話よりも実際の動きを検証した方が、しっくりと頭に入ってくると思います。
ということで、PreferredLocations指定時のHTTP通信内容をFiddlerで確認してみたいと思います。
前提条件は以下とします。

  • 書き込みリージョン(マスター)は Japan West です
  • レプリケーションされた読み取りリージョンは「WestEurope」と「BrazilSouth」です

実行するコードは、先程のリスト4です。
Fiddlerを確認すると、3つのHTTPS通信が行われています。

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

①「/」へのGET
まず、「https://cosmosdoc.documents.azure.com/」へのGETリクエストを行っています。
特定リージョンではなく「https://cosmosdoc.documents.azure.com/」に対してのGETとなります。
以下にRequest / Reponseのトレースをつらつらと張りつけますが、Response Body内のJSONで「writableLocations / readableLocations」という項目があります。
それぞれ「書き込みリージョン情報」および「読み取りリージョン情報」となります。
これによりDocumentDBライブラリは接続先データベースのマスター及びレプリケーションリージョンの構成を把握します。

--- 1つ目のHTTP Request(/) ---

GET https://cosmosdoc.documents.azure.com/ HTTP/1.1
x-ms-version: 2017-02-22
User-Agent: documentdb-dotnet-sdk/1.14.1 Host/32-bit MicrosoftWindowsNT/6.2.9200.0
x-ms-date: Sat, 08 Jul 2017 13:51:10 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dEYpxSo1lqVjtGwbUoMVFp2eZLPlR8OeC06L5WcsTtEg%3d
Host: cosmosdoc.documents.azure.com
Connection: Keep-Alive


--- 1つ目のHTTP Request(/)に対するResponse ---
HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Content-Location: https://cosmosdoc.documents.azure.com/
Server: Microsoft-HTTPAPI/2.0
x-ms-max-media-storage-usage-mb: 2048
x-ms-media-storage-usage-mb: 0
x-ms-databaseaccount-consumed-mb: 0
x-ms-databaseaccount-reserved-mb: 0
x-ms-databaseaccount-provisioned-mb: 0
Strict-Transport-Security: max-age=31536000
x-ms-gatewayversion: version=1.14.33.2
Date: Sat, 08 Jul 2017 13:51:09 GMT

5B7
{  
  "_self":"",
  "id":"cosmosdoc",
  "_rid":"cosmosdoc.documents.azure.com",
  "media":"//media/",
  "addresses":"//addresses/",
  "_dbs":"//dbs/",
  "writableLocations":[  
    {  
      "name":"Japan West",
      "databaseAccountEndpoint":"https://cosmosdoc-japanwest.documents.azure.com:443/"
    }
  ],
  "readableLocations":[  
    {  
      "name":"Japan West",
      "databaseAccountEndpoint":"https://cosmosdoc-japanwest.documents.azure.com:443/"
    },
    {  
      "name":"Brazil South",
      "databaseAccountEndpoint":"https://cosmosdoc-brazilsouth.documents.azure.com:443/"
    },
    {  
      "name":"West Europe",
      "databaseAccountEndpoint":"https://cosmosdoc-westeurope.documents.azure.com:443/"
    }
  ],
  "userReplicationPolicy":{  
    "asyncReplication":false,
    "minReplicaSetSize":3,
    "maxReplicasetSize":4
  },
  "userConsistencyPolicy":{  
    "defaultConsistencyLevel":"BoundedStaleness"
  },
  "systemReplicationPolicy":{  
    "minReplicaSetSize":3,
    "maxReplicasetSize":4
  },
  "readPolicy":{  
    "primaryReadCoefficient":1,
    "secondaryReadCoefficient":1
  },
  "queryEngineConfiguration":"{\"maxSqlQueryInputLength\":30720,\"maxJoinsPerSqlQuery\":5,\"maxLogicalAndPerSqlQuery\":500,\"maxLogicalOrPerSqlQuery\":500,\"maxUdfRefPerSqlQuery\":2,\"maxInExpressionItemsCount\":8000,\"queryMaxInMemorySortDocumentCount\":500,\"maxQueryRequestTimeoutFraction\":0.9,\"sqlAllowNonFiniteNumbers\":false,\"sqlAllowAggregateFunctions\":true,\"sqlAllowSubQuery\":false,\"allowNewKeywords\":true,\"sqlAllowLike\":false,\"maxSpatialQueryCells\":12,\"spatialMaxGeometryPointCount\":256,\"sqlAllowTop\":true,\"enableSpatialIndexing\":true}"
}

②コレクション情報取得
PreferredLocationsで第1優先先として指定した「WestEurope」に対してのGETリクエストとなります(https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations)。

--- 2つ目のHTTP Request ---

GET https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations HTTP/1.1
x-ms-date: Sat, 08 Jul 2017 13:51:10 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dkTjmw8Dl1qxV8Boc6tjAEgxvV5K32FIYxENUN7CHdOQ%3d
Cache-Control: no-cache
x-ms-consistency-level: BoundedStaleness
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-westeurope.documents.azure.com



--- 2つ目のHTTP Requestに対するResponse ---

HTTP/1.1 200 Ok
Cache-Control: no-store, no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Content-Type: application/json
Content-Location: https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000
x-ms-last-state-change-utc: Tue, 04 Jul 2017 00:10:13.508 GMT
etag: "00007800-0000-0000-0000-5960908e0000"
collection-partition-index: 0
collection-service-index: 0
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/GroupwareDB
x-ms-content-path: -2kXAA==
x-ms-xp-role: 1
x-ms-request-charge: 2
x-ms-serviceversion: version=1.14.33.2
x-ms-activity-id: 4bdac467-4bfd-4710-a3c8-457a154e746a
x-ms-session-token: 0:1539
x-ms-documentdb-collection-index-transformation-progress: -1
x-ms-gatewayversion: version=1.14.33.2
Date: Sat, 08 Jul 2017 13:51:12 GMT

225
{
  "id":"RoomReservations",
  "indexingPolicy":{
    "indexingMode":"consistent",
    "automatic":true,
    "includedPaths":
    [
      {
        "path":"\/*",
        "indexes":[
          {"kind":"Range","dataType":"Number","precision":-1},
          {"kind":"Hash","dataType":"String","precision":3}
        ]
      }
    ],
    "excludedPaths":[]
  },
  "partitionKey":{"paths":["\/Room"],"kind":"Hash"},
  "_rid":"-2kXAO4IMQA=",
  "_ts":1499500658,
  "_self":"dbs\/-2kXAA==\/colls\/-2kXAO4IMQA=\/",
  "_etag":"\"00007800-0000-0000-0000-5960908e0000\"",
  "_docs":"docs\/",
  "_sprocs":"sprocs\/",
  "_triggers":"triggers\/",
  "_udfs":"udfs\/",
  "_conflicts":"conflicts\/"
}
0

③ WestEuropeリージョンに対してクエリー処理が行われます。
https://cosmosdoc-westeurope.documents.azure.com/dbs/GroupwareDB/colls/RoomReservations/docs

--- 3つ目のHTTP Request ---

POST https://cosmosdoc-westeurope.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, 08 Jul 2017 13:51:12 GMT
authorization: type%3dmaster%26ver%3d1.0%26sig%3dod2h8xICdwZh0fqdvSt1Dl6OUZW19p9qHRZVXlOTwp8%3d
Cache-Control: no-cache
x-ms-consistency-level: BoundedStaleness
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-westeurope.documents.azure.com
Content-Length: 98
Expect: 100-continue

{"query":"SELECT * FROM root WHERE (root[\"Room\"] = \"スタンディングテ\\u30fcブル\") "}


--- 3つ目のHTTP Requestに対するResponse ---

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: Tue, 04 Jul 2017 00:10:13.881 GMT
x-ms-resource-quota: documentSize=10240;documentsSize=10485760;documentsCount=-1;collectionSize=10485760;
x-ms-resource-usage: documentSize=1;documentsSize=859;documentsCount=1164;collectionSize=1152;
x-ms-item-count: 97
x-ms-schemaversion: 1.3
x-ms-alt-content-path: dbs/GroupwareDB/colls/RoomReservations
x-ms-content-path: -2kXAO4IMQA=
x-ms-xp-role: 2
x-ms-request-charge: 70.74
x-ms-serviceversion: version=1.14.33.2
x-ms-activity-id: 25dd501b-ff38-49d5-aea4-e8946e204445
x-ms-session-token: 0:1539
x-ms-gatewayversion: version=1.14.33.2
Date: Sat, 08 Jul 2017 13:51:14 GMT

E093
{"_rid":"-2kXAO4IMQA=","Documents":[{"id":"0000000004","Room":"スタンディングテーブル","Title":"アーキテクチャ社内勉強会(06\/02回)","ReservedUserId":"kubota","ReservedUserName":"久保田元也","Start":"2017-06-02T13:00:00.0000000",
...省略...
"_attachments":"attachments\/","_ts":1499500709}],"_count":97}
0

上記トレース情報から、ライブラリがリージョンを確認し、その上で優先指定されたリージョンに対してクエリー操作を発行している様子を確認することができます。

【補足】

つい最近 三宅@ZEN さんも、この点についての投稿を行われているので、そちらも参考になります。
k-miyake.github.io

8.4 障害復旧およびフェールオーバー

グローバルレプリケーションを設定していると、特定リージョンで障害が発生した際にフェールオーバーによる障害復旧を行うことが出来ます。
もしくは、障害とは関係なく手動でのフェールオーバーも可能です。
フェールオーバーには「自動」と「手動」の2つの方法が用意されています。

8.4.1 自動フェールオーバー

自動フェールオーバーについて、「読み取りリージョンが停止した場合」「書き込みリージョンが停止した場合」の、2つのケースについて以下に説明します。

(1) 読み取りリージョンが停止した場合

書き込み(マスター)リージョンは正常稼働状態で、読み取りリージョンが停止した場合の動作について説明します。
データベース操作ロジックにおいて、PreferredLocationsで指定された優先リージョンのうち、有効なリージョンに自動的に接続します。PreferredLocationsで指定されたリージョンがすべて停止している場合は、書き込みリージョンにアクセスします。

具体的な例は以下の通りです。
前提は・・・

  • JapanEastが書き込みリージョン
  • JapanWestおよびBrazilSouthがレプリケーションされた読み取りリージョン
  • 操作ロジックのPreferredLocations設定はリスト5の通り

この時、BrazilSouthが停止した場合には、JapanWestから読み取りが行われます。
BrazilSouthに加えてJapanEastも停止してしまっていた場合には、書き込みリージョンであるJapanEastから読み取りが行われます。

リスト5 
ConnectionPolicy connectionPolicy = new ConnectionPolicy();
connectionPolicy.PreferredLocations.Add(LocationNames.BrazilSouth);
connectionPolicy.PreferredLocations.Add(LocationNames.JapanWest);

(2) 書き込みリージョンが停止した場合

書き込みリージョンが停止した場合の自動フェールオーバーは、「自動フェールオーバーの有効化」が、オンになっている必要があります。
Azureポータル上で「データをグローバルにレプリケートする」→「自動フェールオーバー」で設定を行うことができます。

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

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

書き込みリージョンが停止した場合、書き込みリージョンのデータベースは、自動的にオフラインモードになります。
更に優先度上位の読み込みリージョンが、書き込みリージョンに昇格します。
元々の書き込みリージョンが復旧したら、データのマージ及び競合の解消を行い、手動フェールオーバーにより書き込みリージョンに再度昇格させます。

※書き込みリージョンの障害からのフェールオーバーについては、私自身実際の操作経験がない為、公式ドキュメントからの受け売りですm(_ _)m

8.4.2 手動フェールオーバー

手動フェールオーバーは、Azureポータルもしくはプログラムから行うことができます。
そして、書き込みリージョンの変更(フェールオーバー)処理において、「データ ロス 0」「アベイラビリティ ロス 0」が保証されています。

Azureポータルでの操作手順

Azureポータルを表示し、「データをグローバルにレプリケートする」メニューを選択します。 「手動フェールオーバー」メニューを選択します。

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

書き込みリージョンに変更(昇格)する読み取りリージョンを選択する画面が表示されます。
書き込みリージョンに昇格させる読み取りリージョンを選択してOKをクリックします。

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

フェールオーバー処理にしばらくの時間がかかりますが、以上で手動による「書き込みリージョンのフェールオーバー」が完了です。前述の通り、稼働状態でこの作業を行うことが可能です。
以下が手動フェールオーバーが完了した画面です。選択したWest Europeが書き込みリージョンに昇格しています(私の環境では、ブラウザをリロードしないと表示が反映されませんでした)。

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

Cosmos DB公式サイトにおいて、手動フェールオーバーの利用目的の1つとして以下のようなことが挙げられていました。

Follow the clock model: If your applications have predictable traffic patterns based on the time of the day, you can periodically change the write status to the most active geographic region based on time of the day.

つまり・・・
1日の中で時間帯による書き込みトラフィックのパターンが予測できる場合、時間毎に最もアクティブなリージョンに書き込みリージョンをフェールオーバーする。

個人的には、何かあった時、計画されたアプリケーションアップデートの際、に使用するものかと思っていましたが、上記のような運用上の仕組みとして書き込みリージョンの手動フェールオーバーを行っても良いようです。
実運用適用には、いろいろ試してから適用したいですね・・・。

8.5 まとめ

「Cosmos DB入門」と題して全8回にわたって投稿を行いました。
Cosmos DB、つまりNoSQLは、すべてのリレーショナルデータベースに置き換わるものではありません。
SQL ServerSQL Database(PaaS)は引き続き利用され続けるでしょうし、それは正しいことです。一方、クラウドネイティブなシステムにおいて、Cosmos DBの適用が、よりスケーラブル で よりローレイテンシー を実現する肝となることもあると考えます。
本格的にNoSQLを導入する場合、トランザクションやデータの一貫性といった点だけでも、従来のRDBの感覚とは全く異なる、NoSQLならではのコツが必要になってきます。むしろ多くの苦労を要する事も多々あることでしょう。同時に、それと引き換えに得られるメリットは十分にあるとも考えます。
本連載では、やはり「入門」というレベルに留まった内容であり、本番運用のシステムに適用するにはより多くの知識が必要になると考えています。
まずは、Cosmos DBの概要から設定・操作・コーディング手法の基本を学ぶきっかけになっていただけたならうれしく思います。
私個人としては、今後も本連載以外のトピックや、私自身の知識とノウハウの積み重ねにより更に踏み込んだ面白い投稿をしていけたらと思っています。
ということで、長文投稿ばかりの「Cosmos DB入門」を読んでいただいた皆様、ありがとうございました!

Azure Cosmos DB入門(7)

本コンテンツは「Azure Cosmos DB入門」の(7)です

ryuichi111std.hatenablog.com

7 Cosmos DBプログラミング ~ Table編

今回は マルチデータモデルの最後のモデルである Table編 になります。

7.1 はじめに

以前説明したDocumentDB / MongoDBは、共にデータモデルが「ドキュメント データモデル」でした。
今回の Table は、その名前の通り「テーブル型」のデータモデルとなります。
「テーブル型」といってもRDBのテーブルとは異なりますので注意が必要です。あくまでも NoSQL であり、Key-Valueストアのデータとなります。テーブル間のリレーションや、それらをJOINしての操作などはもちろんできません。

「キーとなる値に対して、値(列値の集合)が紐づいている」データ構造、これがCosmos DBの「テーブル」となります。

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

テーブル内の各データ項目は「エンティティ」と呼ばれ、スキーマレスですので、異なるデータ構造のエンティティを同一テーブルに格納することができます。

※Cosmos DB(Table)自体は2017/6/24時点で、まだパブリックプレビュー状態です。

7.1.1 Cosmos DB(Table) と Azure Table Storage

Azureに詳しい方であれば、このようなテーブル構造は「Azure Table Storage」として提供されていたと気が付くのではないかと思います。
その通りであり、機能概要としては同様のサービスとなります。
ただし、Cosmos DBはパフォーマンスに対するSLA、グローバルディストリビューション可能・・・等の特徴を提供します。
Cosmos DB(Table) と Azure Table Storageの特徴比較は以下のようになります。

  • Cosmos DB(Table)
    スループットの予約RUを設定し、待機時間の非常に短いSLA保証されたパフォーマンス実現する。
    グローバルディストリビューションが可能。
    スループット最適化の価格モデル。
    等々・・・

  • Azure Table Storage
    高速だがCosmos DBのようなパフォーマンス保証されたモデルではない。
    単一リージョン構成のサービスである。
    ストレージ最適化の価格モデル。
    等々・・・

つまり、高速な動作・グローバルディストリビューションなどを求める際には「Cosmos DB(Table)」を利用し、それほど高速なスループットは必要なくコストパフォーマンスの良さを求める場合には「Azure Table Storage」を利用する、といった使い分けが可能です。
また、Cosmos DB(Table)のことを、通称「Premiumテーブル」と呼びます。

7.1.2 アクセス用クラスライブラリ

Cosmos DB(Table) と Azure Table Storageは、同様の機能を提供すると説明しました。
従来、Azure Table Storageを利用する場合、アクセスライブラリとして「WindowsAzure.Storage」を利用していました。
Cosmos DB(Table)に接続するライブラリとしては「WindowsAzure.Storage-PremiumTable」というものがNuGetで提供されています。
WindowsAzure.Storage-PremiumTableで提供されるクラス・メソッドのI/Fは、WindowsAzure.Storageと互換となります。
つまり、参照するライブラリを変更するだけで、接続先のテーブルサービスを切り替えることができます。

7.2 Cosmos DB(Table)

Cosmos DB(Table)におけるいくつかのポイントについて説明しておきます。

7.2.1 データベース構造

Cosmos DB(Table)は、他のデータモデルである「ドキュメント(DocumentDB / MongoDB)・グラフ(Gremlin)」と比較して少し異なる部分があります。
それは Azure Table Storage との相互互換性を維持した仕様の影響です。この点は、特に 良い悪い の話ではないので仕様を理解して利用していけば良い点です。
大きな2点について以下に説明します。

(1) データベースアカウント:データベース=1:1

DocumentDB/MongoDB/Gremlinにおけるデータモデルでは、以下の図に示すようなデータベース構造でした。

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

つまり、「1つのデータベースアカウント」に対して「複数のデータベース」が紐づき、さらに各データベースに対して「複数のコレクション」が紐づきます。
そして、ドキュメントはコレクションに紐づきます。

対して Tableデータモデル では、以下の図のような構造になります。

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

1つのデータベースアカウントには「1つのデータベース」が紐づきます。1つのデータベースに対しては「複数のテーブル」が紐づきます。
1つのデータアイテム(データ項目)は「エンティティ」と呼ばれ、各テーブルに紐づきます。

(2) PartitionKeyは必須

DocumentDB/MongoDB/Gremlinでは「PartitionKey(パーティションキー)」の指定は任意でした(ある程度の規模のシステムを想定した場合には、設定したうえでの運用が現実的です)。
Tableでは PartitionKey の指定が必須になっています。元々 Azure Table Storage において、エンティティをユニークに識別するキーとして「PartitionKey + RowKey」の2つのキーを利用するという仕様に合わせる形になります。
Cosmos DB(Table)においても PartitionKey と RowKey の2つのキーによりテーブル内のエンティティをユニークに識別します。
PartitionKey と RowKey は共に string型 となります。

7.2.2 Cosmos DB固有情報の設定

すでに説明した通り、「Cosmos DB(Table)」と「Azure Table Storage」へのアクセス用クラスライブラリは互換のI/Fが提供されます。
しかし、Cosmos DB(Table)固有の設定機能が存在します。例えば「接続方式(HTTPS or TCP)」であったり、「スループット(RU)の指定」であったり、「一貫性レベルの指定(Consistency Level)」であったりです。
DocumentDB・MongoDB・Gramlin APIモデルにおいては、ロジック上のパラメータ等で設定が可能でした。しかしテーブルAPIモデルでは、構成ファイル(.config)で各種設定を行います。
これは従来のAzure Table Storage用ライブラリとのI/F互換を保つためです。Cosmos DB側ライブラリに固有のプロパティやオプション引数を追加してしまうと、ユーザープロジェクトが参照するライブラリの切り替えのみによる、利用Azureサービスの切り替えができなくなってしまうためです。

構成ファイルで指定可能な設定項目は以下の通りです。

キー 説明
TableConnectionMode 接続モードの指定です。Direct モードと Gateway モードから選択。既定値の Direct の方がパフォーマンス良い。
TableConnectionProtocol 接続プロトコルの指定です。HttpsTcp から選択。Tcpの方がパフォーマンスが良い。
TablePreferredLocations 読み取りリージョンの優先場所をカンマ区切りで指定します。複数リージョンにグローバルレプリカした場合に利用します。
TableConsistencyLevel 一貫性レベルを指定します。Strong / Session / Bounded-Staleness / ConsistentPrefix / Eventual
TableThroughput 予約するスループット(RU)を指定します。テーブル作成時にこの値が適用されます。既定値は 400 です。
TableIndexingPolicy インデックスポリシーを指定します。何も指定しないと、すべての列にインデックスが作成されます。
TableQueryMaxItemCount クエリー時に1回のラウンドトリップで返却される最大項目数を指定します。既定値は -1 でCosmos DBにより自動で最適化されます。
TableQueryEnableScan クエリーがンデックスを使用できない場合、スキャンを使用して実行するかどうかを指定します。 既定値は false です。
TableQueryMaxDegreeOfParallelism クロスパーティションクエリーを行う際の並列処理次数を指定します。0 = プリフェッチなしの直列処理、1 = プリフェッチありの直列処理、2以上 = 指定次数による並列処理。既定値は -1 でCosmos DBにより自動で決定されます。

※ 接続に関しては「TableConnectionMode=Direct / TableConnectionProtoco=Tcp」とする事がパフォーマンスにとって最適です。「ローカル開発実行 & Azure Cosmos DB接続」の際に、Fiddler で通信内容を確認する場合には「TableConnectionMode=Gateway / TableConnectionProtoco=Https」が有効です。送受信されるHTTPから「冗長な呼び出しが行われる実装になっていないか?」「消費RUがどれくらいか?」等の確認が可能です。

7.3 準備

具体的なプログラミングの説明に入るために、まず、AzureへのCosmos DB(Table)の準備と、Visual Studioプロジェクトの準備を行います。

7.3.1 Cosmos DB(Table)データベースアカウントの作成

Azureポータルを利用して「Cosmos DB(Table)」を作成します。
Azureポータルをブラウザで表示し「新規」を選択。検索テキストボックスに「Azure Cosmos DB」と入力します。

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

「Azure Cosmos DB」を選択します。

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

「作成」をクリックします。

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

アカウント情報を入力して「作成」をクリックします。
ここでは以下の設定を行いました。
ID: tablecosmos
API: テーブル(キーと価)
リソースグループ: tablecosmos
場所: 西日本

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

以下のCosmos DBアカウントが作成されます。

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

7.3.2 Visual Studio 2017プロジェクトの作成

Visual Studio 2017を起動し、メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
ここでは、プロジェクトテンプレートは「コンソールアプリケーション」、名前は「CosmosTableExample」としました。

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

次に、Cosmos DB(Table)にアクセスするために、NuGetパッケージライブラリをプロジェクトに追加します。
ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックし、表示されたメニューから「NuGetパッケージの管理」を選択します。

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

表示されたNuGetパッケージ管理画面から、左上の「参照タブ」を選択し、検索テキストボックスに「WindowsAzure.Storage-PremiumTable」と入力します。 一覧に WindowsAzure.Storage-PremiumTable が表示されるので、選択して「インストール」ボタンをクリックします。

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

以上でプロジェクトの準備が整いました。

7.4 テーブルの作成

具体的な「Cosmos DB(Table)」(Premiumテーブル)の操作を行う実装に移ります。
まずテーブルを作成しますが、「Azureポータル上でGUI操作により作成する方法」と「コードで作成する方法」を順に説明します。

7.4.1 Azureポータルでテーブルを作成

Azureポータルからテーブルを作成し、エンティティを追加するまでの操作を説明します。
Azureポータルで対象Cosmos DBアカウントを表示し、「データエクスプローラ」メニューを選択します。
「New Table」をクリックすると右側に「Add Table」ペインが表示されます。
テーブルID、ストレージ容量、スループット、RU/m の指定を行いOKボタンをクリックします。
ここでは「テーブルID=PersonTable、ストレージ容量=10GB、スループット=400、RU/m=OFF」としました。
f:id:daigo-knowlbo:20170624104829p:plain

以下のように、作成されたテーブルを確認することができます。
データベース名は固定で「TablesDB」となります。
テーブルIDは指定した「PersonTable」となりました。

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

続いて「Add Entity」をクリックします。

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

「Add Table Entity」ペインが表示されるので、エンティティの項目名と価を入力します。
PartitionKeyとRowKeyは必須の固定項目です。それ以降の項目値については「Add Property」をクリックすることで任意の数だけ増やすことができます。

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

作成されたエンティティは以下の通りです。

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


1点DateTime型について注意が必要です。
ここではDateTime型の「Bitrth」項目を定義しました。Add Table EntityのUI上では 2000/9/16 00:00:00 を設定しましたが、追加後の一覧画面では 2000/9/14 21:00:00 となっています。しかし、実際のデータ(本当に登録された値)は 2000/9/15 06:00:00 となっています(一覧からエンティティをダブルクリックしてEdit Table Entity画面を表示すると確認可能)。
DateTime型はCosmos DB内部ではGMT時間で管理されます。しかしAzureポータル上では、日本の+9h処理がおかしいようでこのようなズレが発生しています。
設定した2000/9/16 00:00:00(日本時間)に対して、なぜか -18h された2000/9/15 06:00:00がデータとして登録されてしまいます。さらに2000/9/15 06:00:00を一覧画面で表示する際に -9h した2000/9/14 21:00:00が表示されてしまいます。
コードで登録する際はこの問題は発生しません。


もう1度「Add Entity」をクリックして、更にエンティティを追加しましょう。
今度は先程のエンティティで設定した項目に加えて「Email」という項目を増やしました。

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

2つ目のエンティティにのみ Email項目 が追加されているのを確認することができます。
つまり、異なるスキーマのエンティティを同一テーブルに格納することができる「スキーマレス テーブル」ということを確認することができます。

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

次はコードでテーブルを作成する処理に移ります。
その為、一旦 今作成したTablesDBを削除しておきましょう。
TablesDB右側の「…」をクリックするとポップアップメニューが表示されるので、「Delete Database」を選択します。

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

7.4.2 コードでテーブルを作成

プログラムコードでテーブルを作成する手順を説明します。

(1) 構成ファイル(.config)の定義

前述の通り、DocumentDBなどの場合と異なり、接続に関する設定は構成ファイル(.config)で行います。
ここでは以下に示すように、「接続モード(TableConnectionMode)」「接続プロトコル(TableConnectionProtocol)」「スループット(TableThroughput)」を指定しました(リスト1)。
また、TableConnectionStringキーとして接続文字列も定義しています。

リスト1 app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
  </startup>

  <appSettings>
    <add key="TableConnectionString" value="【Azureポータル→接続文字列から取得】" />
    
    <!-- Client options -->
    <add key="TableConnectionMode" value="Direct"/>
    <add key="TableConnectionProtocol" value="Tcp"/>

    <!--Table creation options -->
    <add key="TableThroughput" value="500"/>

  </appSettings>
</configuration>

(2) テーブル作成処理の実装

テーブル作成処理の実装を行います。
まず、「WindowsAzure.Storage-PremiumTable」パッケージで提供されるクラスライブラリにおける、Table操作の大きな流れは以下のようになります。

  1. アカウントオブジェクト(CloudStorageAccount)を作成
  2. テーブルクライアント(CloudTableClient)を作成
  3. テーブルオブジェクト(CloudTable)を作成
  4. 操作オブジェクトを作成
  5. テーブルオブジェクトに操作オブジェクトを渡して実行

テーブルの作成では「1~3+α」の処理となります。具体的な実装は以下のリスト2になります。

リスト2 // テーブル作成のコードスニペット

// app.configから接続文字列を取得
string connectionString = 
  ConfigurationManager.AppSettings["TableConnectionString"];
//  アカウントオブジェクトを取得
CloudStorageAccount cloudStorageAccount = 
  CloudStorageAccount.Parse(connectionString);
// テーブルクライアントオブジェクトを取得
CloudTableClient cloudTableClient = 
  this.cloudStorageAccount.CreateCloudTableClient();
// テーブルオブジェクトを取得
CloudTable table = 
  this.cloudTableClient.GetTableReference("Person");
// 存在しなかったら作成
table.CreateIfNotExists();

今回のプロジェクトでは、テーブルデータを操作する処理を TableManagerクラス にまとめたいと思います。
そこで、プロジェクトに「TableManagerクラス(TableManager.cs)」を追加します。

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

リスト2を踏まえた TableManagerクラス の実装がリスト3です。

リスト3 TableManager.cs

using System.Configuration;

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;

namespace CosmosTableExample
{
  public class TableManager
  {
    private CloudStorageAccount cloudStorageAccount = null;
    private CloudTableClient cloudTableClient = null;

    // コンストラクタ
    public TableManager()
    {
      string connectionString = ConfigurationManager.AppSettings["TableConnectionString"];

      this.cloudStorageAccount = CloudStorageAccount.Parse(connectionString);
      this.cloudTableClient = this.cloudStorageAccount.CreateCloudTableClient();

    }

    // テーブルを作成します
    public bool CreateTable(string tableName)
    {
      bool result = false;

      CloudTable table = this.cloudTableClient.GetTableReference(tableName);
      result = table.CreateIfNotExists();

      return result;
    }
  }
}

CloudStorageAccount / CloudTableClientは今後追加する処理でも使いまわす為にフィールド変数として定義しています。更に、この2つのフィールド変数はコンストラクタで初期化するように実装しています。
テーブルを作成する処理は「CreateTable()メソッド」として実装しました。引数でテーブル名を指定できる形にしています。 テーブルの作成処理には、CloudTable.CreateIfNotExists()メソッドを呼び出していますが、非同期処理版の CreateIfNotExistsAsync()メソッド もライブラリで提供されています。

TableManager.CreateTable()の呼び出しの実装はリスト4となります。

リスト4 Program.cs

var tableManager = new TableManager();
tableManager.CreateTable("PersonTable");

リスト4実行後のAzureポータル画面は以下の通りです。

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

データベース名は固定で「TablesDB」となります。
その下に PersonTable テーブルが追加されているのを確認することができます。
また、app.configの TableThroughput で指定した通り、Througput(RU)は 500 となっています。

7.5 テーブルの操作

作成したテーブル(TablesDB -> PersonTable)に対してエンティティの追加・検索・更新・削除の操作を行っていきます。

7.5.1 エンティティ型の定義

まず、テーブルに追加するエンティティの型を定義します。
エンティティ型は、C#クラスで定義することができます。
ここでは Personクラス をエンティティとして定義します(リスト5)。

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

リスト5 Person.cs
using System;

using Microsoft.WindowsAzure.Storage.Table;

namespace CosmosTableExample
{
  public class Person : TableEntity
  {
    public Person() { }

    public Person(string country, string personId)
    {
      this.PartitionKey = country;
      this.RowKey = personId;
    }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string Email { get; set; }

    public DateTime Birth { get; set; }
  }
}

Tableに保存するエンティティは、「Microsoft.WindowsAzure.Storage.Table.TableEntityクラス」を継承して定義します。
プロパティとして「FirstName / LastName / Email / Birth」を持たせました。
引数付きコンストラクタにおいて以下の処理を行っています。

  • country(国籍)を PartitionKey として設定
  • personId を RowKey として設定

PartitionKey / RowKeyは、プロパティ名を指定して設定するものではなく、TableEntityクラスにおいて定義された PartitionKeyプロパティ / RowKeyプロパティ に対して値そのものに設定します。

7.5.2 エンティティの追加

Personエンティティを PersonTable に追加する実装を行います。
TableManagerクラスに InsertPerson() メソッドを追加します(リスト6)。

リスト6 TableManager.cs

public bool InsertPerson(string tableName, Person person)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);
  // INSERT操作オブジェクトを取得
  TableOperation insertOperation = TableOperation.Insert(person);
  // 実行
  TableResult result = table.Execute(insertOperation);

  return true;
}
  • TableOperationオブジェクト
    Microsoft.WindowsAzure.Storage.Table.TableOperationクラスは、テーブルに対する操作を表すオブジェクトです。
    TableOperationクラスには、各操作に対応したTableOperationオブジェクトを生成するstaticメソッドが用意されています。
    今回はエンティティの追加、つまりINSERT操作を行う為、TableOperation.Insert()メソッドにより「INSERT処理を行うTableOperationオブジェクト」を作成します。

  • CloudTable.Execute()
    TableOperationオブジェクトは「テーブルに対する操作を表すオブジェクト」であり、具体的にCosmos DBサーバーに対して処理を行うわけではありません。
    CloudTableオブジェクトの「Execute()メソッド」を呼び出すことで、Cosmos DBに対する具体的な処理が実行されます。

  • TableResultオブジェクト
    処理結果がTableResultオブジェクトとして返却されます。

TableManager.InsertPerson()の呼び出し元の実装はリスト7となります。

リスト7 Program.cs TableManager.InsertPerson()呼び出し元

TableManager tableManager = new TableManager();

Person person = new Person("Japan", "0000000001")
{
  FirstName = "Ryuichi",
  LastName = "Daigo",
  Birth = new DateTime(2000, 9, 16),
  Email = "daigo@clearboxtechnology.net"
};

tableManager.InsertPerson("PersonTable", person);

また、リスト6の「TableResult result = table.Execute(insertOperation);」が実行された際の、resultの値をウォッチで確認した画面が以下です。

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

処理の結果として作成されたエンティティの Etag、HTTPステータスコード204 を確認することができます。
ETagは、Cosmos DBが各リソース(今回のケースではエンティティ)に対して自動的に割り当てる値です。同時実行制御に利用され、リソース(エンティティ)が更新されるたびに自動で更新されます。
204はHTTPレスポンスコード「204 No Content」に該当します。

追加されたPersonエンティティの内容をAzureポータルで確認したものが以下です。

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

先程も説明しましたが、一覧におけるDateTime表示の問題により、Birthの値の表示がおかしくなっています。
2000/9/16 00:00:00(日本時間)として登録したデータは、Cosmos DB上では 2000/9/15 15:00:00(GMT) として登録されます。
以下はエンティティをダブルクリックして Edit Table Entity 表示で確認した画面です。
f:id:daigo-knowlbo:20170624172939p:plain

RowKeyが未設定の場合

テーブルにエンティティを追加する際、PartitionKeyの設定は必須です。一方 RowKey は未設定でエンティティ作成を行うことが可能です(リスト8)。

リスト8 Program.cs

TableManager tableManager = new TableManager();

Person person = new Person("Japan", "") // RowKeyをから文字列に設定
{
  FirstName = "Ryuichi",
  LastName = "Daigo",
  Birth = new DateTime(2000, 9, 16),
  Email = "daigo@clearboxtechnology.net"
};

tableManager.InsertPerson("PersonTable", person);

上記コードでエンティティ登録を行ったエンティティを、Azureポータルで確認したのが以下の画面です。

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

RowKeyは、Cosmos DBのシステム上では必須項目なので、自動でGUIDが割り当てられました。

7.5.3 エンティティの一括追加

バッチで一括でテーブル操作を行う為の「TableBatchOperationクラス」というものが用意されています。
TableBatchOperationを使用することで、エンティティの一括追加処理を実装することができます。
TableManagerクラスに、複数のPersonを一括INSERTするメソッドInsertPersonBatch()を実装したのがリスト9です。

リスト9 TableManager.cs

public bool InsertPersonBatch(string tableName, List<Person> persons)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);

  // バッチ操作オブジェクトを作成
  TableBatchOperation tableBatchOperation = new TableBatchOperation();

  // INSERT操作オブジェクトを作成
  foreach (Person person in persons)
  {
    TableOperation insertOperation = TableOperation.Insert(person);
    tableBatchOperation.Add(insertOperation);
  }

  // バッチ実行
  IList<TableResult> results = table.ExecuteBatch(tableBatchOperation);

  return true;
}
  • TableBatchOperation と ExecuteBatch
    TableBatchOperationは new 演算子インスタンス化します。
    バッチ処理したい操作、つまりTableOperationオブジェクトを、Add()することができます。
    一度にバッチ化することができるオペレーション数は 100 です。
    また、同一 PartitionKey を持つエンティティに対するバッチ操作のみが許可されています。異なる PartitionKey を持つエンティティの操作をバッチ化することはできません。
    作成したTableBatchOperationオブジェクトをCloudTable.ExecuteBatch()メソッドの引数に設定することで一括操作を行うことができます。

適当なPersonオブジェクトリストを作成して、リスト9のTableManager.InsertPersonBatch()を呼び出す実装が以下のリスト10です。
PartitionKeyとなる Country 毎に100件のPersonオブジェクトを作成して、一括追加処理を行っています。
名前や生年月日はRandomクラスを使用して適当な値になるようにしています。
また、InsertPersonBatch() を呼び出すたびに 1,500ms スリープしています。これは、ソース中のコメントにもあるように、今回のテーブルの予約RUである500RUを超えないように配慮した実装となります。
(あくまでもサンプルなので、実運用コードではSleepしないようにしてください。スレッドがロックされてしまいます・・・)

リスト10 Program.cs

TableManager tableManager = new TableManager();

List<Person> persons = new List<Person>();

string[] country = { "Japan", "UnitedKingdom", "America", "France", "Porland" };
Random random = new Random();
int totalIndex = 0;
for (int i = 0; i < 5; i++)
{
  persons.Clear();

  for (int n = 0; n < 100; n++)
  {
    Person person = new Person(country[i], string.Format("{0:1000000000}", totalIndex))
    {
      FirstName = "FirstTest" + totalIndex.ToString(),
      LastName = "LastTest" + totalIndex.ToString(),
      Birth = new DateTime(random.Next(1960, 2010), random.Next(1, 12), random.Next(1, 28)),
      Email = string.Format("test{0}@test.jp", totalIndex)
    };

    persons.Add(person);
    totalIndex++;
  }

  tableManager.InsertPersonBatch("PersonTable", persons);

  // 予約RUをオーバーするので一旦スリープ
  System.Threading.Thread.Sleep(1500);
}

作成されたエンティティリストは、以下の画面のようになります。

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

7.5.4 エンティティの検索

ここまでの手順を行った場合、PersonTableには500件以上のPersonエンティティが登録された状態となっているはずです。
次にこれらのデータを検索する処理を実装します。

その前にapp.configを少し修正しておきたいと思います。
以下のように設定を変更し、HTTPSで通信が行われるように変更します。

app.config

<!-- Client options -->
<add key="TableConnectionMode" value="Gateway"/>
<add key="TableConnectionProtocol" value="Https"/>

これは、バックエンドにおけるクエリー操作の動きを確認する為です。

(1) PartitionKeyとRowKeyを指定した検索

PartitionKeyとRowKeyを指定した検索を行います。
テーブル内でコレクションをユニークに識別する2項目によるクエリーです。
つまり、単一エンティティの抽出になります。

TableManagerクラスに「FindByPartitionKeyAndRowKey()メソッド」を追加します(リスト11)。

リスト11 TableManager.cs

public async Task<Person> FindByPartitionKeyAndRowKey(
    string tableName, string partitionKey, string rowKey)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);

  // テーブルオペレーションを作成
  TableOperation tableOperation = TableOperation.Retrieve<Person>(partitionKey, rowKey);

  // クエリー実行
  TableResult person = await table.ExecuteAsync(tableOperation);

  if (person.Result != null)
    return person.Result as Person;

  return null;
}
  • TableOperation.Retrieve()
    TableOperation.Retrieve()は、PartitionKeyとRowKeyを指定したクエリーオペレーションオブジェクト(TableOperation)を返却します。

  • CloudTable.ExecuteAsync()
    TableOperationオブジェクトを引数にしてCloudTable.ExecuteAsync()を呼び出すことでクエリーが実行されます。
    戻り値の型は「TableResult」となります。TableResult.Resultプロパティに取得したPersonオブジェクトが格納されています。

TableManager.FindByPartitionKeyAndRowKey()の呼び出しコードは以下の通りです(リスト12)。

リスト12 Program.cs
var person = await this.tableManager.FindByPartitionKeyAndRowKey("PersonTable", "America", "1000000296");

// 結果をコンソール出力
Console.WriteLine("FindByKey(\"PersonTable\", \"America\", \"1000000296\")の結果");
Console.WriteLine(
  string.Format("PartitionKey={0} RowKey={1} FirstNam={2} LastName={3} EMail={4} Birth={5}",
  person.PartitionKey, person.RowKey, person.FirstName, person.LastName, person.Email, person.Birth.ToString("yyyy/MM/dd"))
  );
行われたHTTPS通信

リスト12が実行された際(厳密にはリスト11の「TableResult person = await table.ExecuteAsync(tableOperation);」が実行された際)のHTTPSリクエストとレスポンスをFiddlerで監視したものが以下です。

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

  • URI
    URIでドキュメントのRowKeyを指定しています。

  • HTTP Request Headerの x-ms-documentdb-partitionkey
    x-ms-documentdb-partitionkeyにPartitionKeyを指定しています。
    つまり、リクエストするURI と x-ms-documentdb-partitionkey により取得対象のエンティティを指定しています。

  • HTTP Reqponse Headerの x-ms-request-charge
    処理に要したRUは 1 でした。

  • HTTP Response Body
    ボディに取得されたエンティティがJSON形式で返却されています。

(2) PartitionKeyを指定した検索

PartitionKeyを指定した検索を行います。
RowKeyの指定は行わないので、複数のエンティティが取得対象となります。
単一クエリー条件による検索であり、PartitionKey以外の項目をクエリー条件にする場合も同様の方法をとることができます。

リスト13 TableManager.cs
public async Task<List<Person>> FindByPartitionKey(string tableName, string partitionKey)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);

  // クエリーオブジェクトを作成
  TableQuery<Person> query = new TableQuery<Person>()
    .Where(
    TableQuery.GenerateFilterCondition(
      "PartitionKey", 
      QueryComparisons.Equal, 
      partitionKey)
    );

  // 実行
  var persons = await table.ExecuteQuerySegmentedAsync<Person>(query, null);

  return persons.Results;
}
  • TableQueryオブジェクト
    クエリーを表すTableQueryオブジェクトを作成します。
    今回は抽出条件を追加したいので、Where()メソッドを呼び出します。
    文字列データの比較を行う場合は「TableQuery.GenerateFilterCondition()」メソッドを呼び出します。
    GenerateFilterCondition()は、引数に応じた条件指定文字列をstring型で返却します。
    リスト13の呼び出しでは、単純に「PartitionKey eq ‘【partitionKey変数の値】'」となります。

  • CloudTable.ExecuteQuerySegmentedAsync()
    TableQueryオブジェクトを引数として、ExecuteQuerySegmentedAsync()メソッドを呼び出すことでクエリーが実行されます。

TableManager.FindByPartitionKey()の呼び出しコードは以下の通りです(リスト14)。

リスト14 Program.cs

var persons = await this.tableManager.FindByPartitionKey("PersonTable", "Japan");

// 結果をコンソール出力
Console.WriteLine("FindByPartitionKey(\"PersonTable\", \"Japan\")の結果");
foreach (var person in persons)
{
  Console.WriteLine(
    string.Format("PartitionKey={0} RowKey={1} FirstNam={2} LastName={3} EMail={4} Birth={5}",
    person.PartitionKey, person.RowKey, person.FirstName, person.LastName, person.Email, person.Birth.ToString("yyyy/MM/dd"))
    );
}
行われたHTTPS通信

リスト14が実行された際(厳密にはリスト13の「var persons = await table.ExecuteQuerySegmentedAsync(query, null);」が実行された際)のHTTPSリクエストとレスポンスをFiddlerで監視したものが以下です。

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

  • URI
    URIではPersonTableのドキュメントが対象であることのみ示されています。

  • HTTP Request Headerの x-ms-documentdb-query-enablecrosspartition
    x-ms-documentdb-query-enablecrosspartition が True に設定されています。
    クロスパーティション検索(PartitionKeyの異なるデータの検索)を行う際には、DocumentDBでは明示的なパラメータの指定が必要でした。
    しかし、Tableでは無条件にクロスパーティション検索が有効になるようです。
    今回のようにPartitionKeyを条件に加えている際にもTrueとなります。

  • HTTP Request Body
    リクエストボディに以下のようなクエリー文字列が設定されています。
    {“query”:“select * from entity where (entity[‘$pk’] = \"Japan\”)“}

  • HTTP Reqponse Headerの x-ms-item-count
    検索結果が 102 件であったことが示されています。

  • HTTP Reqponse Headerの x-ms-request-charge
    処理に要したRUは 35.08 でした。
    102件のデータを抽出したのでこれくらいの値になったようです。

  • HTTP Response Body
    ボディに取得されたエンティティがJSON形式で返却されています。

(3) 複数の条件による検索(DateTime含む)

最後に、複数条件による検索を行います。
PartitionKeyであるCountry と Birth をクエリー条件とします。
TableManagerクラスにFindByCountryAndBirth()メソッドを追加します(リスト15)。 country(PartitionKey)はイコール演算子の抽出条件とし、birthに関しては指定した年月日以降の値のエンティティを抽出対象とします。

リスト15 TableManager.cs

public async Task<List<Person>> FindByCountryAndBirth(string tableName, string country, DateTime birth)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);

  // 日付を20桁のTicksに変換
  string sBirth = birth.Ticks.ToString("00000000000000000000");
  
  // テーブルオペレーションを作成
  TableQuery<Person> query = new TableQuery<Person>()
    .Where(
    TableQuery.CombineFilters(
      TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, country),
      TableOperators.And,
      TableQuery.GenerateFilterCondition("Birth", QueryComparisons.GreaterThanOrEqual, sBirth)
      //TableQuery.GenerateFilterConditionForDate("Birth", QueryComparisons.GreaterThanOrEqual, new DateTimeOffset(birth))
      ));
  
  var persons = await table.ExecuteQuerySegmentedAsync<Person>(query, null);

  return persons.Results;
}
  • TableQueryオブジェクト
    先程と同様にTableQueryオブジェクトを作成します。
    今回は抽出条件が複数であるため TableQuery.CombineFilters() メソッドを利用します。このメソッドは、2つの条件式を任意のオペレータ(ANDやORなど)でつなぎ合わせます。
    CombineFiltersオブジェクトをネストさせて、より複雑な条件を指定することも可能です。

  • Birth(DateTime型)の条件指定
    DateTime(日付)のクエリー条件を指定するために「TableQuery.GenerateFilterConditionForDate()」を呼び出したいところですが、これを使うと実行時エラーが発生してしまいます。
    OData.Edmの日付に関する書式が不正になるようなのですが、何故そうなってしまうのかは不明でした・・・
    解決策として、DateTime.Tockの値を20桁の文字列にして Birth と条件比較するとうまく動作することが確認できました。

TableManager.FindByCountryAndBirth()の呼び出しコードは以下の通りです(リスト16)。

リスト16 Program.cs

var persons = await this.tableManager.FindByCountryAndBirth("PersonTable", "France", new DateTime(1990,1,1,0,0,0,DateTimeKind.Utc));

// 結果をコンソール出力
Console.WriteLine("FindByCountryAndBirth(\"PersonTable\", \"France\", new DateTime(1990,1,1))の結果");
foreach (var person in persons)
{
  Console.WriteLine(
    string.Format("PartitionKey={0} RowKey={1} FirstNam={2} LastName={3} EMail={4} Birth={5}",
    person.PartitionKey, person.RowKey, person.FirstName, person.LastName, person.Email, person.Birth.ToString("yyyy/MM/dd"))
    );
}
行われたHTTPS通信

リスト16が実行された際(厳密にはリスト15の「var persons = await table.ExecuteQuerySegmentedAsync(query, null);」が実行された際)のHTTPSリクエストとレスポンスをFiddlerで監視したものが以下です。

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

内容的には「(2) PartitionKeyを指定した検索 -> 行われたHTTPS通信」と同様なので割愛いたします。

7.5.5 エンティティの更新

既存のエンティティを更新する処理を実装します。
TableManagerクラスにReplacePerson()メソッドを追加します(リスト17)。

リスト17 TableManager.cs
public bool ReplatePerson(string tableName, Person person)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);
  // REPLACE操作オブジェクトを取得
  TableOperation replaceOperation = TableOperation.Replace(person);
  // 実行
  TableResult result = table.Execute(replaceOperation);

  return true;
}

エンティティの更新は、まず既存エンティティを取得し、値を変更の上で更新処理を実行します。
TableManager.ReplacePerson()メソッドの呼び出し例はリスト18となります。

リスト18 Program.cs

// エンティティを取得
Person person = 
  await tableManager.FindByPartitionKeyAndRowKey(
     "PersonTable", 
     "Japan", 
     "0000000001");

// エンティティ値を変更
person.LastName = "Modify!!!";

// エンティティを更新
bool result = tableManager.ReplatePerson("PersonTable", person);

また、"エンティティを取得して"から"更新するまでの間"に、別のプロセス等によりエンティティが更新された場合、更新処理は失敗します。
エンティティの変更が行われると、Cosmos DB内部でエンティティに設定されたETag値の更新が行われます。取得した際のETag値 と 更新する際のETag値 は、同時実行制御として比較されたうえで更新処理が行われます。
非現実的な実装ですが、リスト19のように明示的にETag値を変更すると、同様のエラーを引き起こすことができます。

リスト19 Program.cs

// エンティティを取得
Person person = 
  await tableManager.FindByPartitionKeyAndRowKey(
     "PersonTable", 
     "Japan", 
     "0000000001");

// エンティティ値を変更
person.LastName = "Modify!!!";
person.ETag = Guid.NewGuid().ToString(); // わざとエラーを発生させる

// エンティティを更新
bool result = tableManager.ReplatePerson("PersonTable", person);

【補足】TableOperation.InsertOrReplace()

上記サンプルではTableOperationオブジェクトの作成に「TableOperation.Replace()」を使用しました。
TableOperation.InsertOrReplace()を使用すると、既存エンティティが存在する場合は更新、存在しない場合は新規エンティティ作成を行うオペレーションオブジェクトが作成されます。

7.5.6 エンティティの削除

既存のエンティティを削除する処理を実装します。 TableManagerクラスにDeletePerson()メソッドを追加します(リスト20)。

リスト20 TableManager.cs
public bool DeletePerson(string tableName, Person person)
{
  // テーブルオブジェクトを取得
  CloudTable table = this.cloudTableClient.GetTableReference(tableName);
  // DELETE操作オブジェクトを取得
  TableOperation deleteOperation = TableOperation.Delete(person);
  // 実行
  TableResult result = table.Execute(deleteOperation);

  return true;
}

エンティティの削除は、まず既存エンティティを取得し、値を変更の上で更新処理を実行します。
TableManager.DeletePerson()メソッドの呼び出し例はリスト21となります。

リスト21 Program.cs

// エンティティを取得
Person person = 
  await tableManager.FindByPartitionKeyAndRowKey(
    "PersonTable", 
    "Japan", 
    "0000000001");

// エンティティを削除
bool result = tableManager.DeletePerson("PersonTable", person);

7.7 まとめ

Csomos DB(テーブル)APIが「Azure Table Storage」APIと同一のクラスメソッドI/Fを持っている事は非常に魅力的です。
これにより、共通のプログラムを、用途・コストに合わせて展開することも可能になります。
本投稿ではテーブルの代表的な操作方法のみの説明になりましたが、Cosmos DB(Table)を利用する上での1つのきっかけとなればと思っております。

7.8 資料

本記事のサンプルは以下からダウンロード可能です。

github.com

<< Azure Cosmos DB入門(6)へ | Azure Cosmos DB入門(8)へ >>

Azure Cosmos DB入門(6)

本コンテンツは「Azure Cosmos DB入門」の(6)です

ryuichi111std.hatenablog.com

6 Cosmos DBプログラミング ~ Gremlin編

さて、今回は Gremlin編 になります。

6.1 はじめに

以前説明したDocumentDB / MongoDBは、共にデータモデルが「ドキュメント データモデル」でした。
今回のGremlinは、がらりと変わり「グラフ データモデル」になります。

6.1.1 グラフ データモデルとは

グラフデータモデルとは「facebookの友達」「twitterのフォロー」「駅の路線図の繋がり」のような繋がりのデータ構造を指します。
グラフデータモデルを扱うデータベースを、グラフデータベースと呼びます。
例えば、facebookの友達・フォローの関係をグラフデータモデルで表すと以下のようになります。

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

グラフで最も重要な要素として、以下の2つの概念があります。

  • Vertex
    直訳で「頂点」ですね。上の例の図では、「人」「企業」の丸い要素(ノード)が該当します。
    Vertexには「ラベル」を付けることができます。例では「人」「企業」がラベルに該当します。ラベルは、Vertexの種類を表します。
    また、Vertexには任意の数の「プロパティ」を関連付けることができます。例では「名前・生年月日・性別」がプロパティに該当します。

  • Edge
    Edgeは「VertexとVertexを結ぶ線(辺)」を表します。上の例の図では 矢印線 が該当します。
    Edgeにも「ラベル」を付けることができます。例では「友達」「フォロー」「いいね」に該当します。ラベルは、Edgeの種類を表します。
    また、Edgeには任意の数の「プロパティ」を関連付けることができます。例では「いつから?・関係」がプロパティに該当します。

このようなデータ構造はRDBでも表現は可能です。しかし、データ構造上の概念の異なるテーブル形式に保存するため、分かりにくい もしくは 複雑な データ構造として保存されることになります。
また、グラフデータベースのトラバーサル言語であるGremlinを使用すると、以下のような検索を簡単に行うことができます。

  • 高橋さんの友達の友達はだれ?(→下田さん、佐藤さん、上田さん、神山さん)
  • 高橋さんの友達をフォローしている人はだれ?(→木村さん、川田さん)

つまり、「複数の要素のグラフ上の関係性の保持」および「それらの要素の関係性の検索」を容易に行うことができるものが グラフデータベースです。

6.1.2 Gremlinとは

グラフデータベースについて学ぼうとすると「TinkerPop」という言葉が出てきます。
TinkerPopは、現在、Apache Software Foundationのトッププロジェクトであり、「グラフコンピューティング フレームワーク」です。TinkerPopという抽象化層の上に各種ベンダーがグラフデータベースを構築しています。
Cosmos DBも同様にTinkerPopの上に実装されたグラフデータベースという位置づけになります。
また、Cosmos DB以外にも以下のようなグラフデータベースが存在します。

  • Neo4j
  • DESGraph
  • IBM Graph
  • Titan
  • …等々多数

そして、グラフデータベースの利用者(利用アプリケーション)は、「Gremlin traversal language」によって、グラフデータを操作します。
「Gremlin traversal language」は、RDBでいうところの「SQL」に該当します。つまり、グラフデータベースに対して、データを検索する・追加する・更新する、といった操作を行うための言語です。

TinkerPopは元々(Ver.2.xまで)は、以下のようなコンポーネントの組み合わせで構成されていました。

  • Rexster(RESTインターフェイス
  • Furnace(グラフアルゴリズム) / Frames(オブジェクトグラフマッパー)
  • Gremlin(グラフ トラバーサル言語)
  • Pipes(データフロー)
  • BluePrints(プロパティグラフモデル)

Ver.3.xからはこれらが統合され、全体で「Gremlin」と呼ばれるようになっています。
この辺りのお話は、TinkerPopやGremlinに関する少し複雑な部分に入ってしまい、「Azure Cosmos DB入門」と題した本投稿から逸脱(?)しそうなので、詳細は以下の公式ページを参照してください。

tinkerpop.apache.org

6.2 準備

では、具体的なCosmos DB(Gremlin)開発のお話に入るために、Azure上へのCosmos DBの準備と、Visual Studioプロジェクトの準備を行います。

6.2.1 Cosmos DB(Gremlin)データベースアカウントの作成

Azureポータルを利用して「Cosmos DB(Gremlin(グラフ))」を作成します。
Azureポータルをブラウザで表示し「新規」を選択。検索テキストボックスに「Azure Cosmos DB」と入力します。

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

「Azure Cosmos DB」を選択します。

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

「作成」をクリックします。

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

アカウント情報を入力して「作成」をクリックします。
ここでは以下の設定を行いました。
ID: gremlincosmos
API: Gremlin(グラフ)
リソースグループ: gremlincosmos
場所: 西日本

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

以下のCosmos DBアカウントが作成されます。

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

6.2.2 Visual Studio 2017プロジェクトの作成

Visual Studio 2017を起動し、メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
ここでは、プロジェクトテンプレートは「コンソールアプリケーション」、名前は「CosmosGremlinExample」としました。

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

次に、Cosmos DB(Gremlin)にアクセスするために、NuGetパッケージライブラリをプロジェクトに追加します。
ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックし、表示されたメニューから「NuGetパッケージの管理を選択します。

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

表示されたNuGetパッケージ管理画面から、左上の「参照タブ」を選択し、検索テキストボックスに「Microsoft.Azure.Graphs」と入力します。 一覧に Microsoft.Azure.Graphs が表示されるので、選択して「インストール」ボタンをクリックします。

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

Microsoft.Azure.Graphs」と、その依存関係である「Microsoft.Azure.DocumentDB」がインストールされます。

6.3 データベースとコレクションの作成

今回のCosmos DB(Gremlin)においても、DocumentDBの場合と同様に「データベースアカウント → データベース → コレクション → ドキュメント(グラフデータ)」というデータモデル構造となります。
まず、データベースとコレクションを作成したいと思います。
作成方法は「Azureポータルで作成」「プログラムで作成」などがあります。
「Azureポータルで作成」「プログラムで作成」の2つの方法は共に、DocumentDBの時とまったく同じ方法となります。

6.3.1 プログラムで作成

本稿では、Gremlin(グラフ)操作を管理するクラスとして GremlinManagerクラス を用意することとします。
プロジェクトに GremlinManagercs を追加します。

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

(1)接続情報の定義

「接続情報」及びこれから「作成するデータベース名・コレクション名」等をGremlinManagerクラスに定数として定義します(リスト 1)。

リスト1 GremlinManager.cs
private const string EndPoint = "【URI】";
private const string AuthKey = "【認証キー】";
private const string DatabaseId = "BookStoreDb";
private const string CollectionId = "BookStoreCollection";

URI】【認証キー】は各データベースアカウントごとに適切な値を設定します。
Azureポータルで「キー」タブを選択することで確認することができます。

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

データベースIDは「BookStoreDb」、コレクションIDは「BookStoreCollection」としました。これはサンプルとして、オンライン書店のデータベースを想定します。

(2)基本名前空間のusing定義

Gremlin接続・操作を行う上での基本的な名前空間をusing定義しておきます(リスト 2)。

リスト2 GremlinManager.cs

// 一般的に利用する名前空間
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

// Cosmos DB(Gremlin)関連の名前空間
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;
using Microsoft.Azure.Graphs;
using System.Text;

(3)接続クライアントオブジェクトの定義

CosmosDB(Gremlin)に接続する為の接続クライアント「Microsoft.Azure.Documents.Client.DocumentClient」をGremlinbManagerクラスのフィールド変数「client」として定義します(リスト 3)。
clientはコンストラクタで初期化することとします。

リスト3 GremlinManager.cs

private DocumentClient client = null;
...

public GremlinManager()
{
  this.client = new DocumentClient(
    new Uri(GremlinManager.EndPoint),
    GremlinManager.AuthKey,
    new ConnectionPolicy
    {
      ConnectionMode = ConnectionMode.Direct,
      ConnectionProtocol = Protocol.Tcp
    });
}

(4)データベースとコレクション作成メソッドの追加

GremlinManagerクラスにデータベースを作成するメソッド「CreateDatabase()」、コレクションを作成するメソッド「CreateCollection()」を定義します(リスト 4)。
また、DocumentCollectionオブジェクトは、後々の使い勝手を考慮し、クラスメンバフィールドに保持することとします。

リスト4 GremlinManager.cs

private DocumentCollection collection = null;
...

public async Task<Database> CreateDatabase()
{
  // データベースを作成
  Database database =
    await this.client.CreateDatabaseIfNotExistsAsync(
      new Database { Id = GremlinManager.DatabaseId });

  return database;
}

public async Task<DocumentCollection> CreateCollection()
{
  // パーティションキー指定はなし
  DocumentCollection collection = new DocumentCollection();
  collection.Id = GremlinManager.CollectionId;

  // スループットは 400RU
  RequestOptions options = new RequestOptions();
  options.OfferThroughput = 400;

  // コレクションを作成
  this.collection =
    await this.client.CreateDocumentCollectionIfNotExistsAsync(
      UriFactory.CreateDatabaseUri(GremlinManager.DatabaseId),
      collection, options);

  return collection;
}

以下が GremlinManager.CreateDatabase() / .CreateCollection() 呼び出しコードです(リスト5)。

リスト5 Program.cs

private GremlinManager manager = new GremlinManager();
...
var database = await manager.CreateDatabase();
var collection = await manager.CreateCollection();

上記コードによるデータベース・コレクションの作成を行った結果をAzureポータルで確認したものが以下です。
パーティションキーなし、予約RUは400での作成となります。

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

6.4 グラフデータの操作

BookStoreDb - BookStoreCollection にグラフデータを追加していきます。
このデータベースはオンライン書店を想定します。

以下のようなグラフデータを作成することとします。

【サンプルグラフデータ】 f:id:daigo-knowlbo:20170619024530p:plain

  • Customer Vertex
    Customer Vertexは顧客を表します。
    ラベルは「Customer」、それ以外には「id」のみを持ちます。

  • Book Vertex
    Book Vertexは書籍を表します。
    ラベルは「Book」、「id」の他に「title」「author」のプロパティを持ちます。
    idは書籍の ISBN番号 を使用しています。

  • order Edge
    order Edgeは、Customer(顧客)が購入したBook(書籍)を表します。

6.4.1 Vertexの追加

Vertexの追加は Gremlin では以下のような構文で行います。

ラベルのみ指定してVertexを作成
g.addV('【ラベル】')

ラベルとIDを指定してVertexを作成
g.addV('【ラベル】').property('id', '【ID】')

GremlinManagerクラスに、上記 Gremlin構文 を実行する「AddVertex()」メソッドを追加します(リスト6)。

リスト6 GremlinManager.cs

// ラベルとidを指定してVertexを追加
public async Task<bool> AddVertex(string label, string id)
{
  string gr = 
    string.Format("g.addV('{0}').property('id', '{1}')", label, id);

  var ret = 
    await this.client.CreateGremlinQuery<dynamic>(
      this.collection,gr)
    .ExecuteNextAsync();

  return true;
}
  • CreateGremlinQuery()メソッド
    DocumentClient.CreateGremlinQuery()メソッドを呼び出すことでGremlinクエリーオブジェクトを作成することができます。
    CreateGremlinQuery()メソッドは、DocumentClientクラスの拡張メソッドで、Microsoft.Azure.Graphsで実装されています。

また、Bookは id 以外のプロパティ(title / author)を追加します。
リスト7に示すような「SetProperty()」、もう1つの「AddVertex()」オーバーロードメソッドをGremlinManagerクラスに追加しました。

リスト7 GremlinManager.cs

// idを持つVertexにプロパティを追加します
public async Task<bool> SetProperties(string id, Dictionary<string, string> properties)
{
  StringBuilder grSb = new StringBuilder(
    string.Format("g.V('{0}')", id));
  foreach (string key in properties.Keys)
  {
    grSb.Append(string.Format(".property('{0}', '{1}')", key, properties[key]));
  }

  var ret =
    await this.client.CreateGremlinQuery<dynamic>(
      this.collection, grSb.ToString())
    .ExecuteNextAsync();

  return true;
}

// Vertexの作成とプロパティ設定同時に行います
public async Task<bool> AddVertex(string label, string id, Dictionary<string, string> properties)
{
  StringBuilder grSb = new StringBuilder(
    string.Format("g.addV('{0}').property('id', '{1}')", label, id));
  foreach (string key in properties.Keys)
  {
    grSb.Append(string.Format(".addProperty('{0}', '{1}')", key, properties[key]));
  }

  var ret =
    await this.client.CreateGremlinQuery<dynamic>(
      this.collection, grSb.ToString())
    .ExecuteNextAsync();

  return true;
}

GremlinManager.AddVertex() / SetProperties() を呼び出して、4つのCustomer Vertex と 5つのBook Vertex を作成する処理はリスト8の通りです。

リスト8 Program.cs

GremlinManager manager = new GremlinManager();

// add Customer Vertex
var cv1 = await manager.AddVertex("Customer", "daigo");
var cv2 = await manager.AddVertex("Customer", "tanaka");
var cv3 = await manager.AddVertex("Customer", "kido");
var cv4 = await manager.AddVertex("Customer", "sakai");

// add Book Vertex
// AddVertex() + SetProperties()呼び出し
var bv1 = await manager.AddVertex("Book", "978-4101339115");
var prop1 = new Dictionary<string, string>();
prop1.Add("title", "きらきらひかる");
prop1.Add("author", "江國香織");
var bp1 = await manager.SetProperties("978-4101339115", prop1);

// もう1つのAddVertex()オーバーロード呼び出し
var prop2 = new Dictionary<string, string>();
prop2.Add("title", "流しのしたの骨");
prop2.Add("author", "江國香織");
var bv2 = await manager.AddVertex("Book", "978-4101339153", prop2);

var prop3 = new Dictionary<string, string>();
prop3.Add("title", "キッチン");
prop3.Add("author", "吉本ばなな");
var bv3 = await manager.AddVertex("Book", "978-4041800089", prop3);

var prop4 = new Dictionary<string, string>();
prop4.Add("title", "月に吠える");
prop4.Add("author", "萩原朔太郎");
var bv4 = await manager.AddVertex("Book", "978-4903620510", prop4);

var prop5 = new Dictionary<string, string>();
prop5.Add("title", "抱擁、あるいはライスには塩を");
prop5.Add("author", "江國香織");
var bv5 = await manager.AddVertex("Book", "978-4087713664", prop5);

6.4.2 Edgeの追加

続いて Customer / Book をつなげる Edge を追加します。

まず、Gremlinにおける Edge追加構文は以下の通りです。

【FROMのVertex】.addE('【ラベル】').to(【ToのVertex】)

例)g.V('daigo').addE('order').to(g.V('978-4101339115'))

GremlinManagerクラス にEdgeを作成する「AddEdge()」メソッドを追加します(リスト9)。

リスト9 GremlinManager.cs

public async Task<bool> AddEdge(string label, string fromId, string toId)
{
  string gr = string.Format(
    "g.V('{0}').addE('{1}').to(g.V('{2}'))", 
    fromId, label, toId);

  var ret =
    await this.client.CreateGremlinQuery<dynamic>(
      this.collection, gr)
    .ExecuteNextAsync();

  return true;
}

GremlinManager.AddEdge()を呼び出してサンプルとなる Customer / Book Vertexに対するEdgeを追加する処理を実装します(リスト10)。

リスト10 Program.cs

// add order Edge
// daigo -> きらきらひかる
var e1 = await manager.AddEdge("order", "daigo", "978-4101339115");
// tanaka -> きらきらひかる
var e2 = await manager.AddEdge("order", "tanaka", "978-4101339115");
// tanaka -> 月に吠える
var e3 = await manager.AddEdge("order", "tanaka", "978-4903620510");
// sasaki -> キッチン
var e4 = await manager.AddEdge("order", "sasaki", "978-4041800089");
// sasaki -> 流しのしたの骨
var e5 = await manager.AddEdge("order", "sasaki", "978-4101339153");
// kido -> きらきらひかる
var e6 = await manager.AddEdge("order", "kido", "978-4101339115");
// kido -> 抱擁、あるいはライスには塩を
var e7 = await manager.AddEdge("order", "kido", "978-4087713664");
// kido -> 流しのしたの骨
var e8 = await manager.AddEdge("order", "kido", "978-4101339153");

以上で【サンプルグラフデータ】の作成が完了しました。

6.4.3 Vertexの検索

作成したグラフに対する検索を行います。

(1) すべてのVertexを一覧

まずは すべてのVertex を一覧してみます。

Gremlin構文は以下となります。

g.V()

GremlinManagerクラスにGetAllVertex()メソッドを追加します(リスト11)。

リスト11 GremlinManager.cs

public async Task<List<dynamic>> GetAllVertex()
{
  List<dynamic> result = new List<dynamic>();

  var query =
    this.client.CreateGremlinQuery<dynamic>(
      this.collection, "g.V()");

  if(query.HasMoreResults)
  {
    foreach (dynamic item in await query.ExecuteNextAsync())
    {
      result.Add(item);
    }
  }

  return result;
}

GremlinManager.GetAllVertex()の呼び出しと実行結果は以下の通りです(リスト12)。

リスト12 Program.cs
GremlinManager manager = new GremlinManager();

Console.WriteLine("");
Console.WriteLine("-start- すべてのVertexをリストします");

var ret = await manager.GetAllVertex();
foreach (var vertex in ret)
{
  Console.WriteLine("---");
  Console.WriteLine("id: " + vertex.id);

  string label = vertex.label;
  if (label == "Book")
  {
    Console.WriteLine("title: " + vertex.properties.title[0].value);
    Console.WriteLine("author: " + vertex.properties.author[0].value);
  }
}

Console.WriteLine("-end-");
リスト12の実行結果
-start- すべてのVertexをリストします
---
id: daigo
---
id: tanaka
---
id: kido
---
id: sakai
---
id: 978-4101339115
title: きらきらひかる
author: 江國香織
---
id: 978-4101339153
title: 流しのしたの骨
author: 江國香織
---
id: 978-4041800089
title: キッチン
author: 吉本ばなな
---
id: 978-4903620510
title: 月に吠える
author: 萩原朔太郎
---
id: 978-4087713664
title: 抱擁、あるいはライスには塩を
author: 江國香織
-end-

(2) idでVertexを検索

特定のidを持つVertexを検索します。
Gremlin構文は以下の通りです。

g.V('【id】')

GremlinManagerクラスにGetVertexById()メソッドを追加します(リスト13)。

リスト13 GremlinManager.cs

public async Task<dynamic> GetVertexById(string id)
{
  dynamic result = null;

  string gr = string.Format("g.V('{0}')", id);

  var query =
    this.client.CreateGremlinQuery<dynamic>(
      this.collection, gr);

  if (query.HasMoreResults)
  {
    foreach (dynamic item in await query.ExecuteNextAsync())
    {
      result = item;
    }
  }

  return result;
}

GremlinManager.GetVertexById()の呼び出し及び実行結果は以下の通りです(リスト14)。

リスト14 Program.cs

GremlinManager manager = new GremlinManager();


dynamic vertex = await this.manager.GetVertexById("978-4087713664");

Console.WriteLine("");
Console.WriteLine("-start- 978-4087713664のVertexを検索します");

Console.WriteLine("id: " + vertex.id);
Console.WriteLine("title: " + vertex.properties.title[0].value);
Console.WriteLine("author: " + vertex.properties.author[0].value);

Console.WriteLine("-end-");

6.4.4 Edgeの検索

すべてのVertex を一覧してみます。

Gremlin構文は以下となります。

g.E()

GremlinManagerクラスにGetAllEdge()メソッドを追加します(リスト15)。

リスト15 GremlinManager.cs

public async Task<List<dynamic>> GetAllEdge()
{
  List<dynamic> result = new List<dynamic>();

  var query =
    this.client.CreateGremlinQuery<dynamic>(
      this.collection, "g.E()");

  if (query.HasMoreResults)
  {
    foreach (dynamic item in await query.ExecuteNextAsync())
    {
      result.Add(item);
    }
  }

  return result;
}

上記コードから分かるように、Vertex一覧と同じ要領です。

6.4.5 Vertex~Edge~Vertexを辿る(1)

次に少しグラフデータベースらいしい検索を行ってみます。

  • daigo が購入した Book を同様に購入した Customer を取得します
    (つまり嗜好の同じCustomerを抽出します)

GremlinManager.GetSameOrderCustomers()メソッドを追加します(リスト16)。

リスト16 GremlinManager.cs

public async Task<List<dynamic>> GetSameOrderCustomers(string id)
{
  List<dynamic> result = new List<dynamic>();

  string gr = string.Format(
    "g.V('{0}').as('self').outE('order').inV().inE().outV().where(neq('self'))",
    id);
  // 上記の省略形は以下です。
  //string gr = string.Format(
  //  "g.V('{0}').as('self').out('order').in().where(neq('self'))",
  //  id);

  var query =
    this.client.CreateGremlinQuery<dynamic>(
      this.collection, gr);

  if(query.HasMoreResults)
  {
    foreach (dynamic item in await query.ExecuteNextAsync())
    {
      result.Add(item);
    }
  }

  return result;
}

GetSameOrderCustomers()で実行されるGremlin構文は以下になります。

g.V('daigo').as('self').outE('order').inV().inE().outV().where(neq('self'))

ごちゃごちゃしていますが、先頭から順番に「.(ドット)」区切りで見ていくと理解することができます。
また、以下の説明の下に、処理に該当する番号を振った図を示します。

  • 1 “g.V(‘daigo’)”
    Vertexの中から id=daigo のものを取得します。

  • 2 “as(‘self’)”
    as()はエイリアスになります。
    selfには任意の文字列を指定することができます。 以降の構文中で self をして Vertex(“daigo”) を指すことができます。

  • 3 “outE(‘order’)”
    対象のVertexから出力しているEdge、さらにラベルが'order'のものを取得します。
    ここでは1つの order Edge が該当します。

  • 4 “inV()”
    対象のEdgeから入力しているVertexを取得します。
    ここでは1つの Book Vertex が該当します。

  • 5 “inE()”
    対象のVertexに入力しているEdgeを取得します。
    ここでは3つの Order Edge が該当します。

  • 6 “outV()”
    対象のEdgeに出力しているVertexを取得します。
    ここでは3つの Customer Vertex が該当します。

  • 7 “where(neq(‘self’))”
    whereはSQLなどと同じく抽出条件を指定します。
    neq()は not equal の意味を持ちます。
    selfは as() により指定した V(‘daigo’) を指します。
    つまり、このGremlin構文においては「自分自身は除外する」という意味を持ちます。 最終的に2つのCustomer Vertexが該当します。

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

GremlinManager.GetSameOrderCustomers()の呼び出しと実行結果は以下の通りです(リスト17)。

リスト17 Program.cs

Console.WriteLine("");
Console.WriteLine("-start- daigoと同じ書籍を購入したCustomerをリストします");

var ret = await manager.GetSameOrderCustomers("daigo");
foreach (var vertex in ret)
{
  Console.WriteLine(vertex.id);
}

Console.WriteLine("-end-");
リスト17の実行結果

-start- daigoと同じ書籍を購入したCustomerをリストします
tanaka
kido
-end-

6.4.6 Vertex~Edge~Vertexを辿る(2)

最後に以下のクエリーを行います。

  • daigo が購入した書籍と同じものを購入したCustomerが購入した別の本、つまりdaigoへのリコメンド書籍を取得します。

リスト18がGremlinManager.GetRecomendBooks()の実装になります。

リスト 18 GremlinManager.cs

public async Task<List<dynamic>> GetRecomendBooks(string id)
{
  List<dynamic> result = new List<dynamic>();

  string gr = string.Format(
    "g.V('{0}').as('self').outE('order').inV().as('sourceBook').inE().outV().where(neq('self')).outE('order').inV().where(neq('sourceBook'))",
    id);

  var query =
    this.client.CreateGremlinQuery<dynamic>(
      this.collection, gr);

  if (query.HasMoreResults)
  {
    foreach (dynamic item in await query.ExecuteNextAsync())
    {
      result.Add(item);
    }
  }

  return result;
}

GremlinManager.GetRecomendBooks()の呼び出しはリスト19になります。

リスト19 Program.cs

GremlinManager manager = new GremlinManager();

Console.WriteLine("");
Console.WriteLine("-start- daigoにおすすめの書籍をリストします");

var ret = await manager.GetRecomendBooks("daigo");
foreach (var vertex in ret)
{
  Console.WriteLine("id: " + vertex.id);
  Console.WriteLine("title: " + vertex.properties.title[0].value);
  Console.WriteLine("author: " + vertex.properties.author[0].value);
}

Console.WriteLine("-end-");

6.5 まとめ

Cosmos DBのGremlin(グラフ)についての説明を行いました。
Gremlin言語については、ほんの一部の機能のみの説明にとどめましたが、SQLと同様に多数の構文が存在するため、以下の公式ドキュメントから確認していただくのが良いと思います。

TinkerPop3 Documentation

また、本説明ではシングルパーティションコレクションを作成・使用しましたが、パーティショニングに関する考え方はDocumentDB / MongoDBの時と同様です。

では、次回は「Table API(データモデル)」の説明になります。

6.6 資料

本記事のサンプルは以下からダウンロード可能です。

github.com

<< Azure Cosmos DB入門(5)へ | Azure Cosmos DB入門(7)へ >>

Azure SQL Databaseで、Elastic PoolにGEOレプリケーションする時にハマった話

はじめに

Azure SQL Databaseは単独リージョンへの展開のみで 99.99% の可用性をSLAで保証しています。(SQL Database の SLA

とはいえ「2017年3月 のAzure障害祭り」のようなことが発生することも事実です。
(私個人としては、”Azure(やクラウド)批判派”ではなく”Azure(クラウド)推進派”であり、障害は起こる前提を想定すべき、との立場です)

単独で 99.99% のアベイラビリティに加えて「GEOレプリケーション」を組んでおく事が、さらなる安心、さらなるアベイラビリティの向上につながります。

という事で(仕事上のこともあり)、Azure SQL DatabaseのGEOレプリケーションについて調べる中で、少しハマった事がありましたので、ここにメモしておきます。

行おうとした事は以下です。

  • 複数のデータベースを東日本リージョンに作成する
  • 複数のデータベースは、1つのエラスティック データベース プールを利用する
  • 複数のデータベースは、それぞれ西日本リージョンにGEOレプリケーションする
  • 西日本リージョンのGEOレプリケーション先は、マスターより eDTU値 を落としたエラスティック データベース プールとする

1. マスターのSQL Databaseを作成

Azureポータルを表示します。

1.1. 1つ目のSQL Databaseを作成

「新規作成 → SQL Databaseを検索」します。

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

SQL Database」を作成します。

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

作成するSQL Databaseは以下の設定とします。

データベース名: RdExampleSQL1
リソースグループ: RdExampleSQL
サーバー: サーバー名 rdsqlsrv1 / 場所 東日本

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

SQLエラスティックプールを使用。
エラスティックプールデータベース: 名前 RdSqlPool1 / 価格レベル Standard Pool / プールの構成 200 eDTU

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

「作成」ボタンをクリックしたら、デプロイが完了するまでしばらく待ちます・・・

1.2. 2つ目のSQL Databaseを作成

先程と同様の手順にてSQL Databaseの作成を行います。
構成内容は以下とします。

データベース名: RdExampleSQL2
リソースグループ: RdExampleSQL
SQLエラスティックプール使用: あり
エラスティックプールデータベースプール: RdSqlPool1(先程作成したRdExampleSQL1と共通のプール)

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

以上で東日本リージョンに「RdSqlPool1」というエラスティック データベース プール上に、2つのデータベース「RdExampleSQL1」と「RdExampleSQL2」が作成されました。

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

1.3. [NG!!]Geoレプリケーションを構成

ここからが、私がちょっとハマった点です。。。
「RdExampleSQL1」SQLデータベースを西日本リージョンにGEOレプリケーションさせようとしました。

「RdExampleSQL1」を選択します。

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

「geoレプリケーション」を選択します。

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

レプリケーション先として「西日本」リージョンを選択します。

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

すると、「エラスティック データベース プール」が選択できません。
「対象サーバー」を構成しても「エラスティック データベース プール」はロックされた状態のままです。

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

ここで一度、私は「GEOレプリケーションにはエラスティック データベース プールが使えないのではないか?」と考えてしまいました・・・
が、事実はそうではなく以下の手順を行うことで可能でした。
(ちなみに、以下の方法は帰宅途中の電車の中で突然「もしかして!?」と思いつきました。人間の突然の思いつき、は不思議・・・^^;)

2. レプリケーション先のエラスティック データベース プールを作成

正しい手順としては、「1.2. 2つ目のSQL Databaseを作成」の続きになります。

「新規作成 → SQL エラスティック データベース プール」を検索・作成します。

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

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

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

以下の画面で、「サーバー」をクリックし、「新しいサーバーの作成」を選択します。
サーバー構成は以下とします。

サーバー: rdsqlsrv2
場所: 西日本

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

「プールの構成」をクリックします。eDTU を 50 に設定します(マスターの eDTU 200 より低い値)。

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

「作成」ボタンをクリックしたら、デプロイが完了するまでしばらく待ちます・・・

以下が、西日本リージョンに「エラスティック データベース プール(RdSqlPool2)/ サーバー(rdsqlsrv2)」が作成された画面です。

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

3. Geoレプリケーションを構成

では RdExampeSQL1(東日本リージョン) を西日本リージョンにGEOレプリケーションしましょう。

RdExampleSQL1(SQLデータベース)を選択し、「geoレプリケーション」メニューをクリックします。

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

レプリケーション先として「西日本」リージョンを選択します。

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

以下の画面で、「対象サーバー」をクリックすると、先程作成した「rdsqlsrv2」が選択することができます。
「rdsqlsrv2」を選択します。

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

「エラスティック データベース プール」をクリックします。

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

「RdSqlPool2」を選択します。

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

「OK」ボタンをクリックすると、デプロイが開始されます。

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

RdExampeSQL2 についても同様の geoレプリケーション 設定を行います。

4. 作成結果

以上の操作により、以下のようなAzure SQL Database構成が完成しました。

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

  • 東日本リージョンのエラスティック データベース プールは 200eDTU

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

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

  • 東日本リージョンの2つのデータベース「RdExampleSQL1 / RdExampleSQL2」は西日本リージョンの 50 eDTU 環境のレプリケーションされている
    f:id:daigo-knowlbo:20170616011717p:plain
    f:id:daigo-knowlbo:20170616011800p:plain

まとめ

ということで、Azure SQL Databaseにおいて、無事、2リージョン間で「エラスティック データベース プールによるGeoレプリケーション」が完成しました。
エラスティック データベース プールを使うか、データベース個別にDTUを確保するか等はシステム規模や要件、コストにより要検討事項ですが、決した安くはないクラウドにおいて、適切なプランや構成を選択していきたいですね。

また、今回「Amazon RDS」も並行して調べてみたのですが、RDSの場合はMulti-AZ構成にしても「SQL Serverを使用した場合には、アベイラビリティに関するSLAが付かない」という点にちょっとびっくりしました。「MySQL, MariaDB, Oracle or PostgreSQL」の場合には 99.95% のアベイラビリティSLAされます。

という個人的にハマったことを含めての投稿でした。

※週末にはまたCosmos DB入門の続きを書きますー、ではー。

Azure Cosmos DB入門(5)

本コンテンツは「Azure Cosmos DB入門」の(5)です

ryuichi111std.hatenablog.com

5 Cosmos DBプログラミング ~ MongoDB編

前回・前々回のDocumentDB編に続き、今回は「MongoDB編」となります。

5.1 はじめに

MongoDBは、DocumentDBと同じく、保持するデータモデル形式が「ドキュメントデータモデル」となります。
つまり JSON形式 のドキュメントデータを保持・検索・更新するNoSQLデータベースとなります。
そもそも、MongoDBは 2009年にリリースされました。
当時、先発NoSQLプロダクトであった「Redis」や「Cassandra」よりも高機能なドキュメント指向データベースとして注目を集めました。(Apache CouchDBなんかも2005年からあるドキュメント指向データベースです・・・私はこちらには明るくないのですが・・・)

Cosmos DBでAPI/データモデルとしてMongoDBを選択するメリットには、以下のようなポイントがあげられます。

* 既存の MongoDBベースシステム のCosmos DBへの移行が容易
既にMongoDBベースのシステムがあり、これをクラウド化する場合、データアクセス部に関して既存資産を生かしたままCosmos DBへの移行が行えます。
※厳密には、Cosmos DB(MongoDB API/データモデル)とオンプレMongoDBとの互換性レベルについては、私も未確認なので要検証の部分があると思っています。
例えば、Cosmos DBでは 単一要素に対するHashedシャーディング のみをサポートしています。

* MongoDBの知識を有するエンジニアによるCosmos DBシステムの構築が容易
Cosmos DBの他のAPIモデルである DocumentDB や Tableに比べて、MongoDBスキルを有するエンジニアの方が世の中には多いでしょう。
その為、エンジニアの学習コストを抑えたうえでCosmos DBベースのシステムの構築が可能になります。

* Azureクラウド および オンプレ 両対応のシステムの構築が可能
API・データモデルに DocumentDB や Table を選択した場合、その動作環境はAzureクラウドに限定されてしまいます。
システムによってはクラウドおよびオンプレでサービス展開を行いたいケースもあるでしょう。そのようなケースでは、MongoDBモデルが有効になります。

5.1.1 Cosmos DB(MongoDBデータモデル)とMongoDB

マルチデータモデルである Cosmos DB のサポートする1つのデータモデルとしてMongoDBがサポートされています。
MongoDBは、DocumentDB と同じく「JSONフォーマットベースのドキュメント」をデータとして保持します(正確にはDocumentDBよりも以前からMongoDBはドキュメン指向NoSQLデータベースとして君臨していました)。
Cosmos DBによるMongoDB対応を図にすると以下のようなイメージになります。

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

Cosmos DBはMongoDBのプロトコルに準拠したI/Fを提供します。
その為、従来から存在するMongoDB用のクラスライブラリを用いた開発が行えますし、「mongoコマンド」や「Studio 3T for MongoDB(旧MongoChef)」から接続する事が可能です。

5.2 準備

5.2.1 Cosmos DB(MongoDB)データベースアカウントの作成

Azureポータルを利用して「Cosmos DB(DocumentDB)」を作成します。

Azureポータルをブラウザで表示し「新規」を選択。検索テキストボックスに「Azure Cosmos DB」と入力します。

f:id:daigo-knowlbo:20170608232706p:plain f:id:daigo-knowlbo:20170608232709p:plain

「Azure Cosmos DB」を選択します。

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

「作成」をクリックします。

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

アカウント情報を入力して「作成」をクリックします。
ここでは以下の設定を行いました。
ID: cosmosmongo111
API: MongoDB
リソースグループ: cosmosmongo111
場所: 西日本

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

以下のCosmos DBアカウントが作成されます。

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

5.2.2 Visual Studio 2017プロジェクトの作成

Visual Studio 2017を起動し、メニュー「ファイル → 新規作成 → プロジェクト」を選択します。
ここでは、プロジェクトテンプレートは「コンソールアプリケーション」、名前は「CosmosMongoDBExample」としました。

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

次に、Cosmos DB(DocumentDB)にアクセスするために、NuGetパッケージライブラリをプロジェクトに追加します。
ソリューションエクスプローラからプロジェクトをマウス右ボタンクリックし、表示されたメニューから「NuGetパッケージの管理を選択します。

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

表示されたNuGetパッケージ管理画面から、左上の「参照タブ」を選択し、検索テキストボックスに「MongoDB.Driver」と入力します。
一覧に MongoDB.Driver が表示されるので、選択して「インストール」ボタンをクリックします。

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

「MongoDB.Driver」と、その依存関係である「MongoDB.Driver.Core」がインストールされます。
「MongoDB.Driver」は、「MongoDB, Inc.」により提供される「Official .NET driver for MongoDB.」です。
つまり、Azure Cosmos DB(MongoDB)専用のものではなく、Cosmos DBとは無関係な 一般的なMongoDB に接続するための公式ドライバーとなります。

最後にCosmos DB(MongoDB)に保存するドキュメントモデルクラスとして以下のRoomReservationInfo.csをプロジェクトに追加します。これは前回のDocumentDBの解説で使用したのと同じドキュメントモデルクラスになります。
1点だけ、「Idプロパティに対する JsonProperty属性 指定」に相違があります。
モデルクラスをJSON形式に変換した際のプロパティ名の変更マップの指定ですが、DocumentDB時には「id」としていました。今回のMongoDBでは「_id」としています。
データモデルの方言の違いで、DocumentDBでは「ドキュメント識別子=id」であるのに対し、MongoDBでは「ドキュメント識別子=_id」と決められているためです。パーティションキー(シャーディングキー)の指定がある場合は、id / _idに加えてパーティションキー値(シャーディングキー値)との複合キーが、コレクション内におけるドキュメント識別子となります。

// RoomReservationInfo.cs

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace CosmosMongoDBExample
{
  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; }
  }
}

以上で、プロジェクトの下準備が完了しました。

5.3 データベースとコレクションの作成

今回のCosmos DB(MongoDB)においても、DocumentDBの場合と同様に「データベースアカウント → データベース → コレクション → ドキュメント(JSONデータ)」というデータモデル構造となります。
まず、データベースとコレクションを作成したいと思います。
作成方法はいくつかありますが、その中でも以下の3つの方法についてここでは取り上げることとします。

  • Azureポータルで作成
  • プログラムで作成
  • mongoコマンドで作成

5.3.1 Azureポータルで作成

Azureポータルで、作成した cosmosmongo111 を選択し、サイドバーから「データエクスプローラ」メニューを選択します。
続いて「New Collection」ボタンをクリックします。

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

「Add Collection」画面が表示されるので、必要事項を入力することでデータベースおよびコレクションを作成することができます。

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

作成されたデータベース・コレクションは以下の通りです。

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

この後の手順を進めるために、一旦、このGroupwareDBデータベースは削除しましょう。
「GroupwareDB」の右側にマウスカーソルを持っていくと「…」が表示され、これをクリックすると以下のようなポップアップメニューが表示されます。 「Delete Database」を選択します。

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

削除の確認のためにデータベースIDを入力し、「OK」ボタンをクリックします。

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

データベースの削除が完了しました。

5.3.2 プログラムで作成

本稿では、Cosmos DB(MongoDB)操作を管理するクラスとして MongoDbManagerクラス を用意することとします。
プロジェクトに MongoDbManagercs を追加します。

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

(1)接続情報の定義

「接続情報」及びこれから「作成するデータベース名・コレクション名」等をMongoDbManagerクラスに定数として定義します(リスト 1)。

リスト1 MongoDbManager.cs
private const string host = "【ホスト名】";
private const string userName = "【ユーザー名】";
private const string password = "【パスワード】";
private const string database = "GroupwareDB";
private const string collection = "RoomReservations";
private const int port = 10255;

// 上記は各値をコードに埋め込んでいますが、実運用コードでは構成から読み取る等の工夫が必要

【ホスト名】【ユーザー名】【パスワード】は各データベースアカウントごとに適切な値を設定します。
Azureポータルで「接続文字列」タブを選択することで確認することができます。

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

データベースIDは「GroupwareDB」、コレクションIDは「RoomReservations」としました。これはサンプルとして、グループウェアにおける会議室予約のデータベースを想定します。

(2)基本名前空間のusing定義

MongoDB接続・操作を行う上での基本的な名前空間をusing定義しておきます(リスト 2)。

リスト2 MongoDbManager.cs
// 一般的に利用する名前空間
using System;
using System.Collections.Generic;
using System.Security.Authentication;
using System.Linq;
using System.Threading.Tasks;

// MongoDB関連の名前空間
using MongoDB.Driver;
using MongoDB.Driver.Core;

(3)接続クライアントオブジェクトの定義

CosmosDB(MongoDB)に接続する為の接続クライアント「MongoDB.Driver.MongoClient」をMongoDbManagerクラスのフィールド変数「client」として定義します(リスト 3)。
また、clientはコンストラクタで初期化することとします。

リスト3 MongoDbManager.cs
public class MongoDbManager {
  private MongoClient client = null;
  
  public MongoDbManager()
  {
    // MongoClientの初期化
    var settings = new MongoClientSettings
    {
      Server = new MongoServerAddress(MongoDbManager.host, MongoDbManager.port),
      ServerSelectionTimeout = TimeSpan.FromSeconds(5)
    };
    settings.SslSettings = new SslSettings();
    settings.UseSsl = true;
    settings.SslSettings.EnabledSslProtocols = SslProtocols.Tls12;

    MongoIdentity identity = new MongoInternalIdentity(MongoDbManager.database, MongoDbManager.userName);
    MongoIdentityEvidence evidence = new PasswordEvidence(MongoDbManager.password);

    settings.Credentials = new List<MongoCredential>()
    {
      new MongoCredential("SCRAM-SHA-1", identity, evidence)
    };
    this.client = new MongoClient(settings);
  }
}

MongoClientSettingsオブジェクトにより接続の為の情報をいくらか設定する必要があります。
MongoClientSettingsのコンストラクタでは、接続先ホスト名とポート番号、タイムアウト時間(5秒)により初期化しています。
また、Azure Cosmos DBはSSL(TLS)による接続をサポートするため、MongoClientSettings.SslSettingsの設定も行っています。

(4)コレクション作成メソッドの追加

MongoDbManagerクラスにデータベースを作成するメソッド「CreateCollection()」を定義します(リスト 4)。

リスト4 MongoDbManager.cs
public async Task<bool> CreateCollection()
{
  IMongoDatabase mongoDatabase = 
    this.client.GetDatabase(MongoDbManager.database);

  await mongoDatabase.CreateCollectionAsync(MongoDbManager.collection);

  return true;
}

MongoClient.GetDatabase()メソッド呼び出しにより「データベース」オブジェクト(IMongoDatabaseインターフェイス型)を取得することができます。未作成のデータベースでもGETすることができます。
続けて、取得したIMongoDatabaseオブジェクトのCreateCollectionAsync()を呼び出すことで、データベースおよびコレクションが作成されます(データベースもこのタイミングで同時に作成されます)。

以下が MongoDbManager.CreateCollection() 呼び出しコードです(リスト5)。

リスト5 Program.cs
MongoDbManager manager = new MongoDbManager();
bool result = manager.CreateCollection().Result;

上記コードによるデータベース・コレクションの作成を行った結果をAzureポータルで確認したものが以下です。
シングルパーティション構成、予約RUは1000での作成となります。

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

先程と同様、一旦 GroupwareDB データベースは削除しておきましょう。

5.3.3 mongoコマンドで作成

mongoコマンドを使用して、Cosmos DB(MongoDB)に接続することが可能です。
mongoコマンドは、データベース・コレクションの作成、ドキュメントの作成・更新・削除・抽出、また、シャーディング設定といった多くの操作を行うことが可能です。

mongoコマンド利用の準備

mongoコマンドはMongoDBをインストールすることで利用可能になります。
MongoDBは以下のURLからダウンロードすることができます。

MongoDB Download Center | MongoDB

以下のダウンロード画面では、ドロップダウンによる Version の選択があります。Windows10のようなクライアントOSでも「Windows Server 2008 R2 64-bit and later, with SSL support x64」を選択すればOKです。

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

ダウンロードしたセットアップEXEを起動し、インストーラの指示に従うのみでインストール可能です。
しかし、MongoDBサーバーは不要、Cosmos DB(MongoDB)との接続クライアントであるmongoコマンドのみを必要とする場合は、以下の画面に示すように Custom インストールモードにて「Client」のみをセットアップすることができます。

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

インストール完了後は「環境変数」の設定により、「C:\Program Files\MongoDB\Server\3.4\bin」にPathを通しておくと便利です。

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

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

環境変数設定後は、Windowsを再起動します。

mongoコマンドを利用

コマンドプロンプトを起動します。
以下のコマンドを実行し、Azure Cosmos DB(MongoDB)と接続します。
※【ホスト名】【ユーザーID】【パスワード】は、Azureポータル→cosmosmongo111→接続文字列から確認。

mongo --ssl --host 【ホスト名】 --port 10255 -u 【ユーザーID】 -p 【パスワード】

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

「use 【データベース名】」コマンドを実行します。
ここでは use GroupwareDB としました。

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

GroupwareDBデータベースは以下に、シャーディングを有効にしたコレクションを作成することにします。
シャーディングとはMongoDBの機能で「水平スケーリング」を行うための機能です。Cosmos DBでは「パーティショニング」として実装されています。
以下のコマンドを実行します。

db.runCommand( { shardCollection: "GroupwareDB.RoomReservations", key: { Room: "hashed" } } )

このコマンドを実行したタイミングで、データベースおよびシャーディング有効コレクションが作成されます。
シャーディングキー(=パーティションキー)は「Room」としました。対象コレクションに保存する予定の RoomReservationInfoクラスのRoomプロパティ を表します。

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

Azureポータルで確認した画面が以下になります。シャーディング(パーティショニング)が有効になったため、設定可能な最低RUが2500となっています。そして、実際に作成されたコレクションのRUは2600となります(なぜ2500ではなく2600なのかは不明・・・)。

f:id:daigo-knowlbo:20170609032024p:plain
f:id:daigo-knowlbo:20170609233527p:plain

5.4 ドキュメントの作成

MongoDbManagerクラスにドキュメントを作成するメソッド「CreateRoomReservationInfo()」を定義します(リスト 6)。

リスト6 MongoDbManager.cs
public async Task<bool> CreateRoomReservationInfo(RoomReservationInfo roomReservationInfo)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);
  await mongoCollection.InsertOneAsync(roomReservationInfo);

  return true;
}

MongoClientオブジェクトから IMongoDatabaseオブジェクト を取得します(MongoClient.GetDatabase()メソッド呼び出し)。
さらに IMongoCollectionオブジェクトを取得します(IMongoDatabase.GetCollection())。
IMongoDatabase.GetCollection()では扱うモデルクラス型を設定します。ここではRoomReservationInfo型としました。 IMongoCollection.InsertOneAsync()メソッドを呼び出すことで新規のドキュメントを作成することができます。

5.4.1 ドキュメントの一括投入 ~ Studio 3T for MongoDB(旧Mongo chef)

以降の検索の説明等では、もっと大量のドキュメントが登録されている状態が好ましいです。
その為に、ここでデータ量を増やしておきたいと思います。
ここでは「Studio 3T for MongoDB(旧Mongo chef) 」というツールを利用します。
SQL Serverでいうところの「SQL Server Management Studio」のようなツールです。
以下からダウンロード可能です。

studio3t.com

インストールを行い起動します。起動したら左上の「Connect」をクリックします。

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

表示された「Connection Manager」ウィンドウで「New Connection」をクリックします。

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

表示された「New Connection」ウィンドウに接続情報を入力します。
入力する情報は Azureポータル → 対象のCosmosDB → 接続文字列 で確認することができます。

「Server」タブの設定は以下の通りです。

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

「Authentication」タブの設定は以下の通りです。

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

SSL」タブの設定は以下の通りです。

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

接続した画面が以下になります。

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

左のツリーから RoomReservationsコレクションを選択し、マウス右ボタンクリックでポップアップメニューを開きます。
ポップアップメニューから「Import Data…」を選択します。

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

JSON形式を選択しNextボタンをクリックします。

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

「+」ボタンをクリックし、インポートソースとなるjsonファイル(exampledata.json)を選択します。

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

インポート内容の確認を行い、「Start Import」ボタンをクリックします。

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

jsonファイルからAzure Cosmos DB(MongoDB)にデータが投入されました。

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

5.5 ドキュメントの検索

RoomReservationsコレクションに登録されたドキュメントをいくつかのパターンで検索してみます。

5.5.1 Mongoクエリー言語による検索

Mongoクエリー言語によるクエリーを行います。
Mongoクエリー言語はJSON形式で記述が可能なMongoDB用クエリー言語です。

(1) Roomによる検索

パーティションキー(シャーディングキー)であるRoom(RoomReservationInfo.Room)による検索を実装します。
MongoDbManagerクラスにFindByRoom(stirng room)メソッドを追加します。

リスト7 MongoDbManager.cs
public async Task<List<RoomReservationInfo>> FindByRoom(string room)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  var find = await mongoCollection.FindAsync<RoomReservationInfo>("{ Room: \"" + room + "\"}");
  return find.ToList();
}

コレクションオブジェクトに対して、FindAsync()メソッドを呼び出します。
は取得するドキュメントクラスである RoomReservationInfo を指定します。
引数の文字列がMongoクエリー言語による抽出条件式です。FindByRoom()メソッド引数を使用して文字列連結していますが、展開すると例えば以下のようなJSON文字列になります。

{ Room: "第1会議室" }

「Room要素の値が第1会議室である」という条件式になります。
SQL風に表すと以下のようになります。

WHERE Room = '第1会議室'

MongoDbManager.FindByRoom()の呼び出し側の実装は以下の通りです(リスト8)。

リスト8 MongoDbManager.cs
MongoDbManager manager = new MongoDbManager();

var roomReservationInfos = manager.FindByRoom("第1会議室").Result;

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));
}

(2) Roomによる検索(その2)

作成したFindByRoom()メソッドは以下のように書き換えることができます(リスト9)。
動作上の意味はイコールになります。

リスト9 MongoDbManager.cs
public async Task<List<RoomReservationInfo>> FindByRoomEx(string room)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  FilterDefinition<RoomReservationInfo> query = 
      Builders<RoomReservationInfo>.Filter.Eq(r => r.Room, room);
  var find = await mongoCollection.FindAsync<RoomReservationInfo>(query);
  return find.ToList();
}

リスト8とリスト9の違いは、FindAsync()メソッドへの引数にあります。
リスト8では「{ Room: “第1会議室” }」のようなJSON文字列条件式を渡していました。
一方、リスト9では「FilterDefinition型」を引数として渡しています。
FindAsync()メソッドの定義を見ると、本来 JSON文字列(stirng型) を引数として受け取る実装はありません。
なぜJSON文字列(string)を引数として渡すことができているのか?それはC#のimplicitキーワード機能に起因します。

implicit (C# リファレンス) | Microsoft Docs

「MongoDB.Driver.FilterDefinitionクラス」の実装は、以下のgithubで確認することができます。

mongo-csharp-driver/FilterDefinition.cs at master · mongodb/mongo-csharp-driver · GitHub

その中で、以下のリスト10の実装が行われています。
「implicit operator FilterDefinition(string json)」、つまり、string json型からFilterDefinition型への暗黙的変換を行うオペレータを定義しています。
リスト8が実行される際、FindAsync()の「引数string」は、FilterDefinitionのimplicit operatorにより、FilterDefinitionオブジェクトに型変換されています。

リスト10 FilterDefinition.cs
// Mongo C# Driver実装より引用

public abstract class FilterDefinition<TDocument>
{
  ... 省略
  
  /// <summary>
  /// Performs an implicit conversion from <see cref="System.String"/> to <see cref="FilterDefinition{TDocument}"/>.
  /// </summary>
  /// <param name="json">The JSON string.</param>
  /// <returns>
  /// The result of the conversion.
  /// </returns>
  public static implicit operator FilterDefinition<TDocument>(string json)
  {
    if (json == null)
    {
      return null;
    }

    return new JsonFilterDefinition<TDocument>(json);
  }
  
  ... 省略
}

(3) _id + パーティションキーによる検索

次に _id と パーティションキーRoom による検索を行います。
今回のサンプルコレクションでは、パーティショニング(シャーディング)設定を行っているので、「_id + パーティションキーRoom」がドキュメントをユニークに識別するキーになります(リスト11)。

リスト11 MongodbManager.cs
// Roomと_idから、単一のRoomReservationInfoを取得します。
public async Task<RoomReservationInfo> FindByRoomAndId(string room, string id)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  var find = await mongoCollection.FindAsync<RoomReservationInfo>(
    "{ Room: \"" + room + "\", _id: \"" + id + "\"}");

  return find.FirstOrDefault();
}

上記リスト11で実行されるMongoクエリーJSONは、以下のような形式になります。

{ Room: "第1会議室", _id: "000000001" }

(4) 複合条件による検索

次に「Room=【Room】 AND AssignMembers.UserID = 【UserID】」という2つの値のAND条件の実装を行います(リスト12)。

リスト12 MongoDbManager.cs
public async Task<List<RoomReservationInfo>> FindByRoomAndAssignMember(string room, string assignMemberId)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  var find = await mongoCollection.FindAsync<RoomReservationInfo>(
    "{ Room: \"" + room + "\" , AssignMembers: { $elemMatch: { UserId: \"" + assignMemberId + "\"}}}");
  return find.ToList();
}

5.5.2 LINQによる検索

JSONによるMongoクエリー言語以外にLINQクエリーを利用することができます。
リスト8をLINQベースで書き直したFindByRoomLinq()メソッドの実装はリスト13です。

リスト13 MongoDbManager.cs
public async Task<List<RoomReservationInfo>> FindByRoomLinq(string room)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  var find = await mongoCollection.FindAsync<RoomReservationInfo>(r => r.Room == room);

  return find.ToList();
}

5.6 ドキュメントの更新

ドキュメントを更新する処理を実装します(リスト14)。
RoomReservationInfoの Titleプロパティ のみを変更する処理とします。

リスト14 MongoDbManager.cs
public async Task<bool> UpdateTitle(RoomReservationInfo roomReservationInfo)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  // パーティショニング(シャーディング)構成なので Room + _id でユニークなキー
  var result = await mongoCollection.UpdateOneAsync<RoomReservationInfo>(
    r => r.Id == roomReservationInfo.Id && r.Room == roomReservationInfo.Room, 
    new UpdateDefinitionBuilder<RoomReservationInfo>().Set(i => i.Title, roomReservationInfo.Title));

  return true;
}

ドキュメントの更新には、UpdateOneAsync()メソッドを呼び出します。

第1引数で、更新対象のドキュメントを特定します。
第2引数で、更新個所を設定します。今回は Title のみの更新になります。

リスト14のドキュメント更新実装を呼び出す処理は以下の通りです(リスト15)。

リスト15 
MongoDbManager manager = new MongoDbManager();

// 1件取得
var roomReservationInfo = manager.FindByRoomAndId("第1会議室", "0000000000").Result;

// タイトルプロパティを変更
roomReservationInfo.Title = "タイトル変更!!!!";

// 更新処理呼び出し
var result = manager.UpdateTitle(roomReservationInfo).Result;

5.7 ドキュメントの削除

ドキュメントを削除する処理を実装します(リスト16)。

リスト16 MongoDbManager.cs
public async Task<bool> Delete(RoomReservationInfo roomReservationInfo)
{
  var mongoDatabase = this.client.GetDatabase(MongoDbManager.database);
  var mongoCollection = mongoDatabase.GetCollection<RoomReservationInfo>(MongoDbManager.collection);

  var result = await mongoCollection.DeleteOneAsync<RoomReservationInfo>(
    r => r.Id == roomReservationInfo.Id && r.Room == roomReservationInfo.Room);

  return true;
}

DeleteOneAsync()を呼び出します。
引数でドキュメントをユニークに識別するための「Room(パーティションキー)」+「_id」を設定しています。

リスト16のドキュメント削除実装を呼び出す処理は以下の通りです(リスト17)。

リスト17
MongoDbManager manager = new MongoDbManager();

var roomReservationInfo = manager.FindByRoomAndId("第1会議室", "00001").Result;

var result = manager.Delete(roomReservationInfo).Result;

5.8 資料

本記事のサンプルは以下からダウンロード可能です。

github.com

5.9 つづく・・・

次回はCosmos DBによる Graph DB、Gremlinについての説明になります。ではー。

<< Azure Cosmos DB入門(4)へ | Azure Cosmos DB入門(6)へ >>