Azure DocumentDB Emulatorを使ってみた。で、.NET Coreから操作した話。

技術者としての尊敬の対象である Scott Hanselman 氏のブログで「Azure DocumentDB Emulator」についての記事が書かれていたので、自分でも使ってみました。

www.hanselman.com

こんなことをやった

「Azure DocumentDB」は、もはや、広く知られた Azureが提供する NoSQL データベースです。
いわゆる「ちょー早くて、ちょースケーラブルで、RDBの概念は捨ててから使ってね」っていうデータベースですね。
NoSQLは、その登場以来、どこにどう使うべきか?という部分についての議論・検討が行われ、まだまだ一般化していないように思います。
私自身も実は会社の業務としてNoSQLを採用したプロジェクトに関わった事はありません・・・

まあ、それはさておき(また、本投稿はNoSQLの本質を議論する趣旨ではないので・・・)、今回は以下のことを試してみたので、それらをまとめておこうと思います。

  • 「Azure DocumentDB Emulator」をインストールしてみた
  • 「.NET Core Consoleアプリ」から「Azure DocumentDB Emulator」に接続してデータ操作してみた
  • 「Azure DocumentDB(クラウド)」から「Azure DocumentDB Emulator(ローカル)」にデータをエクスポート(リストア)してみた
  • データのコンソール出力にはSerilogを使った

「Azure DocumentDB Emulator」のインストール

さあ、今回の主役である「Azure DocumentDB Emulator」をインストールします。
この「Azure DocumentDB Emulator」は、2016/11にリリースされました。
これは クラウドのAzure DocumentDB をローカルPC上でエミュレートするソフトウェアです。オフライン状態でも、Azure DocumentDB開発が可能になります。
以下のリンクページ 上部の「Download the Emulator」をクリックするとインストーラをダウンロードする事が出来ます。
(ちなみにこれはWindows版のみでMac版は残念ながらありません)

docs.microsoft.com

ダウンロードした msi ファイルは、ダブルクリックし「Next」や「OK」や、指示通りにクリックしていればインストール完了します。
インストールが完了すると、自動的に起動し、タスクトレイに以下のようなアイコンが表示されます(スタートメニューから「DocumentDB Emulator」を選択しても明示的に起動する事が出来ます)。

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

また、このアイコンをマウス右クリックすると以下のようなメニューが表示されます。

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

「Open Data Explorer...」をクリックすると以下のような管理画面が表示されます(インストール直後は、この画面も自動で表示されるはずです)。

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

見てわかるように「https://localhost:8081/_explorer/index.html#」という8081ポートでホストされた管理コンソール画面となります。
「.NET / .NET Core / Java...」等のタブが有り、ここに表示されている「Code Samples」をクリックすると、何も考えなくても「ビルド→実行」が可能なサンプルプロジェクトをダウンロード可能です。
画面上部の「Explorer」タブをクリックすると、DocumentDB Emulatorに保存されているデータベース内容を確認する事が出来ます(以下の画面)。
初期状態では 空 になります。

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

「.NET Core Consoleアプリ」+「Azure DocumentDB Emulator」でデータ保存・読み込み

では、せっかくなので最新の Visual Studio 2017 RC + .NET Core を使って DocumentDB Emulator に接続してみます。

①プロジェクトの作成

Visual Studio 2017 RCを起動し、メニュー「ファイル→新規作成→プロジェクト」を選択します。

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

ここではシンプルに、テンプレートとして「Console App(.NET Core)」を選択し、プロジェクト名は「DocDbExampleCoreConsole」としました。

②Nuget参照の追加

次にNugetで必要なライブラリへの参照を追加します。
メニュー「プロジェクト→Nuget パッケージの管理」を選択します。
まず必要なのは「Microsoft.Azure.DocumentDB.Core」になります。

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

さらに、これは本投稿の本質ではありませんが、コンソールへのデータログ出力用に「Serilog」および、そのコンソールSinkである「Serilog.Sinks.Console」も追加します。

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

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

③モデルクラスの作成

DocumentDBに保存するモデルクラスを作成します。
ブログの投稿内容を想定したモデルを作成することとします。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPost.cs」とします。

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

