MongoDBとRavenDBの違いから読み解く、ドキュメント・データベースごとの設計思想特集:MongoDBで理解する「ドキュメント・データベース」の世界(後編)

MongoDBと同じ「ドキュメント・データベース」であるRavenDBの特徴を見てみよう。そこから設計思想の違いが見えてくる。

» 2012年11月30日 07時08分 公開
[国分すばる,]
特集:MongoDBで理解する「ドキュメント・データベース」の世界(後編)
Insider.NET

 前回の前編では、MongoDBを例にドキュメント・データベース(Document Database)の基本的な機能を見てきたが、いくつかMongoDB独自の考え方や特徴も含まれていた。今回の後編では、MongoDB以外のドキュメント・データベースとして「RavenDB」(インストールや基本的な使い方についてはこちらを参照)を見ていくことで、同じドキュメント・データベース同士での相違を簡単に確認したい。

 いくつかのドキュメント・データベースを見ていくことで、同じ「ドキュメント・データベース」という共通の概念ではあるが、それぞれがどのような位置付けであるか、その相違を発見できるだろう(なお、ここではデータベースごとの機能比較表を作って優劣を決めたいのではなく、あくまでドキュメント・データベースへの理解を深めるために、その差異に焦点を当てたいということである)。なお本稿では、全てのコードはC#言語で記述している。

RavenDBから読み解く、データベースごとの設計思想

 まず、RavenDBというデータベースであるが、そもそも、これは.NETを強く意識したデータベースであり、特別なケースを除き、一般にはWindowsで動作させることになるだろう。この点は、無論、MongoDBと異なる不利な点ではあるが、この後、さらに深く設計思想を見ていく中で、こうした実装の背景も理解していこう。

実装基盤や管理方法の差異

 まず容易に想像できることだが、使用している実装基盤に違いがあり、データベース・エンジンとして見た場合に、それぞれに独自な特徴を持っている。ここでは、こうした詳細を網羅的に解説はしないが、そもそもベースとなる実装基盤が全く異なっているという点を理解しておいてほしい。

 例えば、IO高速化の手法として、MongoDBではメモリマップド・ファイル(memory-mapped file)が使用されているが、後述するRavenDBでは、ExchangeサーバなどでもおなじみのJetベースのエンジン(ESE: Extensible Storage Engine)が使用されている。

 さらに、管理方法についても、RavenDBでは、設定情報も「オブジェクト(Object)」(=ドキュメント:Document。以降、単に「オブジェクト」と表記)として登録されていて、このドキュメントを編集することで管理を行う。こうした点から、RDB(リレーショナル・データベース)ライクな使い方(例えば、コマンドライン・ユーティリティの充実、など)ではなく、徹底した「ドキュメント指向」を意識している点が伺える。

使い方の共通点と差異

 そのうえで、ドキュメント・データベースとしての多くの共通点がある。まずMongoDB同様、RavenDBでも、id属性を内部で管理し、Key-ValueによるNoSQLのデータベースとなっている。また、「Index」と呼ばれる別のKeyを持つことができ、前回説明したシャーディング(Sharding)と同様のデータ分散が可能だ。C#言語を使用した場合は、同様にLINQも使用できる(つまり、言語で提供されているオブジェクトと同様の扱い方が可能だ)。MongoDBを使ったことがあるエンジニアなら、多くの部分でそのままRavenDBを使いこなせるだろう。

 しかし、使い方の面で、根本的に概念が異なるいくつかの側面も持っている。MongoDBに慣れたエンジニアが恐らく最初に出くわす奇妙な点は、まず「検索」だろう。

 例えばRavenDBで、インデックス(Index)の明示的な作成を行わずに、下記のコードのように検索を行ったと仮定しよう(下記は、RavenDBを使用するC#コードの例だ。コードの詳細な解説は省略するが、RavenDBを取り扱うためのシンタックス自体については、特に違和感はないだろう)。

using System;
using System.Linq;
using Raven.Client;
using Raven.Client.Document;

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

class Program
{
  static void Main()
  {
    using (var ds = new DocumentStore
    {
      Url = "http://localhost:8080/"
    })
    {
      ds.Initialize();

      using (IDocumentSession session = ds.OpenSession())
      {
        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);
        }
      }
    }
    Console.ReadKey();
  }
}


RavenDBを使用するコンソール・アプリケーションのコード例


 インデックスを作成していないため、前編で解説したとおり、MongoDBでは、データ量が増えた場合に全件の確認となり、良いパフォーマンスは期待できない。しかし、RavenDBでは状況が異なり、良いパフォーマンスが発揮されるのだ。その理由を説明しよう。

