RavenDB の特徴と使い方 (プログラミング)

最近 はまっている RavenDB について書いておこうと思う。

RavenDB は、軽量なドキュメント データベース (NoSQL) で、MongoDB などを使っている人は、似た概念のものと思ってもらって良い。(もちろん、細かな点は違うけど。) 構造上、「速い」というのはもちろんだが、その特徴として、.NET との親和性が良く、.NET アプリケーションに埋め込めるという点がある。(ASP.NET MVC の開発者にとっては、超うれしい。)
また、実際に使ってみるとわかるが、そうした簡単な表現では足りないくらい、さまざまなメリットと特徴があるので、今日は、その辺りを、伝えられる限り書いておこうと思う。(海外の一部のマニア達の間では、流行ってるみたいだ。。。)

インストールとデータベースの準備

まず、構成をちゃんと理解してもらうために、インストール方法から書いておこうと思う。

RavenDB には、.NET のアプリケーション (ASP.NET 含む) に埋め込んで使う方法と、 HTTP にホスト (IIS に配置, もしくは サーバーのバイナリを起動) して使用する方法がある。(HTTP ホストについては、「RavenDB の Replication, Scale Out」に記載した。なお、Multiple Database の構成など、HTTP にホストしないと使えないものもあるので要注意。)
今回は、アプリケーションに埋め込んで使用するが、この場合は、NuGet からインストールできる。

install-package RavenDB.Embedded

Attempting to resolve dependency 'RavenDB.Database (= 1.0.888)'.
Attempting to resolve dependency 'Newtonsoft.Json (= 4.0.8)'.
Attempting to resolve dependency 'NLog (= 2.0.0.2000)'.
Attempting to resolve dependency 'RavenDB.Client (= 1.0.888)'.
Successfully installed 'Newtonsoft.Json 4.0.8'.
Successfully installed 'NLog 2.0.0.2000'.
Successfully installed 'RavenDB.Database 1.0.888'.
Successfully installed 'RavenDB.Client 1.0.888'.
Successfully installed 'RavenDB.Embedded 1.0.888'.
Successfully added 'Newtonsoft.Json 4.0.8' to ConsoleApplication5.
Successfully added 'NLog 2.0.0.2000' to ConsoleApplication5.
Successfully added 'RavenDB.Database 1.0.888' to ConsoleApplication5.
Successfully added 'RavenDB.Client 1.0.888' to ConsoleApplication5.
Successfully added 'RavenDB.Embedded 1.0.888' to ConsoleApplication5.

この段階では、まだデータベース ファイル等は生成されず、必要な dll の配置と参照設定が追加されるのみだ。

なお、先日リリースされた ASP.NET MVC 4 RC 版と一緒に使用する場合は、まだ RavenDB で使用しているライブラリーのバージョンと競合するため (Microsoft.AspNet.WebApi パッケージで使用しているライブラリーのバージョンと競合するため)、下記の通り、PreRelease 版の RavenDB を入れておく。(2012 年 06 月現在)

get-package -l -filter RavenDB.Embedded -pre

Id                             Version
--                             -------
RavenDB.Embedded               1.2.2010-Unstable

install-package RavenDB.Embedded -version 1.2.2010-Unstable -pre

ここでは説明しないが、もちろん、Import、Export、Backup など、データベース管理における一般的なタスクも実行できる。(HTTP ホストの場合、RavenDB Management Studio を使って簡単に実行できる。)

基本的な使い方

「軽量」(light-weight) と記載したが、では、どんな感じで light なのか見てみよう。

RavenDB を使用するには、RavenDB Client (Client 用のライブラリー) を使用するが、 IIS にホストしている場合は、REST (Web API, HTTP API) として使用することもできる。(.NET プログラマーの方は、ちょうど、WCF Data Services のような使い方だと思って良い。)

データベース (データ、インデクス等) はファイルとして作成されるが、これらの必要なファイルは実行時に作成される。(一度作成されたら、以降は、作成されたデータベース ファイルを使用する。)
データベース用のディレクトリのみを準備しておき、このディレクトリを指定して RavenDB を初期化することで、必要なデータベース ファイルが作成される。特にそれ以上の特別な準備は不要で、Entity Framework、Hibernate などの OR Mapper (ORM) 同様、簡単なコードでデータベースの Provisioning が完了する。