実装は以下のとおりであり、ブログ投稿を表す「BlogPostクラス」、ブログ投稿者を表す「Userクラス」、ブログ投稿へのコメントを表す「Commentクラス」から構成されます。

// BlogPost.cs
using System.Collections.Generic;
using Newtonsoft.Json;

namespace DocDbExampleCoreConsole
{
  /// <summary>
  /// ブログポスト(1投稿)を表すクラスです。
  /// </summary>
  public class BlogPost
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "title")]
    public string Title { get; set; }

    [JsonProperty(PropertyName = "contents")]
    public string Contents { get; set; }

    [JsonProperty(PropertyName = "author")]
    public User Author { get; set; }

    [JsonProperty(PropertyName = "comments")]
    public List<Comment> Comments { get; set; }
  }

  /// <summary>
  /// ブログ投稿者を表すクラスです。
  /// </summary>
  public class User
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "name")]
    public string Name { get; set; }
  }

  /// <summary>
  /// ブログ投稿へのコメントを表すクラスです。
  /// </summary>
  public class Comment
  {
    [JsonProperty(PropertyName = "id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "text")]
    public string Text { get; set; }
  }
}

リポジトリクラスの作成

モデルクラスをDocumentDBに出し入れするためのデータベースアクセス用のリポジトリクラスを作成します。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPostRepository.cs」とします。

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

実装は以下の通りです。

// BlogPostRepository.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;

namespace DocDbExampleCoreConsole
{
  public class BlogPostRepository
  {
    /// <summary>
    /// 任意のデータベースID
    /// </summary>
    private static readonly string DatabaseId = "Blog";

    /// <summary>
    /// 任意のコレクションID
    /// </summary>
    private static readonly string CollectionId = "BlogPost";

    /// <summary>
    /// エンドポイント
    /// </summary>
    private static readonly string EndPoint = "https://localhost:8081/";

    /// <summary>
    /// 認証キー(固定)
    /// </summary>
    private static readonly string AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

    private static DocumentClient client;

    /// <summary>
    /// ブログポストデータを作成します。
    /// </summary>
    /// <param name="blogPost"></param>
    /// <returns></returns>
    public static async Task<Document> CreateBlobPostAsync(BlogPost blogPost)
    {
      return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), blogPost);
    }

    /// <summary>
    /// すべてのブログポストデータを取得します。
    /// </summary>
    /// <returns></returns>
    public static async Task<IEnumerable<BlogPost>> GetAllBlogPostsAsync()
    {
      IDocumentQuery<BlogPost> query = client.CreateDocumentQuery<BlogPost>(
        UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        new FeedOptions { MaxItemCount = -1 })
        .AsDocumentQuery();

      List<BlogPost> results = new List<BlogPost>();
      while (query.HasMoreResults)
      {
        results.AddRange(await query.ExecuteNextAsync<BlogPost>());
      }

      return results;
    }

    /// <summary>
    /// データベース・コレクションの初期化を行います。
    /// </summary>
    public static void Initialize()
    {
      client = new DocumentClient(new Uri(EndPoint), AuthKey, new ConnectionPolicy { EnableEndpointDiscovery = false });
      CreateDatabaseIfNotExistsAsync().Wait();
      CreateCollectionIfNotExistsAsync().Wait();
    }
    
    /// <summary>
    /// 存在しなければデータベースを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateDatabaseIfNotExistsAsync()
    {
      try
      {
        await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDatabaseAsync(new Database { Id = DatabaseId });
        }
        else
        {
          throw;
        }
      }
    }

    /// <summary>
    /// 存在しなければコレクションを作成します。
    /// </summary>
    /// <returns></returns>
    private static async Task CreateCollectionIfNotExistsAsync()
    {
      try
      {
        await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId));
      }
      catch (DocumentClientException e)
      {
        if (e.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
          await client.CreateDocumentCollectionAsync(
            UriFactory.CreateDatabaseUri(DatabaseId),
            new DocumentCollection { Id = CollectionId },
            new RequestOptions { OfferThroughput = 1000 });
        }
        else
        {
          throw;
        }
      }
    }
  }
}

