BEACHSIDE BLOG

Azure と GitHub と C# が好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

Azure DocumentDB の開発ことはじめ - Client クラスの開発(1/2)

Azure DocumentDb に接続する Client を開発するのに、
Microsoft.Azure.Documents.Client名前空間の DocumentClient クラスでは、以前のブログAzure DocumentDB を使うときに知っておきたいいくつかのこと - BEACHSIDE BLOG
でちょっと触れた「 Request rate is too large 」のエラーに対応するのに大変だったり、その他諸々の設定するのが面倒と思ってます。
ということで、ちょっと拡張したClientクラスを作った際のメモです。

> Environment

  • Visual Studio 2015 Update1(RC)で、コンソールアプリ
  • .NET Framework4.5.2
  • Nugetの Microsoft.Azure.DocumentDB (1.5.0)
  • Nugetの Microsoft.Azure.DocumentDB.TransientFaultHandling (1.2.0)

多少カスタマイズされてますが、ソースはこちらにあります。

> Overview

以下の6章の編成です。

今回 >> Clientクラスの開発(1/2)
  0. 事前準備
  1. コンソールアプリ作成と下準備
  2. Clientクラス「DdbClinet」クラスの作成1(データベースの操作)
  3. Clientのインスタンスを生成するための「DdbClinetFactory」クラス作成


次回 >> Clientクラスの開発(2/2)
  4. Clientクラス「DdbClinet」クラスの作成2(コレクションの操作、Documentの操作)
  5. Documentを操作するクラスの作成
  6. Program.csからコールしてみる


DocumentDBのアクセスに関するクラスの構成は、3つに分けて構成しました。

  • DdbClinet

コアとなるDocumentDBのClientのクラス。データベースインスタンスの操作と、DocumentのCRUDに関する共通メソッドで構成。

  • DdbClinetFactory

DdbClinetのインスタンス生成クラス。Clientの構成情報をどっかから取得し、インスタンスを生成する。

  • データベースごとにDocumentのCRUDをするクラス

コレクション内のDocumentのCRUD操作の扱うクラス。今回は単一のコレクションしか扱ってませんが、複数のコレクションを扱うのであれば、コレクションの定義を増やして使う、または責務を考えて別クラスにするとか...使い方は用途次第。



余談ですが、
今はVisual Studio英語版を使ってるので、キャプチャーがそんな表示になってます。
(Update1CTPのとき、私だけ?!エディタのフォントが変更できなくなる事故があったのですが、Visual Studio 日本語版のデフォルトのフォント「MSゴシック」でコードを見てると精神不安定?精神崩壊に陥りそうでプログラミングに支障がでるので、デフォルトのフォントがConsolasな英語版にしました...)


>0. 事前準備

azureのポータル側で、DocumentDB のアカウントを事前に作っておきます。まだの方は、ここら辺を参考に...
azure.microsoft.com
まぁ、色々とお金かかると思うので、自己責任でご注意しながらやってくださいね。

接続に使うURLや、エンドポイント、認証キーの情報を取得するためです。

>1. コンソールアプリ作成と下準備

まずは、新規プロジェクトをconsoleアプリ作成します。
f:id:beachside:20151101155325p:plain
ここら辺はさっと。

今回の開発用にnugetでいくつかインストールします。

まず、nuget package managerを開きます。
f:id:beachside:20151101160122p:plain

DocumentDB の開発に必要なものを2つインストールします。
f:id:beachside:20151101160143p:plain
検索機能を使って「documentdb」で検索すると、さっと選択できます。そして以下をインストール。2015/10/30時点での最新版です。

  • Microsoft.Azure.DocumentDB (1.5.0)
  • Microsoft.Azure.DocumentDB.TransientFaultHandling (1.2.0)


次は、DocumentDBの接続情報は、とりあえず定番はアプリケーション構成ファイルに入れて、読みだそうと思うので、プロジェクトに参照の追加をします。
プロジェクトの「参照」を右クリックで参照の追加を選択します(日本語名称間違ってたらごめんなさい)。
f:id:beachside:20151101160748p:plain

画面左側は、「Assemblies」を選択し、右上の検索で、「config」と入力すれば、「System.Configuration」がでます。これを追加します。

