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)へ >>