次の方法で共有


働くプログラマ

MongoDB と NoSQL を試す (第 3 部)

Ted Neward

サンプル コードのダウンロード

Ted Neward先月のコラムでは、調査テストを使用して、MongoDB の調査を続けました。そこでは、テスト中にサーバーを開始したり停止したりする方法と、ドキュメント間の参照を把握する方法を紹介し、参照関係の把握が難しくなる理由についてもいくつか説明しました。今月は、MongoDB の機能の中でも中間的な機能として、クエリ述語、集計関数、および MongoDB.Linq アセンブリによる LINQ サポートについて調べます。また、MongoDB を運用環境でホストする際の注意事項についてもいくつか説明します。

最後にヒーローが残された . . .

スペースの都合上、ここでこれまでのコラムについてはあまり触れることができませんので、msdn.microsoft.com/magazine から 5 月号6 月号のコラムをオンラインでご覧ください。コラムでは、関連するコードを添えて、調査テストを具体的に示しました。この調査テストでは、私のお気に入りのテレビ番組のキャラクターをあらかじめサンプル データとして設定し、これを操作しています。内容を思い出していただけるように、図 1 に先月の調査テストを示します。ここまではよろしいですか。

図 1 サンプル調査テスト

[TestMethod]
        public void StoreAndCountFamilyWithOid()
        {
          var oidGen = new OidGenerator();
          var peter = new Document();
          peter["firstname"] = "Peter";
          peter["lastname"] = "Griffin";
          peter["_id"] = oidGen.Generate();

          var lois = new Document();
          lois["firstname"] = "Lois";
          lois["lastname"] = "Griffin";
          lois["_id"] = oidGen.Generate();

          peter["spouse"] = lois["_id"];
          lois["spouse"] = peter["_id"];

          var cast = new[] { peter, lois };
          var fg = db["exploretests"]["familyguy"];
          fg.Insert(cast);

          Assert.AreEqual(peter["spouse"], lois["_id"]);
          Assert.AreEqual(
            fg.FindOne(new Document().Append("_id",
              peter["spouse"])).ToString(), lois.ToString());

          Assert.AreEqual(2,
            fg.Count(new Document().Append("lastname", "Griffin")));
        }

高齢者たちを皆呼び出す . . .

先月のコラムでは、クライアント コードを使用して、特定の条件に一致する ("lastname" フィールドが指定した String に一致する、"_id" フィールドが特定の Oid に一致するなど) すべてのドキュメントをフェッチしましたが、述語スタイルのクエリ ("age" フィールドが "18" よりも大きい値のドキュメントをすべて検出するなど) をフェッチする方法は説明しませんでした。おわかりのように、MongoDB では、実行するクエリを記述する際に、SQL スタイルのインターフェイスは使用しません。代わりに、ECMAScript や JavaScript を使用します。そのため、データをフィルター処理したり集計したりするためには、ストアド プロシージャとほぼ同様に、実際にはコードのブロックを受け取ってサーバーで実行します。

このため、LINQ に似た機能がいくつか用意されています (Mongo.Linq アセンブリでサポートされる LINQ 機能についてはこの後の説明になりますが)。"$where" というフィールドを含むドキュメントと、実行する ECMAScript コードを記述したコード ブロックを指定することで、次のように任意の複雑なクエリを作成できます。

 

[TestMethod]
        public void Where()
        {
          ICursor oldFolks =
            db["exploretests"]["familyguy"].Find(
            new Document().Append("$where", 
            new Code("this.gender === 'F'")));
          bool found = false;
          foreach (var d in oldFolks.Documents)
            found = true;
          Assert.IsTrue(found, "Found people");
        }

ご覧のように、Find 呼び出しは ICursor インスタンスを返します。ICursor インスタンス自体は IEnumerable ではありません (つまり、foreach ループ内では使用できません) が、IEnumerable<Document> という Documents プロパティが含まれています。クエリがデータを大量に返すことになる場合は、Limit プロパティに数値 ("n") を設定することで、最初から "n" 番目までの結果を返すように ICursor を限定できます。