>2. Clientクラス「DdbClinet」クラスの作成1(データベースの操作)

本家Azureのドキュメントのサンプルで使っているのは、Client、Microsoft.Azure.Documents.Client 名前空間の DocumentClient クラスですが、色々辛いです。

ということで、DocumentClientクラスの代わりに、
Microsoft.Azure.Documents.Client.TransientFaultHandling 名前空間の IReliableReadWriteDocumentClient クラスを使います。
これは、ざっくり説明すると「Request rate is too large」に容易に対応できるClientクラスです(ざっくり過ぎですいませぬ...)。

余談ですが、
staticに使いまわす前提でDisposeを明示的にしないのであれば、IReliableReadDocumentClient クラスを使ってImmutableにしてあげる方がいいなーと思ってます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Client.TransientFaultHandling;
class DdbClinet : IDisposable
{
    private IReliableReadWriteDocumentClient _client;
    private readonly FeedOptions _feedOptions;
    private readonly string _databaseName;
  // ここら辺、Lazy<T>とかThreadLocalにすべきかとかは用途に応じて...
    private Lazy<Database> _databaseInstance;
    private Database DatabaseInstance => _databaseInstance.Value;
  
    public DdbClinet(IReliableReadWriteDocumentClient client, string databaseName, int feedOptionMaxItemCount)
    {
        _client = client;
        _databaseName = databaseName;
        _feedOptions = new FeedOptions { MaxItemCount = feedOptionMaxItemCount };
        _databaseInstance = new Lazy<Database>(() => GetDatabaseIfNotExistsCreate(_databaseName));
    }

    #region databaseの操作

    Database GetDatabaseIfNotExistsCreate(string name)
    {
        var database = TryGetDatabase(name) ?? CreateDatabaseAsync(name).Result;
        if (database == null) throw new InvalidOperationException($"データベースの生成に異常ありんご!({name})");

        return database;
    }

    Database TryGetDatabase(string name)
    {
        return _client.CreateDatabaseQuery().Where(d => d.Id == name).AsEnumerable().FirstOrDefault();
    }

    async Task<Database> CreateDatabaseAsync(string name)
    {
        return await _client.CreateDatabaseAsync(new Database { Id = name });
    }

    #endregion
}

いきなりダーンと書いてしまいました...さっくり説明してきます。

データベースのインスタンス取得のためのメソッドが並んでますが、ぼちぼちデータベースの情報を使うのでプロパティに入れて使いまわすようにしています。
また、データベースのインスタンスを取得にいって、存在しなければデータベースを作っちゃってます。
ここでは書いてませんが、IDisposableをimplimentしてますので、エラーがでないようにDisposeメソッドは適当に書いておきましょうね。

その他諸々使って動かせば理解できるコードだと思いますので、とりあえず次に進んでClientのインスタンスを生成するクラスを作ります。

>3. Clientのインスタンスを生成するための「DdbClinetFactory」クラス作成

先ほど書いたDdbClinetクラスのコンストラクターを呼び出すための値を生成するクラスです。

using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Client.TransientFaultHandling;
using Microsoft.Practices.EnterpriseLibrary.TransientFaultHandling;
using static System.Configuration.ConfigurationManager;

namespace DocumentDbDemo.DocumentDbRepository
{
    static class DdbClinetFactory
    {

        static readonly ConnectionMode ConnectionMode = ConnectionMode.Direct;
        static readonly Protocol Protocol = Protocol.Tcp;

        static readonly string EndpointUri;
        static readonly string AuthorizationKey;
        static readonly int RetryCount;
        static readonly TimeSpan RetryInterval;
        static readonly string DatabaseName;
        static readonly int FeedOptionMaxItemCount;

