はじめに
今回は、「7つのデータベース 7つの世界 -オーム社-」の第5章からMongoDBを勉強する。
正直どのDBを選択すれば良いか非常に迷った(今も迷っているし、時間が許す限りは多くのDBを試してみたい)。現在は、とりあえず保存できればいいやとSQLiteを使用していた。使用目的は、Apacheの生ログから解析用に生成したログの保存と利用(利用手法に関しては若干未定)である。
迷ったDBと理由は、
- Redis => ハッシュの探索が高速で、簡単に分散可能(らしい)。
- Riak => Redisと同じく、ハッシュの探索が高速。高可用性。Erlangで作られている。
- HBase => ビッグデータに強し。列指向ってのが難しそうだし、どうせならHadoopと一緒に使いたい。
- MongoDB => データへの問い合わせが簡単。ただ、速度がイマイチらしい。JSONで使用可能。
これらのDBとその理由で迷った結果、MongoDBに決定した。JSONで保存可能なので、そこからD3.jsを利用したデータの可視化が可能なのではないかと考えたからである。ということで、早速始めよう。
概要
MongoDBは、2009年に公開されたらしい。驚いたのだが、新しいのだ。そのため、リレーショナルデータベースの強力なクエリと、RiakやHBaseのような分散データストアのいいとこ取りをしているらしい。これは嬉しい話である。構造化スキーマがないため、データモデルに合わせて成長・変更が可能で、水平スケールできる。
インストール方法
brewで簡単に出来るようだ。
brew install mongodb
mkdir mongodb
mkdir /mongodb/db
mkdir /mongodb/log
ついでに、mongodがデータを保存するディレクトリとログ用のディレクトリを作成した。ここで作成した作業ディレクトリのパスを登録しなくてはいけない。
まずは、Mongoサービスを起動して、--dbpathオプションでパスを登録する。
mongod --dbpath ~/Documents/mongodb/db
この状態で、http://localhost:27017にアクセスすると、起動が確認できる。終了は、Ctrl+Cでできる。
通常は、バックグラウンドで起動するのと、ログを保存するので、それをオプションで追加する。
$ mongod --dbpath ~/Documents/mongodb/db --logpath ~/Documents/mongodb/log/mongodb.log &
$ ps u
$ cat log/mongodb.log
$ kill -KILL プロセスID
起動しているかは、ps uで確認できます。ログを見ると、mongodb.logにログが溜まっていることも確認できます。バックグラウンドで実行しているのを止める時は、killで止めたいプロセスのPID(プロセスID)を入れれば、止めることができます。
--forkで起動している人が多いけど、後ろに&を入れるだけの方が楽だと思います。
1日目「CRUDとネスト」
コマンドラインは楽しい
新しいデータベースとしてbookというものを作ってみる。mongodを起動させてから、以下のコマンドを実行する。
$ cd db
$ mongo book
MongoDB shell version: 2.6.7
connecting to: book
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
http://docs.mongodb.org/
Questions? Try the support group
http://groups.google.com/group/mongodb-user
Server has startup warnings:
2015-09-17T22:20:25.531+0900 [initandlisten]
2015-09-17T22:20:25.531+0900 [initandlisten] ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
起動すると、まずはコンソールでhelpを入力してみなと言われるので、helpを入力する。
helpを見てもらえばわかるが、データベースの一覧を見るには、show dbsを入力する。データベースを切り替えるには、useコマンドを使用する。
Mongoでコレクション(Riakで言うところのバケットに近いらしい)を作るには、コレクションに最初のレコードを追加すればいい。Mongoはスキーマレスなので、事前に何かを定義する必要はない。とうか、bookデータベースも値を追加するまでは存在していないのである。以下のコードは、コレクションtownsを作成/挿入するものである。
> db.towns.insert({
... name: "New York",
... papulation: 22200000,
... last_census: ISODate("2009-07-31"),
... famous_for: [ "statue of library", "food" ],
... mayor : {
... name : "Michael Bloomberg",
... party : "I"
... }
... })
WriteResult({ "nInserted" : 1 })
前節で、ドキュメントはJSON(厳密にはBSON)であると説明している。ので、JSONフォーマットの新しいドキュメントを追加してみる。波括弧{...}は、キーと値で構成されたオブジェクトを意味している(ハッシュテーブルやマップとも呼ばれる)。角括弧[...]は、配列を意味している。これらの値は、任意の深さまでネストできる。
show collectionsコマンドを使えば、存在するコレクションを確認できる。
> show collections
system.indexes
towns
townsは先ほど作成したものだが、system.indexesは常に存在する。コレクションの中身を一覧にするには、find()コマンドを実行する。
> db.towns.find()
{ "_id" : ObjectId("55fabee041ad5a22d4d4d977"), "name" : "New York", "papulation" : 22200000, "last_census" : ISODate("2009-07-31T00:00:00Z"), "famous_for" : [ "statue of library", "food" ], "mayor" : { "name" : "Michael Bloomberg", "party" : "I" } }
リレーショナルデータベースとは違い、Mongoはサーバーサイドでの結合をサポートしていない。1回のJavaScriptの呼び出しで、ドキュメントとネストしたコンテンツを読み取ることが可能である。
新しく挿入した町のJSON出力には、ObjectIdの_idフィールドが含まれている。これは、PostgreSQLの数値型の主キーをインクリメントするSERIALと同じものである。ObjectIdは、タイムスタンプ、クライアントマシンID、クライアントプロセスID、3バイトのインクリメンタルカウンタの計12バイトで構成されている。
この自動採番の仕組みが素晴らしいのは、すべてのマシンのプロセスが、他のMongodインスタンスと重複することなくIDを生成できる点である。この設計上の選択は、Mongoが分散型であることを示している。
JavaScript
Mongoは、mapreduceのように複雑なものからヘルプを出す単純なものまで、あらゆるところにJavaScriptを使っている。
> db.help()
> db.towns.help()
これらのコマンドは、与えられたオブジェクトの機能を一覧にする。dbは、JavaScriptのオブジェクトであり、現在のデータベースに関する情報が含まれている。db.xは、(xという名前の)コレクションを表したJavaScriptのオブジェクトである。コマンドはJavaScriptの関数にすぎない。
> typeof db
object
> typeof db.towns
object
> typeof db.towns.insert
function
関数のソースコードを見るには、引数や丸括弧を付けずに関数を呼び出す(RubyよりもPythonに近いと考えると良いみたいだ)。
> db.towns.insert
function ( obj , options, _allow_dot ){
if ( ! obj )
throw "no object passed to insert!";
var flags = 0;
var wc = undefined;
var allowDottedFields = false;
if ( options === undefined ) {
// do nothing
}
else if ( typeof(options) == 'object' ) {
if (options.ordered === undefined) {
//do nothing, like above
} else {
flags = options.ordered ? 0 : 1;
}
if (options.writeConcern)
wc = options.writeConcern;
if (options.allowdotted)
allowDottedFields = true;
} else {
flags = options;
}
// 1 = continueOnError, which is synonymous with unordered in the write commands/bulk-api
var ordered = ((flags & 1) == 0);
if (!wc)
wc = this.getWriteConcern();
var result = undefined;
var startTime = (typeof(_verboseShell) === 'undefined' ||
!_verboseShell) ? 0 : new Date().getTime();
if ( this.getMongo().writeMode() != "legacy" ) {
// Bit 1 of option flag is continueOnError. Bit 0 (stop on error) is the default.
var bulk = ordered ? this.initializeOrderedBulkOp() : this.initializeUnorderedBulkOp();
var isMultiInsert = Array.isArray(obj);
if (isMultiInsert) {
obj.forEach(function(doc) {
bulk.insert(doc);
});
}
else {
bulk.insert(obj);
}
try {
result = bulk.execute(wc);
if (!isMultiInsert)
result = result.toSingleResult();
}
catch( ex ) {
if ( ex instanceof BulkWriteError ) {
result = isMultiInsert ? ex.toResult() : ex.toSingleResult();
}
else if ( ex instanceof WriteCommandError ) {
result = isMultiInsert ? ex : ex.toSingleResult();
}
else {
// Other exceptions thrown
throw ex;
}
}
}
else {
if ( ! _allow_dot ) {
this._validateForStorage( obj );
}
if ( typeof( obj._id ) == "undefined" && ! Array.isArray( obj ) ){
var tmp = obj; // don't want to modify input
obj = {_id: new ObjectId()};
for (var key in tmp){
obj[key] = tmp[key];
}
}
this.getMongo().insert( this._fullName , obj, flags );
// enforce write concern, if required
if (wc)
result = this.runCommand("getLastError", wc instanceof WriteConcern ? wc.toJSON() : wc);
}
this._lastID = obj._id;
this._printExtraInfo("Inserted", startTime);
return result;
}
それでは、一度shellから抜けだして、ドキュメントをtownsコレクションに追加するjsの関数を作ってみる。フォルダ構成は、/mongo/js/jsファイルである。
function insertCity(
name, population, last_census,
famous_for, mayor_info
) {
db.towns.insert({
name : name,
population: population,
last_census : ISODate( last_census ),
famous_for : famous_for,
mayor : mayor_info
});
}
このコードをシェルにペーストすれば、呼び出せるようになる。本書では、この説明だけなのだが、insert_city.jsを読み込まないとbookで関数が使えないので注意。
実際には、こうやる。
$ mongo book ~/Documents/mongodb/js/insert_city.js --shell
MongoDB shell version: 2.6.7
connecting to: book
type "help" for help
Server has startup warnings:
2015-09-17T22:20:25.531+0900 [initandlisten]
2015-09-17T22:20:25.531+0900 [initandlisten] ** WARNING: soft rlimits too low. Number of files is 256, should be at least 1000
>
これで、insert_city.jsとbookがコネクトされた。--shellオプションをつけることで、そのまま関数を使える。
> insertCity("Punxsutawney", 6200, '2008-01-31', ["phil the groundhog"], { name : "Jim Wehrle" } )
> insertCity("Portland", 582000, '2007-09-20', ["beer", "food"], { name : "Sam Adams", party : "D" } )
> db.towns.find()
{ "_id" : ObjectId("55fabee041ad5a22d4d4d977"), "name" : "New York", "papulation" : 22200000, "last_census" : ISODate("2009-07-31T00:00:00Z"), "famous_for" : [ "statue of library", "food" ], "mayor" : { "name" : "Michael Bloomberg", "party" : "I" } }
{ "_id" : ObjectId("55fac70993b946919b1619ea"), "name" : "Punxsutawney", "population" : 6200, "last_census" : ISODate("2008-01-31T00:00:00Z"), "famous_for" : [ "phil the groundhog" ], "mayor" : { "name" : "Jim Wehrle" } }
{ "_id" : ObjectId("55fac75793b946919b1619eb"), "name" : "Portland", "population" : 582000, "last_census" : ISODate("2007-09-20T00:00:00Z"), "famous_for" : [ "beer", "food" ], "mayor" : { "name" : "Sam Adams", "party" : "D" } }
これで、このコレクションには3つのtownsができた。この結果は前述したdb.towns.find()で確認できる。
読み取り:Mongoでもっと楽しく
find()関数を引数なしに呼び出すことで、すべてのドキュメントを取得できた。特定のドキュメントにアクセスするには、_idプロパティを設定する。_idはObjectId型なので、クエリで使う文字列をObjectId(str)で変換する必要がある。
> db.towns.find({ "_id" : ObjectId("55fac70993b946919b1619ea") })
{ "_id" : ObjectId("55fac70993b946919b1619ea"), "name" : "Punxsutawney", "population" : 6200, "last_census" : ISODate("2008-01-31T00:00:00Z"), "famous_for" : [ "phil the groundhog" ], "mayor" : { "name" : "Jim Wehrle" } }
find()関数には、2つ目の引数(任意)があり、こちらは読み取るフィールドをフィルタするのに使用する。例えば、町の名前が必要ならば、(_idと一緒に)nameを1(あるいはtrue)にしたものを渡す。
> db.towns.find({ "_id" : ObjectId("55fac70993b946919b1619ea") }, { name : 1 })
{ "_id" : ObjectId("55fac70993b946919b1619ea"), "name" : "Punxsutawney" }
反対に、name以外のフィールドすべてを読み取る時は、nameに0(あるいはfalseやnull)を設定する。
> db.towns.find({ "_id" : ObjectId("55fac70993b946919b1619ea") }, { name : 0 })
{ "_id" : ObjectId("55fac70993b946919b1619ea"), "population" : 6200, "last_census" : ISODate("2008-01-31T00:00:00Z"), "famous_for" : [ "phil the groundhog" ], "mayor" : { "name" : "Jim Wehrle" } }
フィールド値・範囲・条件を組み合わせて、任意のクエリを作成することもできるようだ(これはPostgreSQLと同じらしい)。たとえば、最初の文字が「P」で始まり、人口が10,000人未満の町を探すには、Perl互換の正規表現(PCRE)と範囲演算子が使えるようだ。
> db.towns.find(
... { name : /^P/, population : { $lt : 10000 } },
... { name : 1, population : 1 }
... )
{ "_id" : ObjectId("55fac70993b946919b1619ea"), "name" : "Punxsutawney", "population" : 6200 }
Mongoの条件演算子は、field : { $op : value }のフォーマットに従う。$opは、$ne(不等)などの演算子らしい。field < valueのようなもっと簡潔な構文を使いたいかもしれないが、これが、JavaScriptのコードらしい。
クエリ言語がJavaScriptである利点は、演算子を自分で作れることらしい。試しに、「人口が1万人より多く100万人より少ない」という条件の演算子を作ってみる。
> var population_range = {}
> population_range['$lt'] = 1000000
1000000
> population_range['$gt'] = 10000
10000
> db.towns.find( { name : /^P/, population : population_range }, { name : 1 } )
{ "_id" : ObjectId("55fac75793b946919b1619eb"), "name" : "Portland" }
また、数値の範囲だけでなく、日付の範囲も読み取れる。例えば、「last_census」が2008年1月31日以下のnameをすべて探すことも出来る。
> db.towns.find(
... { last_census : { $lte : ISODate('2008-01-31') } },
... { _id : 0, name : 1 }
... )
{ "name" : "Punxsutawney" }
{ "name" : "Portland" }
この例では、_idフィールドに0を設定して、表示させていない点にも注目してほしい。
おわりに
インストール方法のところの起動方法とかが忘れやすいからけっこう大事かも。
MongoDBの薄い本というPDFを発見したから読んでみる。
MongoDB編1日分が長いので、半分に分けました。続きはまた明日。