RavenDBのインデックス(Index)の特徴

 RavenDBでは、基本的なインデックスは自動で作成される。アプリケーションの中で上記のようなコードが実行されると、最初の実行の際にRavenDBが判断し、自動的にNameフィールドにインデックスを作成するのだ。すなわちRavenDBの場合、上記の検索ではインデックスが使用される。これは「動的インデックス(Dynamic Index)」と呼ばれている。

 もちろん、作成されたインデックスの確認や削除も可能だし、複雑なインデックスの場合は、こうした動的な作成に任せず、MongoDBのように開発者自らがインデックスの定義を行って、そのインデックスを使うこともできる(複雑な場合には、むしろ、そのようにすべきだろう)。

 そして、その代表的な例がMapReduceだ。MongoDBでは、インデックスとは別の世界でMapReduceの処理をクエリの代わりに実行したが、RavenDBでは、SQLのgroup by句などに代表される集約(Aggregation)の処理を実現する方法として、MapReduceをインデックス作成で使用できる。

 例えば下記のコードは、OrderオブジェクトのCategoryフィールドごとにグループ化を行って、Categoryフィールドごとのオブジェクトの個数を求める際に使用するインデックスを定義している。なお、MapとReduceの概念については前回説明したとおりなので、ここでは説明を省略する。

using Raven.Client.Indexes;
……省略……
public class CategoryCount
{
  public string Category { get; set; }
  public int Count { get; set; }
}

class Program
{
  static void Main()
  {
    using (var ds = new DocumentStore
    {
      Url = "http://localhost:8080/"
    })
    {
      ds.Initialize();

      // インデックスの作成
      ds.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 CategoryCount
              {
                Category = g.Key,                Count = g.Sum(x => x.Count)
              })
        });
      }
    }
  }
}


RavenDBにおけるMapReduceによるインデックスの定義例


 上記のように作成したインデックスを使用する場合には、下記のコードのようにすればよい。このコード例では、グループ化されたアイテムの最初の1つを取り出してコンソールに出力している。

……省略……
using (var ds = new DocumentStore
{
  Url = "http://localhost:8080/"
})
{
  ds.Initialize();

  // インデックスを使ったクエリ
  using (IDocumentSession session = ds.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);
  }
  ……省略……


RavenDBにおけるMapReduceによるインデックスの使用例


 このようなインデックスの振る舞いは、RDBやMongoDBに慣れてきたエンジニアにとっては奇異に思われるだろう(中の動作が想像しづらいはずだ)。そこで、このインデックスの本質をもう少し掘り下げてみたい。

RavenDBのインデックスの本質とは?

 話を簡単にするため、MapReduceではなく、Map関数のみを使用した簡単なインデックスを作成してみよう。

 下記のコードでは、単純にNameフィールドにインデックスを作成しているだけだが、このコードに書かれているAnalyzersプロパティがポイントだ(このプロパティについては、この後すぐ解説する)。