        static DdbClinetFactory()
        {
            EndpointUri = AppSettings["DdbConfig.endpointUri"];
            AuthorizationKey = AppSettings["DdbConfig.authorizationKey"];
            RetryCount = int.Parse(AppSettings["DdbConfig.retryCount"]);
            RetryInterval = TimeSpan.FromSeconds(int.Parse(AppSettings["DdbConfig.retryInterval"]));
            FeedOptionMaxItemCount = int.Parse(AppSettings["DdbConfig.feedOptionMaxItemCount"]);
            DatabaseName = AppSettings["DdbConfig.databaseName"];
        }
        public static DdbClinet GetInstance()
        {
            var policy = CreateConnectionPolicy(ConnectionMode, Protocol);
            var documentClinet = CreateDocumentClient(EndpointUri, AuthorizationKey, policy);
            var strategy = GetRetryStrategy(null, RetryCount, RetryInterval, false);
            return new DdbClinet(documentClinet.AsReliable(strategy), DatabaseName, FeedOptionMaxItemCount);

        }

        static ConnectionPolicy CreateConnectionPolicy(ConnectionMode connectionMode, Protocol protocol)
        {
            return new ConnectionPolicy
            {
                ConnectionMode = connectionMode,
                ConnectionProtocol = protocol
            };
        }
        static DocumentClient CreateDocumentClient(string endpointUrl, string authorizationKey, ConnectionPolicy connectionPolicy)
                => new DocumentClient(new Uri(endpointUrl), authorizationKey, connectionPolicy);

        static FixedInterval GetRetryStrategy(string name, int retryCount, TimeSpan retryInterval, bool firstFastRetry)
                => new FixedInterval(name, retryCount, retryInterval, firstFastRetry);

    }
}

ここもドーンと書いてしまいました。

まず余談として、
DocumentDBの接続情報はApp.configから取得しますので、c#6.0から使えるusing staticも何気に使ってます。

using static System.Configuration.ConfigurationManager;

これで、アプリケーション構成ファイルから値を取得する際に毎回、

var somethingValue= ConfigurationManager.AppSettings["somethingKey"];
var somethingValue= AppSettings["somethingKey"];

と書けます。まぁ、たいしたことないんですが、新しいことは何かと使いたいものです。


さて本題に戻りますが、
フィールドメンバーにしている値が、DocumentDBに接続する際に設定する情報の一覧だと思ってください。実際の値は、App.configに格納していますが、直接値を書いているもの(ConnectionModeとProtocol)は、めんどくさいから書いただけです。

設定値についての不明な場合は、Azure本家や
Azure DocumentDB を使うときに知っておきたいいくつかのこと - BEACHSIDE BLOG
あたりを辿って情報を探してみましょう。

ここでの設定で、以前も紹介したパフォーマンスに関することもここでおおよそ網羅できます。

beachside.hatenablog.com

azure.microsoft.com

インデックスポリシーとかその他諸々漏れているものは、...........そのうち勉強します.....(汗)

コードについてざっくり説明ですが、
GetInstanceを呼べば、DocumentDBに接続するためのClientクラスが生成されます。リトライのストラテジーやFeedOptionもバシーっと設定していますので、安定のClientになりました(と思いたい)。

DocumentDBの構成情報を格納したApp.configの中はこんな感じに設定してます。
DdbConfig.endpointUrl、DdbConfig.authorizationKey、DdbConfig.databaseNameの値は、Azureポータルから自分の情報を取得して入れます。

リトライやFeedOptionに関する値は、私はこの値を使ってますが、自身の判断で必要な値を入力します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
<appSettings>
  <add key="DdbConfig.endpointUri" value="https://yoururl!!!!!.documents.azure.com:443/"/>
  <add key="DdbConfig.authorizationKey" value="your authorizationKey!"/>
  <add key="DdbConfig.retryCount" value="10"/>
  <add key="DdbConfig.retryInterval" value="1"/>
  <add key="DdbConfig.feedOptionMaxItemCount" value="1000"/>
  <add key="DdbConfig.databaseName" value="Demo"/>
</appSettings>
</configuration>

これで大体基本的なところはできました。
データベースインスタンスを複数扱い、インスタンスによって設定値を変えるのであれば、各種定義用のクラスを作ってそれを受け取るコンストラクターを作っておけば大丈夫ですね(私が別のプログラムでそうしてるだけですが)。


(ってかバタバタ書くと説明書くのがめんどくさくなっちゃって......読み返すとわかりにくいなー...)


おなかがすいたので、また次回にします。