また、データ構造やスキーマ定義も不要で、.NET インスタンスを保存すると、内部で Json 形式にシリアライズされるため、シリアライズ可能なオブジェクトであれば何でも保存できる。

例えば、下記は、RavenDB Client を使って、Order クラスのインスタンスを保存する簡単なサンプル コードだ。(今回は、アプリケーションの実行ディレクトリの下に「Database」という名前のサブ ディレクトリを作成し、ここを使用する。)

using Raven.Client;
using Raven.Client.Embedded;

static void Main(string[] args)
{
  using (IDocumentStore instance =
    new EmbeddableDocumentStore
    {
      DataDirectory=@"~\Database"
    })
  {
    // all db files are created, here !
    instance.Initialize();

    using (IDocumentSession session
      = instance.OpenSession())
    {
      Order o1 = new Order
      {
        Name = "test1",
        Price = 100,
        Category = "material"
      };
      session.Store(o1);
      session.SaveChanges();
      //session.Dispose();
    }

    Console.WriteLine("Done !");
    Console.ReadLine();
    //instance.Dispose();
  }
}

public class Order
{
  public string Name { get; set; }
  public int Price { get; set; }
  public string Category { get; set; }
}

上記の Initialize() メソッドの実行によって、データベース関連の一連のファイルが作成される。(これらのファイルを消せば、データベースは、また初期の状態で再作成される。いたってシンプルだ。)

接続の際は、下記のように、構成ファイル (.config) に接続情報を記述しても良い。

<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <add name="RavenDB" connectionString="DataDir = ~\Database" />
  </connectionStrings>
</configuration>

上記の接続文字列 (RavenDB) を使ってデータベースを初期化する際は、下記の通り記述する。

