Azure DocumentDB Emulatorを使ってみた。で、.NET Coreから操作した話。
技術者としての尊敬の対象である Scott Hanselman 氏のブログで「Azure DocumentDB Emulator」についての記事が書かれていたので、自分でも使ってみました。
こんなことをやった
「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版は残念ながらありません)
ダウンロードした msi ファイルは、ダブルクリックし「Next」や「OK」や、指示通りにクリックしていればインストール完了します。
インストールが完了すると、自動的に起動し、タスクトレイに以下のようなアイコンが表示されます(スタートメニューから「DocumentDB Emulator」を選択しても明示的に起動する事が出来ます)。
また、このアイコンをマウス右クリックすると以下のようなメニューが表示されます。
「Open Data Explorer...」をクリックすると以下のような管理画面が表示されます(インストール直後は、この画面も自動で表示されるはずです)。
見てわかるように「https://localhost:8081/_explorer/index.html#」という8081ポートでホストされた管理コンソール画面となります。
「.NET / .NET Core / Java...」等のタブが有り、ここに表示されている「Code Samples」をクリックすると、何も考えなくても「ビルド→実行」が可能なサンプルプロジェクトをダウンロード可能です。
画面上部の「Explorer」タブをクリックすると、DocumentDB Emulatorに保存されているデータベース内容を確認する事が出来ます(以下の画面)。
初期状態では 空 になります。
「.NET Core Consoleアプリ」+「Azure DocumentDB Emulator」でデータ保存・読み込み
では、せっかくなので最新の Visual Studio 2017 RC + .NET Core を使って DocumentDB Emulator に接続してみます。
①プロジェクトの作成
Visual Studio 2017 RCを起動し、メニュー「ファイル→新規作成→プロジェクト」を選択します。
ここではシンプルに、テンプレートとして「Console App(.NET Core)」を選択し、プロジェクト名は「DocDbExampleCoreConsole」としました。
②Nuget参照の追加
次にNugetで必要なライブラリへの参照を追加します。
メニュー「プロジェクト→Nuget パッケージの管理」を選択します。
まず必要なのは「Microsoft.Azure.DocumentDB.Core」になります。
さらに、これは本投稿の本質ではありませんが、コンソールへのデータログ出力用に「Serilog」および、そのコンソールSinkである「Serilog.Sinks.Console」も追加します。
③モデルクラスの作成
DocumentDBに保存するモデルクラスを作成します。
ブログの投稿内容を想定したモデルを作成することとします。
メニュー「プロジェクト→クラスの追加」を選択し、表示された「新しい項目の追加」ウィンドウで「コード→クラス」を選択し、名前は「BlogPost.cs」とします。
実装は以下のとおりであり、ブログ投稿を表す「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」とします。
実装は以下の通りです。
// 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データを参照してみます。
以下の画面のようにデータを確認する事が出来ます。
ちなみにタスクトレイアイコン マウス右ボタン「Reset Data...」をクリックすると、簡単にきれいさっぱりデータが消去されるのでお気を付けください。
「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 を起動します。
「Nextボタン」をクリックします。
表示された以下のウィンドウにて、Import from は「DocumentDB」、ConnectionString は「Azure上のDocumentDBへの接続文字列(Database=xxx も忘れずに)」、Collection は「インポート対象のコレクション名」を入力して「Nextボタン」をクリックします。
以下の画面において、export先(つまり、ここではローカルの Azure DocumentDB Emulator)の設定を行います。
ConnectionString は、EndPoint=「https://localhost:8081」・AccountLey=「固定値」Database=「任意(ここではBlog)」となります。
入力したら「Nextボタン」をクリックします。
以下のウィンドウが表示されたら「Next」ボタンをクリックします(必要であれば「エラーログファイル」の設定も行って)。
設定内容の最終確認を行う以下の画面が表示されいます。
問題なければ「Next」ボタンをクリックします。
以上で「クラウド Azure DocumentDB」→「ローカル Azure DocumentDB Emulator」へのデータのリストアが完了しました。
まとめ
NoSQLが登場したのはいつだったでしょうか・・・RedisやCassandra、memcashedに興奮し、迷走し・・・Azure黎明期にも当時のNoSQL(DoucmentDBという名称ではなかったと思う・・・)に興奮した記憶があります。
ビジネスアプリケーションにおいては、まだまだNoSQLの導入は遅れているように思います。同時にNoSQLがRDBにとって代わる技術では無い事も事実です。
RDBの堅牢なデータ管理とNoSQLのパフォーマンスを適材適所に組み合わせたシステムの開発、という提案を開発者として行っていきたいものです。
で、まあ新しい技術への探求は相変わらず楽しいのですよね^^