using Lucene.Net.Analysis;
……省略……
using (var ds = new DocumentStore
{
  Url = "http://localhost:8080/"
})
{
  ds.Initialize();

  // インデックスの作成
  ds.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関数のみを使用した簡単なインデックスの定義例


 実は、このようなインデックスを作成した場合、Nameフィールドが「ballpoint pen」だった場合に「pen」で検索しても結果が抽出されるようになる。ポイントは、上記のAnalyzersプロパティだ。上記のコードのとおり、このプロパティに「WhitespaceAnalyzer」を指定することで、空白のトークンで要素を分割し、インデックスとして登録する。

 もうお気付きかと思うが、RavenDBで使われているインデックスとは、実は、全文検索インデックスだ。RavenDBでは、全文検索エンジンとして、オープンソースのLucene.Netを使用している(上記で使われているWhitespaceAnalyzerクラスは、正確には、Lucene.Net.Analysis.WhitespaceAnalyzerクラスだ)。

 全文検索インデックスは、上記のとおり柔軟なインデックス管理が可能だが、そのトレードオフもある。例えば、インデックスはリアルタイムに収集されないという点だ。RavenDBでは、通常のドキュメントにおけるインデックス収集のようにバックグラウンドでのインデックス作成となるため、検索時に古いインデックスのまま(=Staleの状態)となっている場合がある(コードを使って、Staleか否かの検査を行うことは可能だ)。これに対し、MongoDBの場合は、バックグラウンド・オプションを指定しない限り、フォアグラウンドでインデックス作成を行う。

 このように、特徴の1つとして、RavenDBは徹底した「ドキュメント指向」であるという点が重要だ。そして、このように書くとRavenDBが特別なデータベースに思われるかもしれないが、「実は、むしろMongoDBの方が、RDBを強く意識して設計されている」と表現した方が正確だろう。

 上記のような更新と検索の一貫性が保たれないインデックスの遅延など、RDBを使いこなしてきたエンジニアにとっては容認できないかもしれないが、ドキュメントにおけるインデックスとは本来はそのようなものである。「MongoDBは、むしろ、そうしたドキュメント・データベース本来の指向をあえてカスタマイズし、より現実的な指向で設計されている」と言ってよいだろう。

RavenDBのシャーディング(Sharding)

 さらに、RavenDBのシャーディングを見てみよう。RavenDBでは、シャーディングやレプリケーション(Replication)など追加の機能を、「Bundle」と呼ばれるアドオンによって提供しており、既定でいくつかのBundleが使用可能になっている。今回は、この既定のBundleの1つとして含まれているシャーディングを使用する。

 例えば、OrderオブジェクトのCategoryプロパティが「food」の場合と「material」の場合で、保存先のサーバを分散する場合は、下記のようなコードを記述する。

using System.Collections.Generic;
using Raven.Client.Shard;
……省略……

var stores = new Dictionary<string, IDocumentStore>
{
  {
    "material",    new DocumentStore { Url = "http://localhost:8080" }
  },  {
    "food",    new DocumentStore { Url = "http://localhost:8081" }
  }
};

var shrd = new ShardStrategy(stores)
  .ShardingOn<Order>(o => o.Category);

using (var ds = new ShardedDocumentStore(shrd))
{
  ds.Initialize();

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

    Order o2 = new Order
    {
      Name = "ice cream",      Price = 150,      Category = "food"
    };
    session.Store(o2);
    session.SaveChanges();

    Order o3 = new Order
    {
      Name = "notebook",      Price = 80,      Category = "material"
    };
    session.Store(o3);
    session.SaveChanges();
  }
}
……省略……


RavenDBのシャーディングを使うコード例


 上記のコードで、「o1」「o3」はポート8080のサーバに配置され、「o2」はポート8081のサーバに配置される。もちろんCategoryフィールドで検索を行った場合は、MongoDBと同じように、その対象のサーバのみに検索を行って結果を抽出する(つまりRavenDBでも、MongoDBと同様に、シャーディングを強く意識したクエリを行う)。

 ここで注意してほしい点は、上記のコードのとおり、シャーディングのための構成(上記の「ShardStrategy」)がクライアント側で保持されているという点だ。対照的に、MongoDBでは、シャード(Shard)間に共通の情報を保持する構成サーバと、入り口となるMongosサーバが提供され、シャードごとの振り分けはサーバ側で行われていた。

RavenDBの特徴のまとめ

 このようにRavenDBは、アプリケーション・コードそのものと密接に関連して動作することを前提としたデータベースになっている点が分かる。

 例えば、先ほど解説した動的インデックス(dynamic index)を思い出してほしい。RavenDBは、アプリケーションから一定の使い方(同じ検索)をしている限り、そのアプリケーションに最適化されたインデックスが自動生成される。

 このほかにも、RavenDBには「RavenDB.Embedded」と呼ばれるエンベッド版(=組み込み版)のデータベースも提供されていて、データベース・サーバをあえて立てなくても、ASP.NET MVCなどのアプリケーションの一部として組み込んで使用できるようにもなっている(この場合、データは、アプリケーション・サーバのサブディレクトリなどに保存される。もちろん、シャーディングなど、エンベッド版で使える機能にはいくつかの制限はあるが、多くのコードがそのままこのエンベッド版でも動作する)。

 冒頭で、RavenDBを「.NETを意識したデータベース」と表現したが、これらの点からも.NETで構築されたアプリケーションとの強い関連性が理解できるだろう。上記のさまざまな例からわかるように、「RavenDBは、.NETアプリケーションの一部としてドキュメント・データベースを扱うような軽い使い方」が想定されているのだ。


  今回、MongoDBとRavenDBを例に、ドキュメント・データベースごとの違いを見てきたが、どちらが良いか悪いかという単純な比較ではなく、前述のとおり、それぞれのドキュメント・データベースごとの「設計思想」があるという点を理解してほしい。こうしたデータベースごとの差異を全て理解して比較する必要はないが、少なくとも自分が使用しているドキュメント・データベースについて、どのような発想や着眼に基づいて機能が提供されているかを理解しておくことは、決して損なことではないだろう。

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。