読者です 読者をやめる 読者になる 読者になる

BEACHSIDE BLOG

MicrosoftとかC#を好むレンジャーの個人的メモ

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

さて、前回Azure DocumentDB の開発ことはじめ - Client クラスの開発(1/2) - BEACHSIDE BLOGの続きです。

> 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


前回 >> 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からコールしてみる

>4. Clientクラス「DdbClinet」クラスの作成2(コレクションの操作、Documentの操作)

前回作成したDdbClinetクラスに追記していきます。

    class DdbClinet : IDisposable
    {
      //前回の部分は省略

      #region collectionの操作

        public DocumentCollection GetCollectionIfNotExistsCreate(string collectionName, CollectionOfferType offerType = CollectionOfferType.S1)
        {
            var collection = TryGetCollection(DatabaseInstance, collectionName) ??
                                CreateCollectionAsync(DatabaseInstance.CollectionsLink, collectionName, offerType).Result;
            if (collection == null) throw new InvalidOperationException($"コレクションの生成に異常ありんご!({collectionName})");

            return collection;
        }

        DocumentCollection TryGetCollection(Database database, string collectionName)
        {

            return _client.CreateDocumentCollectionQuery(database.CollectionsLink)
                        .Where(c => c.Id == collectionName).AsEnumerable().FirstOrDefault();
        }

        async Task<DocumentCollection> CreateCollectionAsync(string databaseLink, string collectionName, CollectionOfferType offerType)
        {
            var collection = new DocumentCollection() { Id = collectionName };
            var requestOptions = new RequestOptions() { OfferType = ComvertOfferTypeToString(offerType) };
            return await _client.CreateDocumentCollectionAsync(databaseLink, collection, requestOptions);

        }

        static string ComvertOfferTypeToString(CollectionOfferType offerType) => Enum.GetName(typeof(CollectionOfferType), offerType).ToUpperInvariant();

        #endregion

コレクションのインスタンスの取得(存在しなければ生成する)メソッドです。DocumentDBでは、DocumentのCRUDをする際に、コレクションのselflinkというものを使うので、その情報を取得するためのメソッドたちです。
コレクションの作成にはOfferType、つまり価格レベル(性能レベル)を設定するので、コレクションを作る際にその値を指定します。今回はテスト用でもあるので、価格そして性能が最小の「S1」を指定しています。
ここでは、その指定をenumで定義しますので、どっかに適当に定義しておきましょう。

public enum CollectionOfferType
{
    S1,
    S2,
    S3
}


さてさて、次はDocumentを操作するメソッドをいくつか用意してみます。上記の下にそのままコーディングします。

#region documentの操作

public async Task<dynamic> InsertDocumentAsync<TEntity>(string collectionSelfLink, TEntity entity, bool configureAwait)
{
    return await _client.CreateDocumentAsync(collectionSelfLink, entity).ConfigureAwait(configureAwait);
}

public IEnumerable<TEntity> GetDocumentsByPredicate<TEntity>(string collectionSelfLink, Expression<Func<TEntity, bool>> predicate)
{

    return _client.CreateDocumentQuery<TEntity>(collectionSelfLink, _feedOptions).Where(predicate).AsEnumerable();
}


public async Task DeleteDocumentsByQueryAsync(string collectionSelfLink, string query, bool configureAwait)
{
    var documents = _client.CreateDocumentQuery<Document>(collectionSelfLink, query).AsEnumerable().ToArray();
    if (documents.Any())
    {
        foreach (var doc in documents) await DeleteDocumentAsunc(doc, configureAwait);
    }
}

public static async Task DeleteDocumentAsunc(Document document, bool cofigureAwait = true)
{
    await _client.DeleteDocumentAsync(document.SelfLink).ConfigureAwait(cofigureAwait);
}

#endregion

ドキュメントをInsertする「InsertDocumentAsync」、
条件で指定してDocumentを取り出す「GetDocumentsByPredicate」、
クエリで指定したドキュメントを削除する「DeleteDocumentsByQueryAsync」を用意してみました。
ConfigureAwaitをどうしよう?どうすべき?みたいな悩んでます。とりあえずこんなことにしてますが....どうなんすかね?!用と次第だと思いますが...勉強不足です。
(GetDocumentsByPredicateは、Expression使うほどのこともないですが、別の検証で使ってたのでそのまま流用。。。)

さて、実際にこのメソッドを使ってみましょう。次に進みます。

>5. Documentを操作するクラスの作成

と、その前に、Documentのスキーマ的なクラスとして、今回はPersonクラスを作ります。

using Newtonsoft.Json;


public class Person
{
    [JsonProperty(PropertyName = "personId")]
    public string PersonId { get; set; }

    [JsonProperty(PropertyName = "firstName")]
    public string FirstName { get; set; }

    [JsonProperty(PropertyName = "lastName")]
    public string LastName { get; set; }

    [JsonProperty(PropertyName = "friends")]
    public int[] Friends { get; set; }
}

今回のサンプルでは、このModelをDocumentに入れたり取り出したりします。

Documentを操作するクラス「DemoCollecitonManager」(名前ダサい)を作ります。DemoCollecitonというコレクションを操作する用ですが、Managerとかないかなーと思いつつつつつつつつつつけちゃいました。。。。。

public class DemoCollecitonManager
{
    #region fields

    // ここら辺Lazy<T>とかThreadLocalにすべきかとかは、用途に応じて...
    static Lazy<DocumentCollection> _demoCollection;
    static Lazy<DdbClinet> _clinet;

    static DdbClinet Client => _clinet.Value;
    static DocumentCollection DemoCollection => _demoCollection.Value;

    static DemoCollecitonManager()
    {
        _clinet = new Lazy<DdbClinet>(DdbClinetFactory.GetInstance);
        _demoCollection = new Lazy<DocumentCollection>(() => Client.GetCollectionIfNotExistsCreate("DemoCollection"));
    }

    #endregion

    #region Documentの操作

    public static async Task InsertPersonDocument(Person model)
    {
        await Client.InsertDocumentAsync(DemoCollection.SelfLink, model, true);
    }

    public static IEnumerable<Person> GetPersonModelsById(int personId)
    {
        return Client.GetDocumentsByPredicate<Person>(DemoCollection.DocumentsLink, p => p.PersonId == personId);
    }


    #endregion
}

DocumentDBはとにかくネットワークアクセスの回数を減らすのが重要なので、コレクションとClientのメンバーを持っています。コレクションとかのインスタンス周り、コメントにも書いてますが、Lazy使うの?ThreadLocal使うの?とかは用途次第です。今回はサンプル程度ということで何となくでLazy使ってます。

プロパティは、それぞれのインスタンスがなければインスタンス化するだけのシンプルなものです。
Documentの操作は、DdbClinetのメソッドをコールしてるだけです。

>6. Program.csからコールしてみる

さて、最後にコンソールのProgram.csから呼んでみましょう。

class Program
{
    static void Main(string[] args)
    {
        var person1 = new Person()
        {
            PersonId = "P1",
            FirstName = "Char",
            LastName = "ming",
            Friends = new[] { 2, 3 }
        };

        var person2 = new Person()
        {
            PersonId = "P2",
            FirstName = "Dan",
            LastName = "Dy",
            Friends = new[] { 2, 3 }
        };

        //insert samples
        DemoCollecitonManager.InsertPersonDocument(person1).Wait();
        DemoCollecitonManager.InsertPersonDocument(person2).Wait();

        //select sample
        var dandy = DemoCollecitonManager.GetPersonModelsById(person2.PersonId);
        Console.WriteLine(JsonConvert.SerializeObject(dandy, Formatting.Indented));

        Console.ReadKey();

    }
}

Personを2つ用意して、Insertします。あとは、PersonIdで検索して、コンソールにjsonで出力します。
接続の初回は、データベースがなければ作成するし、コレクションがなければ作成するしで遅いですが、DemoCollecitonManagerクラスのDemoCollectionやDdbClinetクラスのDatabaseInstanceに値が取得できた状態でのアクセスならそんなに遅くないです。

ただ、やっぱりDocumentDBと同一のリージョンじゃないと、10倍くらいの速度差がでます(私の環境では)。

deleteについては触れてませんが、queryでdelete対象のdocumentsを取得して削除メソッド呼ぶだけです。クエリで取得すると、IEnumerableを返すので、複数の削除を前提に削除メソッドを作ってます。
コードのサンプルには書いてます。

さて、検証が終わったらデータベースやコレクションはポータルから削除しておきましょう。しないとガシガシ課金がかかります!
以前に性能検証とかでコレクションをS3でたくさん作ったときは、削除し忘れてクレジットが溶けた気分になりました。


今のところ情報がさほど多くないので、これ以外にいい方法あるのかなーとか気になってるところなので、情報が増えたらうれしいですね。

先日、この記事が出てましたが、気にはしてるけど見てないので、今週見よう...きっと...
azure.microsoft.com
github.com