クエリ述語の構文は、4 つの異なる部分から構成されます (図 2 参照)。

図 2 4 つの異なる部分から構成されるクエリ述語の構文

[TestMethod]
        public void PredicateQuery()
        {
          ICursor oldFolks =
            db["exploretests"]["familyguy"].Find(
            new Document().Append("age",
            new Document().Append("$gt", 18)));
          Assert.AreEqual(6, CountDocuments(oldFolks));

          oldFolks =
            db["exploretests"]["familyguy"].Find(
            new Document().Append("$where",
            new Code("this.age > 18")));
          Assert.AreEqual(6, CountDocuments(oldFolks));

          oldFolks =
            db["exploretests"]["familyguy"].Find("this.age > 18");
          Assert.AreEqual(6, CountDocuments(oldFolks));

          oldFolks =
            db["exploretests"]["familyguy"].Find(
            new Document().Append("$where",
            new Code("function(x) { return this.age > 18; }")));
          Assert.AreEqual(6, CountDocuments(oldFolks));
        }

2 番目と 3 番目の形式にある "this" は、常に、調査中のオブジェクトを指します。

ドライバーを通じて、データベースに任意のコマンド (ECMAScript コード) を送信できます。実際には、クエリやコマンドの送信にはドキュメントを使用します。そのため、たとえば、IMongoCollection インターフェイスで使用できる Count メソッドは、次のようにかなり細かくなるコード スニペットでは本当に便利です。

[TestMethod]
        public void CountGriffins()
        {
          var resultDoc = db["exploretests"].SendCommand(
            new Document()
              .Append("count", "familyguy")
              .Append("query",
                new Document().Append("lastname", "Griffin"))
            );
          Assert.AreEqual(6, (double)resultDoc["n"]);
        }

つまり、MongoDB ドキュメントによって記述される集計操作 ("distinct"、"group" など) は、同じメカニズムでアクセスできます。ただし、MongoDB.Driver の API のメソッドとしては公開されていない場合もあります。

"特別な名前" 構文の "$eval" を通じて、クエリ外部の任意のコマンドを、データベースに送信できます。これで、正規の ECMAScript のコード ブロックをサーバーに対して実行できます。ここでも、コード ブロックは事実上ストアド プロシージャのように実行されます。

[TestMethod]
        public void UseDatabaseAsCalculator()
        {
          var resultDoc = db["exploretests"].SendCommand(
            new Document()
              .Append("$eval", 
                new CodeWScope { 
                  Value = "function() { return 3 + 3; }", 
                  Scope = new Document() }));
          TestContext.WriteLine("eval returned {0}", resultDoc.ToString());
          Assert.AreEqual(6, (double)resultDoc["retval"]);
        }

あるいは、指定した eval 関数をデータベースで直接使用します。この方法では柔軟性が十分とはいえない場合、MongoDB では、クエリ内およびサーバー側での実行ブロック内で実行するためのユーザー定義の ECMAScript 関数の記憶域をデータベースのインスタンス上に確保することを許可しています。このとき、この ECMAScript 関数を特別なデータベース コレクション "system.js" に追加します。詳細については、MongoDB の Web サイト (英語) を参照してください。

不完全な LINQ

C# MongoDB ドライバーは、LINQ もサポートしており、開発者は、図 3 のような MongoDB クライアント コードを記述できます。

図 3 LINQ サポートの例

[TestMethod]
        public void LINQQuery()
        {
          var fg = db["exploretests"]["familyguy"];
          var results = 
            from d in fg.Linq() 
            where ((string)d["lastname"]) == "Brown" 
            select d;
          bool found = false;
          foreach (var d in results)
          {
            found = true;
            TestContext.WriteLine("Found {0}", d);
          }
          Assert.IsTrue(found, "No Browns found?");
        }