リポジトリクラス実装のポイントは以下の通りです。

  • DatabaseId / CollectionId
    これらは任意の名称をつけます。ここでは「Blog」「BlogPost」としました。

  • EndPoint
    エンドポイントはデフォルトでは「https://localhost:8081/」となります。

  • AuthKey
    Azure DocumentDB Emulator では認証キーは固定で「C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==」と決められています。

  • Microsoft.Azure.Documents.Client.DocumentClient
    DocumentDBへの接続クライアント(繋ぎ役)は「DocumentClientクラス」となります。本サンプルでは Initialize() メソッド内で初期化を行っています。

  • Initialize() / CreateDatabaseIfNotExistsAsync() / CreateCollectionIfNotExistsAsync()
    データベースは、データ操作を行う前に初期作成を行う必要があります(RDBでいうところのCREATE DATABASE / CREATE TABLE)。これに該当する処理をCreateDatabaseIfNotExistsAsync() / CreateCollectionIfNotExistsAsync()で実装しています。両メソッドは Initialize() から呼び出されています。つまり、初期化処理として BlogPostRepository.Initialize() を呼び出す必要があります。

  • CreateBlobPostAsync(BlogPost blogPost)
    ブログポスト(ドキュメント)をデータベース コレクション」に追加します(RDBのINSERT)。

  • GetAllBlogPostsAsync()
    登録されたすべてのブログポスト(ドキュメント)を取得しています(RDBのSELECT)。

⑤main()の実装(リポジトリクラスの呼び出し)

用意したリポジトリクラス(BlogPostRepository)を利用して、BlogPostモデルクラスのデータベースへの出し入れ処理を実装します。
以下がコンソールアプリケーションのmain処理の実装になります。

// Program.cs
using System.Threading.Tasks;
using System.Collections.Generic;

using Serilog;

namespace DocDbExampleCoreConsole
{
  class Program
  {

    private IEnumerable<BlogPost> blogPosts = null;

    static void Main(string[] args)
    {
      Program program = new Program();

      // データベースを初期化
      program.InitializeDatabase();

      // データを挿入
      program.InitializeData().Wait();

      // データを抽出
      program.GetAllBlogPosts().Wait();

      // 抽出したデータをコンソール出力
      Log.Logger = new LoggerConfiguration()
        .WriteTo.Console()
        .CreateLogger();
      foreach (var blogPost in program.blogPosts)
      {
        Log.Information("{@BlogPost}", blogPost);
      }

      System.Console.ReadLine();
    }

    /// <summary>
    /// データベース・コレクションを初期化します。
    /// </summary>
    private void InitializeDatabase()
    {
      // データベース・コレクションを作成
      BlogPostRepository.Initialize();
    }

    /// <summary>
    /// BlogPostサンプルデータを作成します。
    /// </summary>
    /// <returns></returns>
    private async Task InitializeData()
    {
      // 50件、テスト投稿を作成
      for (int i = 0; i < 50; i++)
      {
        BlogPost blogPost = new BlogPost() {
          Title = string.Format("Title {0}", i),
          Contents = string.Format("Hi! This contents no is '{0}'.", i),
          Author = new User() { Name = "nanasi san" },
          };
        blogPost.Comments = new List<Comment>();
        blogPost.Comments.Add(new Comment() { Text = "Good job!!" });

        await BlogPostRepository.CreateBlobPostAsync(blogPost);
      }
    }

    /// <summary>
    /// すべてのBlogPostデータを取得します。
    /// </summary>
    /// <returns></returns>
    private async Task GetAllBlogPosts()
    {
      this.blogPosts = await BlogPostRepository.GetAllBlogPostsAsync();
    }
  }
}

mainクラス実装のポイントは以下の通りです。

  • program.InitializeDatabase();
    リポジトリークラスの呼び出しにより、データベース・コレクションを作成しています。

  • program.InitializeData().Wait();
    リポジトリークラスの呼び出しにより、BlogPostサンプルデータを50件作成しています。

  • program.GetAllBlogPosts().Wait();
    リポジトリークラスの呼び出しにより、データベースコレクションに登録された全てのBlogPostデータ抽出しています。

  • ログ出力
    抽出の結果として得られたデータは「IEnumerable<BlogPost>」型です。
    個別データである BlogPost オブジェクトをSerilogでコンソール出力しています。
    Serilogを使用する理由は、オブジェクトのログ出力に際し、子プロパティを含めた形で自動でJSON形式出力してくれる機能を活用する為です。