static void Main(string[] args)
{
  using (IDocumentStore instance =
    new EmbeddableDocumentStore
    {
      ConnectionStringName = "RavenDB"
    })
  {
    . . .

なお、RavenDb を HTTP にホスト (配置) している場合は、下記のように DocumentStore を使って接続する。(この場合も、同様に、接続先の情報を .config に記述しても良い。)

using Raven.Client.Document;

var ds = new DocumentStore
  { Url = "http://testsite/Raven" };

Key-Value

RavenDB は NoSQL であり、Key-Value を採用している。データは Json フォーマットで保存されるが、内部で、自動的に、Id (identifier) が Key として付与される。(これが、取得の際の Key として使用される。)
つまり、データは、Id (identifier) という文字列 (Key) と Json ドキュメント (Value) の Key-Value として格納されている。
例えば、下記は、Id が「orders/1」のインスタンスを取得している。(当然だが、速い。)

Order res;
using (IDocumentSession session = instance.OpenSession())
{
  res = session.Load<Order>("orders/1");
}

Id は、プログラマーが指定しない場合、自動的に <class name>/<sequence number> となる。<class name> には、クラス名の小文字の名前を複数形にした名前が設定される。例えば、「Order」クラスなら「orders」となる。つまり、上述したアイテムの新規登録のコードの場合、Id (Key) は「orders/1」となる。
また、Id は、プログラマーが明示的に指定することも可能だ。

session.Store(o1);    // id is "orders/1"
session.Store(o1, "id1"); // id is "id1"

また、シリアライズ対象の .NET クラスに Id という名前の文字列型のメンバーがある場合、これが自動的に Id として Key に割り当てられる。 例えば、下記のコードの場合、Id が同一のため、o2 によって、o1 が変更 (update) される。(つまり、登録されるデータは 1 件のみ。)

static void Main(string[] args)
{
  ...

  using (IDocumentSession session
    = ds.OpenSession())
  {
    Order o1 = new Order
    {
      Id = "id1",
      Name = "test1",
      Price = 100,
      Category = "material"
    };
    session.Store(o1);
    session.SaveChanges();
  }

  using (IDocumentSession session
    = ds.OpenSession())
  {
    Order o2 = new Order
    {
      Id = "id1",
      Name = "test1",
      Price = 200,
      Category = "food"
    };
    session.Store(o2);
    session.SaveChanges();
  }
  ...
}

public class Order
{
  public string Id { get; set; }
  public string Name { get; set; }
  public int Price { get; set; }
  public string Category { get; set; }
}

しかし、下記のコードでは、Id プロパティが指定されていないため、o1、o2 の 2 件のデータが作成 (Create) される。(Id には、「orders/1」、「orders/2」が付与される。)

static void Main(string[] args)
{
  ...

  using (IDocumentSession session
    = ds.OpenSession())
  {
    Order o1 = new Order
    {
      Name = "test1",
      Price = 100,
      Category = "material"
    };
    session.Store(o1);
    session.SaveChanges();
  }

  using (IDocumentSession session
    = ds.OpenSession())
  {
    Order o2 = new Order
    {
      Name = "test1",
      Price = 200,
      Category = "food"
    };
    session.Store(o2);
    session.SaveChanges();
  }
  ...
}

public class Order
{
  public string Name { get; set; }
  public int Price { get; set; }
  public string Category { get; set; }
}

なお、クラスの Id プロパティ (メンバー) を空にして登録 (Store) すると、登録時に、自動的に割り当てられた Id がインスタンス (インスタンスの Id メンバー) に設定される。

この Id だが、RavenDB が IIS にホストされている場合は Uri の断片そのものなのでわかりやすいが、 Web アプリケーションに埋め込む場合には、Query String で使用する際に邪魔になることがある。例えば、orders/1 という Id のアイテムを GET する場合、下記の URL はエラーとなってしまうだろう。

GET /webapplication/Order/?id=orders/1

この場合、MSDN マガジン に書かれているように、 IdentityPartsSeparator プロパティを使って、Id で使用する Separator を変更できる。

using (IDocumentStore instance =
  new EmbeddableDocumentStore
  {
    ConnectionStringName = "RavenDB"
  })
{
  instance.Conventions.IdentityPartsSeparator = "-";
  instance.Initialize();
  ...

Query と Index

この手のデータベースで、いつも困るのが検索だ。
Key-Value の場合、構造上、何かと融通が効かないことが多いが、RavenDB では、高度な Index 管理をサポートすることで、こうした pain を回避している。Index と言っても、RDB の Index とは考え方が異っているので、以下に記載する。(ドキュメント データベースなので、全文検索用の Index の概念だ。)

まず、RavenDB では、Linq の Query を使って以下のように書ける。

var test = from c in session.Query<Order>()
      where c.Name == "test1"
      select c;
foreach (var item in test)
{
  Console.WriteLine("{0} : {1}", item.Name, item.Price);
}

上記では Key (Id) が使えないため、登録されているデータの Name を 1 つ 1 つ調べて答えを返しているように思えるが、 この手のデータベースでは、「データ全件を調べる」ということはしない。(厳密には、LuceneQuery という Index ファイルをそのまま検索すると、全件検査を実行できてしまうが。。。)
では、どのように動いているのだろうか ?

実は、上記のような検索をおこなうと、内部で、動的に Index が作成されて、その Index が使用される。(これは、dynamic index と呼ばれている。ちなみに、前述の Id で検索した場合であっても、上記のような Linq Query を使うと、それに応じた Index が必ず作成される。) また、作成された Index は、しばらく残り、同じ Index を使用する別の検索がおこなわれると、その Index が再利用される。最終的に、何度も同じ Index を使用すると、RavenDB によって、dynamics index は永続化される。(以降、ずっと残る。)
つまり、アプリケーション側で同じ使い方をしていると、そのアプリケーションに最適化された Index が自動的に生成され、永続化されて、使用されるようになる。

こうした仕組みのため、dynamic index を使う場合は、初回の検索のみ遅くなるので注意が必要。また、こうした仕組みのため、EUC による動的検索など、都度、検索文 (SQL) を動的生成するようなアプリケーションにも向いていない。

なお、作成された Index は、下記のコマンドで取得できるので、観察してみるとわかる。(dynamic index が permanent に昇格されたかどうかも、この名前で確認できる。) Index の明示的な削除も可能だ。

string[] indexes = instance.DatabaseCommands.GetIndexNames(
  0,
  int.MaxValue);
foreach (var indexname in indexes)
{
  Console.WriteLine("Index : {0}", indexname);
}

さて、ここまでの説明だと、RDB の Index を想像する人も多いと思うが、実は全然違う。
以下に、この Index の正体をもう少し細かく見てみよう。

Index は、上記 (dynamic index) のように動的に作成することもできるが、プログラマー自身が Index を作成し、これを使用できる。(この Index を static index と呼ぶ。) このため、以降では、この方法で Index を作成して見てみよう。

上記と同じ Name を使った検索を、static index を使って書くと、以下の通りになる。

using Raven.Client.Indexes;

static void Main(string[] args)
{
  ...

  // all db files are created, here !
  instance.Initialize();

  // create index !
  instance.DatabaseCommands.PutIndex(
    "Orders/ByName",
    new IndexDefinitionBuilder<Order>
    {
      Map = (orders => from order in orders
                select new { order.Name })
    });

  . . .

  using (IDocumentSession session = instance.OpenSession())
  {
    // using static index !
    var test = from c in
            session.Query<Order>("Orders/ByName")
          where c.Name == "test1"
          select c;
    foreach (var item in test)
    {
      Console.WriteLine("{0} : {1}", item.Name, item.Price);
    }
  }
  . . .

}

ちなみに、上記で、正しい検索結果にならない場合は、数秒待機してから検索 (Query) してみてほしい。理由は後述する。

この Index のメカニズムを簡単に解説する。RavenDB の Index は、実は、内部では、全文検索 (Full Text Search) エンジンの Lucene.Net が使用されている。上記の Map 関数により、Lucene.NET に登録する Document のフィールドが設定される。(このフィールドを使って、検索可能になる。) そして、検索の際には、Lucene.Net に登録されている Index を使って Document を検索する。
既定では、Map 関数 (上記) で抽出された文字列型のフィールドをそのまま Token として登録するが、いわゆる全文検索エンジンのメリットを活用して、Token 解析を別のものに変更することも可能だ。 例えば、下記では、Name を空白 (whitespace) で Token 分割して Lucene.Net に登録し、 この分割された Token を使って検索 (Query) 可能にしている。この場合、例えば、Name が「Ballpoint pen」だった場合、「pen」で検索しても抽出されるようになる。

using Lucene.Net.Analysis;
. . .

instance.DatabaseCommands.PutIndex(
  "Orders/ByName",
  new IndexDefinitionBuilder<Order>
  {
    Map = (orders => from order in orders
              select new { order.Name }),
    Analyzers =
    {
      {
        orders => orders.Name,
        typeof(WhitespaceAnalyzer).FullName
      }
    }
  });

この Map の関数は、個々のデータごとにそれぞれ独立して処理できるため、 複数のスレッド (タスク) によって分散して Index 生成の処理をおこない、高速化できる。

また、例えば、「Order に設定されている Category を集計し、各 Category と登録されている Order の個数を出力する」といった複雑な検索の場合には、下記のように Map と Reduce を組み合わせることができる。

static void Main(string[] args)
{
  . . .

  // create index
  instance.DatabaseCommands.PutIndex(
    "Orders/ByCategoryCount",
    new IndexDefinitionBuilder<Order, CategoryCount>
    {
      Map = (orders => from order in orders
                select new CategoryCount()
                {
                  Category = order.Category,
                  Count = 1
                }),
      Reduce = (results => from result in results
                  group result by result.Category
                  into g
                  select new
                  {
                    Category = g.Key,
                    Count = g.Sum(x => x.Count)
                  })
    });

  . . .

  using (IDocumentSession session = instance.OpenSession())
  {
    var test = (from c in
            session.Query<CategoryCount>("Orders/ByCategoryCount")
          where c.Category == "material"
          select c).FirstOrDefault();
    Console.WriteLine("{0} : {1}",
      test.Category,
      test.Count);
  }
}

public class CategoryCount
{
  public string Category { get; set; }
  public int Count { get; set; }
}

Reduce は、Map で作成した結果を集約する関数だ。Reduce 関数では、Map で作成された結果をグループ化し、グループごとに独立して処理できる。また、その結果を さらにグループ化し、再度、独立して処理をおこなう。そして、これを繰り返す。つまり、この処理も、複数スレッドで分散して効率的に集約処理を実行できる。 (なお、Reduce は、このように再帰的に処理されるため、入力と出力の型は同じになっている点に注意してほしい。)

このように、RavenDB の Index は、Map Reduce などのタスクを登録し、 この登録されたタスクが作成する結果のビューを使って処理をおこなうイメージだ。(ただし、基本的に、単一マシン、複数スレッドでの実行なので注意してほしい。マシン分割を検討する場合は、Sharding を使うことになる。)

なお、こうした仕組みのため、いくつかの注意点もある。 例えば、この Map Reduce の処理 (タスク) は、検索処理と無関係にバックグラウンドで実行されるため、検索結果が Stale の状態 (つまり、古い Index の状態) になっている場合があるので注意する。こうした場合、上述したように数秒待ってみるか、あるいは、ここでは説明を省略するが、プログラムで Stale かどうかの確認が可能なので、こうした処理をまめに入れておいてほしい。 また、RDB のような Contains を使った検索 (前方一致以外の部分文字列検索) もできない。理由は、上記を見てもらえば明白だろう。ただし、上記のように Analyzer を変更することで、意味的に Token 分割をおこない、Token 単位で検索することはできる。
要は、RavenDB の Index は、OLTP を得意とする RDB のような使い方ではなく、あくまでも「ドキュメント」を扱うのに適した Full Text Search の Index であることを理解しておくと良い。まあ、普通の使い方をしていれば、そのアプリケーションのために最適化されたデータベースとして動作するのだが、こうした内部の動きを理解しておく必要はあるということだ。

なお、Index 作成は background スレッドで実行されるため、Indexing の際のエラーは、プログラムから取得するか (/stats)、HTTP ホストの場合は、RavenDB Management Studio を使用して確認する。
また、Index であまりエラーが頻発する場合、RavenDB が Index を Disable にしてしまう場合がある。その場合、Index を消すか、Index Definition そのものを変更するしかない。
ちなみに、Index は、再構成 (ResetIndex) も可能だ。

その他 (light な世界の、light な制御)

この他に、ここでは説明を省略するが、RavenDBは、階層構造 (Indexing Hierarchical Data) なども高速に扱うことができる。
また、RavenDB は、もちろん、Pessimistic ではなく、Optimistic な制御モデルを採用している。ETag を使った楽観同時実行制御 (Optimistic Concurrency Control) のための仕組みも備わっている。
また、RavenDB を IIS にホストする場合、オブジェクト同士が参照関係にある場合に、1 回の REST 呼び出しで関係するオブジェクトを取得できる。(つまり、関係するオブジェクトの Pre-fetch が可能。)

ここでは詳細の説明を省略するが、”ドキュメント データベースらしさ” は Index だけではないので、いろいろ触ってみるとおもしろい。

ASP.NET との Integration (ASP.NET MVC, ASP.NET Web API)

上記の通り、軽量、柔軟、かつアプリケーションに近いデータベースのため、ASP.NET MVC などの RESTful で軽量なアプリケーション フレームワークとの相性は良い。(MSDN マガジンの記事 では、まさにこの内容について解説されている。) まあ、要は、組み合わせて、アプリケーションのリポジトリーとして使うだけだが、上記のセパレーター (IdentityPartsSeparator プロパティ) の話以外にも、いくつか注意点があるので、最後に記載しておく。

まず、データベースの Initialize (上記の Initialize() メソッド) は、時間がかかるので注意してほしい。 特に、ASP.NET MVC では、stateless に実装することが多いので、Initialize は Application_Start などで実行し、取得したデータベース オブジェクト (DocumentStore、EmbeddableDocumentStore) も static 変数に入れて再利用するなど、初期化方法を工夫してほしい。せっかく速いデータベースでも、「宝の持ち腐れ」となってしまうので注意が必要だ。

また、せっかく RavenDB を使うなら、データベース アクセスなどはビジネス ロジックに混在させず、透過的に使えるような工夫もできるだろう。Event、Handler、ModelBinder だけでなく、IoC (Dependency Resolver) を活用すれば、より高度な処理の分離も可能だ。(IoC については、ここ に日本語で解説されている。)

広告

RavenDB の特徴と使い方 (プログラミング)」への2件のフィードバック

  1. ピンバック: RavenDB の Replication, Scale Out (Sharding) « Net&Web

  2. ピンバック: SignalR の Scale Out とロード バランサー対応 (SignalR.WindowsAzureServiceBus 編) - 松崎 剛 Blog - Site Home - MSDN Blogs

コメントは受け付けていません。