また、MongoDB データベースの動的な性質を維持するため、このサンプルではコードを生成する必要がありません。Linq を呼び出して、MongoDB LINQ プロバイダーを "有効" にするオブジェクトを返すだけです。今月のコラムを執筆している時点では、LINQ サポートはほぼ初歩的状態ですが、次第に強化され、このコラムが公開されるころには、大きく改善されているでしょう。新機能と例についてのドキュメントは、プロジェクト サイトの Wiki (英語) で確認できます。

配布も 1 つの機能

何はともあれ、MongoDB を運用環境で使用するつもりであれば、運用中のサーバーとサービスの実行を管理しなければならない担当者の負担を減らすために、いくつか対処しておくべき点があります。

まず、サーバー プロセス (mongod.exe) をサービスとしてインストールする必要があります。通常、運用サーバーでは、このサーバー プロセスを対話型デスクトップ セッションで実行することは許可されません。そのため、mongod.exe では、サービス インストール オプション ("--install") をサポートしており、これを使用して、[サービス] パネルまたはコマンド ライン (「net start MongoDB」と入力) から開始できるサービスとしてインストールします。ただし、このコラムの執筆時点では、--install コマンドにやや特異な性質が 1 つあり、実行に使用されたコマンド ラインを確認することによって実行可能ファイルのパスを推定するため、コマンド ラインには完全パスを指定する必要があります。つまり、MongoDB を C:\Prg\mongodb にインストールする場合、サーバー プロセスは、「C:\Prg\mongodb\bin\mongod.exe --install」というコマンドを使用して、(管理者権限を持っている) コマンド プロンプトからサービスとしてインストールする必要があります。

ただし、"--dbpath" などのコマンド ライン パラメーターを、このインストール用のコマンドに記述することもあります。このとき、ポート、データ ファイルのパスなど、なんらかの設定を変更すると、サービスを再インストールしなければならなくなります。さいわい、MongoDB では、"--config" コマンド ライン オプションを使用して指定できる構成ファイル オプションをサポートしているため、通常は次のコードのように、構成ファイルの完全パスをサービスのインストール時に渡してから、追加で必要な構成をすべて行うのが最善の方法です。

C:\Prg\mongodb\bin\mongod.exe --config C:\Prg\mongodb\bin\mongo.cfg --install
net start MongoDB

通常、サービスが正しく実行されていることを確認するテストを最も簡単に行うには、MongoDB ダウンロードに同梱されている mongo.exe クライアントを使用して、サービスに接続します。サーバーはソケットを通じてクライアントと通信するため、サーバー間の通信を許可するには、ファイアウォールにそのための穴をあける必要があります。

求めているようなデータ アンドロイドではない

もちろん、セキュリティが確保されない状態で MongoDB サーバーにアクセスするのはよいことではないため、望ましくない訪問者に対して、サーバーをセキュリティ保護することは重要な機能です。MongoDB でも認証をサポートしているものの、SQL Server などの "鉄の塊 (高価な超大型コンピューター)" のような大規模データベースに搭載されている、高度なセキュリティ システムは存在しません。

通常は、まず、mongo.exe クライアントを使用してデータベースに接続し、管理者ユーザーを管理データベース (MongoDB サーバー全体の実行と管理のためのデータを含むデータベース) に追加することで、管理者がログインできるデータベースを作成します。

> use admin
> db.addUser("dba", "dbapassword")

これを実行した後、これ以上何か操作を実行するには、このシェル内であってもアクセスの認証を受ける必要があります。アクセスはシェル内で明示的に認証されます。

> db.authenticate("dba", "dbapassword")

これで DBA は、先ほど例に示したものと同じ addUser 呼び出しを使用して、データベースを変更したり、ユーザーを追加したりすることで、ユーザーを MongoDB データベースに追加できるようになります。