以下がDocDbExampleCoreConsoleコンソールアプリケーションの実行結果となります。

2016-12-08 02:37:27 [Information] BlogPost { Id: "43eb6293-c532-46dd-969d-e6e4258bcd7d", Title: "Title 0", Contents: "Hi・・This contents no is '0'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "86380c10-c45e-46e5-9320-a432be8e838f", Title: "Title 1", Contents: "Hi・・This contents no is '1'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "77350d85-6e11-49ac-9f52-d14729cb41b4", Title: "Title 2", Contents: "Hi・・This contents no is '2'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "e151fef2-74d8-4735-a89a-971603169c36", Title: "Title 3", Contents: "Hi・・This contents no is '3'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "580291de-c99a-4893-b416-48954847413a", Title: "Title 4", Contents: "Hi・・This contents no is '4'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
2016-12-08 02:37:27 [Information] BlogPost { Id: "a80815e6-5d03-49e9-b12e-a2892f504dbd", Title: "Title 5", Contents: "Hi・・This contents no is '5'.", Author: User { Id: null, Name: "nanasi san" }, Comments: [Comment { Id: null, Text: "Good job!!" }] }
...以下省略

⑥「Azure DocumentDB Emulator」のExplorerからデータを確認

「Azure DocumentDB Emulator」の管理画面である「https://localhost:8081/_explorer/index.html#」から投入されたBlogPostデータを参照してみます。
以下の画面のようにデータを確認する事が出来ます。

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

ちなみにタスクトレイアイコン マウス右ボタン「Reset Data...」をクリックすると、簡単にきれいさっぱりデータが消去されるのでお気を付けください。

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

「Azure DocumentDB」から「Azure DocumentDB Emulator」にデータを(エクスポート)リストア

最後にクラウド上の Azure DocumentDB から Azure DocumentDB Emulator にデータリストアする手順を説明しておきます。

「本来の実装はクラウドのAzure DocumentDBで行っているが、出先等で一時的にローカルエミュレータ開発をしたい」
「今までEmulatorが無かったから、常にクラウドに接続していたけれど、クラウドエミュレータを平行利用開発したい」

等々の場合に有効かと・・・

以下のページから「Azure DocumentDB Data Migration Tool」をダウンロードします。

Download Azure DocumentDB Data Migration Tool from Official Microsoft Download Center

ダウンロードしたzipファイルを任意のフォルダに解凍し dtui.exe を起動します。

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

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

表示された以下のウィンドウにて、Import from は「DocumentDB」、ConnectionString は「Azure上のDocumentDBへの接続文字列(Database=xxx も忘れずに)」、Collection は「インポート対象のコレクション名」を入力して「Nextボタン」をクリックします。

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

以下の画面において、export先(つまり、ここではローカルの Azure DocumentDB Emulator)の設定を行います。
ConnectionString は、EndPoint=「https://localhost:8081」・AccountLey=「固定値」Database=「任意(ここではBlog)」となります。
入力したら「Nextボタン」をクリックします。

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

以下のウィンドウが表示されたら「Next」ボタンをクリックします(必要であれば「エラーログファイル」の設定も行って)。

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

設定内容の最終確認を行う以下の画面が表示されいます。
問題なければ「Next」ボタンをクリックします。

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

以上で「クラウド Azure DocumentDB」→「ローカル Azure DocumentDB Emulator」へのデータのリストアが完了しました。

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

まとめ

NoSQLが登場したのはいつだったでしょうか・・・RedisやCassandra、memcashedに興奮し、迷走し・・・Azure黎明期にも当時のNoSQL(DoucmentDBという名称ではなかったと思う・・・)に興奮した記憶があります。
ビジネスアプリケーションにおいては、まだまだNoSQLの導入は遅れているように思います。同時にNoSQLがRDBにとって代わる技術では無い事も事実です。
RDBの堅牢なデータ管理とNoSQLのパフォーマンスを適材適所に組み合わせたシステムの開発、という提案を開発者として行っていきたいものです。

で、まあ新しい技術への探求は相変わらず楽しいのですよね^^