> use mydatabase
> db.addUser("billg", "password")

Mongo.Driver を通じてデータベースに接続する際、Mongo オブジェクトを作成するのに使用する接続文字列の一部として認証情報を渡して、同じ認証を透過的に実行します。

var mongo = new Mongo("Username=billg;Password=password");

当然ながら、パスワードをコードに直接ハードコードしたり、オープンな状態で保存したりしないようにします。データベースを利用するアプリケーションに適した、一定の秩序のあるパスワードを使用します。実際のところ、構成全般 (ホスト、ポート、パスワードなど) は、構成ファイルに保存し、ConfigurationManager クラスを通じて取得します。

一部のコードに触れるために手を差し伸べる

管理者は、実行中のインスタンスを定期的に確認して、実行中のサーバーに関する診断情報を取得することを考えています。MongoDB では、このデータベースを操作するための HTTP インターフェイスをサポートしています。このインターフェイスは、通常のクライアント通信に使用するように構成されるポートよりも、数値的に 1,000 大きい番号のポートで実行されています。したがって、既定の MongoDB ポートは 27017 なので、HTTP インターフェイスはポート 28017 で見つかります (図 4 参照)。

Figure 4 The HTTP Interface for Interacting with MongoDB

図 4 MongoDB を操作するための HTTP インターフェイス

また、この HTTP インターフェイスは、MongoDB.Driver や MongoDB.Linq のネイティブ ドライバーとは対照的に、REST スタイルの通信方法を許可します。MongoDB の Web サイトで詳細を確認できますが、基本的には、コレクションのコンテンツにアクセスするための HTTP URL は、データベース名とコレクション名をスラッシュで区切って指定します (図 5 参照)。

Figure 5 The HTTP URL for Accessing a Collection’s Contents

図 5 コレクションのコンテンツにアクセスするための HTTP URL

WCF を使用して REST クライアントを作成する方法の詳細については、MSDN 記事「Windows Communication Foundation (WCF) における REST (英語)」を参照してください。

ヨーダの言葉

MongoDB は急速に進化している製品なので、これまでのコラムで MongoDB 機能の主な要素については調査してきましたが、調査していないことはまだまだたくさんあります。MongoDB は SQL Server にそのまま置き換わるものではありませんが、従来の RDBMS では適切に機能しない領域ではその代わりになり得る記憶域です。また、MongoDB が現在も進化し続けているように、MongoDB と C# 関連のプロジェクトも進化しています。このコラムを書いているとき、新しい強化機能が数多くベータ版に導入されました。その機能には、プレーンなオブジェクトを使用して厳密に型指定されたコレクションを操作するための強化機能や、大幅に強化された LINQ サポートなどがあります。どちらもぜひご注目ください。

ただし、今回で MongoDB とはお別れです。働くプログラマの方にはなじみがないかもしれませんが (いいえ、ほぼ間違いなくなじみがないでしょう)、開発者の世界の他の分野にも注目してみましょう。それでは、コーディングに励んでください。そして、偉大な開発者マスター ヨーダがかつて言った、「ソースは知識と防御のために使うものじゃ。決してハッキングのためではないのだ」という言葉を覚えておきましょう。

Ted Neward は、Microsoft .NET Framework および Java のエンタープライズ プラットフォーム システムを専門とする独立企業 Neward & Associates の社長を務めています。これまでに 100 個を超える記事を執筆している Ted は、C# MVP であり、INETA の講演者でもあります。さまざまな書籍を執筆および共同執筆していて、『Professional F# 2.0』(Wrox 2010 年、英語) もその 1 つです。彼は定期的にコンサルティングを行い、開発者を指導しています。彼の連絡先は ted@tedneward.com (英語のみ) です。ブログを blogs.tedneward.com (英語) に公開しています。

この記事のレビューに協力してくれた技術スタッフの Sam Corder と Craig Wilson に心より感謝いたします。