はじめに
昨日に引き続き、「7つのデータベース 7つの世界 -オーム社-」の第5章からMongoDBの1日目を勉強する。
家に帰ってから思い出したけど、MongoDBわかれば、今やってるNode.jsとかと一緒にMEANスタックで開発でできるからそれも視野に入れてがんばろー!
1日目「CRUDとネスト」
深く掘り下げる
Mongoはネストした配列データが好きである。以下のようにして、マッチする値を問い合わせることが出来る。
> db.towns.find(
... { famous_for : 'food' },
... { _id : 0, name : 1, famous_for : 1 }
... )
{ "name" : "New York", "famous_for" : [ "statue of library", "food" ] }
{ "name" : "Portland", "famous_for" : [ "beer", "food" ] }
同様に、部分的にマッチする問い合わせも可能である。
> db.towns.find(
... { famous_for : /statue/ },
... { _id : 0, name : 1, famous_for : 1 }
... )
{ "name" : "New York", "famous_for" : [ "statue of library", "food" ] }
すべてにマッチする問い合わせも、もちろん可能である。
> db.towns.find(
... { famous_for : { $all : ['food', 'beer'] } },
... { _id : 0, name : 1, famous_for : 1 }
... )
{ "name" : "Portland", "famous_for" : [ "beer", "food" ] }
逆にマッチしない値も問い合わせることが出来る。
> db.towns.find(
... { famous_for : { $nin : ['food', 'beer' ] } },
... { _id : 0, name : 1, famous_for : 1 }
... )
{ "name" : "Punxsutawney", "famous_for" : [ "phil the groundhog" ] }
Mongoの本当の力は、深くネストしたサブドキュメントを結果として返せるところにある。サブドキュメントを問い合わせるには、フィールド名をネストごとにドットで区切る。例えば、党に所属していない町長がいる町を探すには、以下のようにする。
> db.towns.find(
... { 'mayor.party' : 'I' },
... { _id : 0, name : 1, mayor : 1 }
... )
{ "name" : "New York", "mayor" : { "name" : "Michael Bloomberg", "party" : "I" } }
党の情報がない町長を探すことも出来る。
> db.towns.find( { 'mayor.party' : { $exists : false } }, { _id : 0, name : 1, mayor : 1 } )
{ "name" : "Punxsutawney", "mayor" : { "name" : "Jim Wehrle" } }
上記のようなクエリは、マッチするフィールドが1つならいい。しかし、サブドキュメントの複数のフィールドにマッチさせる時は、どうすればよいのだうか?
$elemMatch
最後になるが、$elemMatchディレクティブを掘り下げたい。まずは、国を保存するコレクションを作成しよう。今回は、_idを文字列にする。
「終わりに」にも書いたけど、bookの中にインサートすれば良い。
> db.countries.insert({ _id : "ca", name : "Canada", exports : { foods : [ { name : "bacon", tasty : false }, { name : "syrup", tasty : true } ] } })
WriteResult({ "nInserted" : 1 })
> db.countries.insert({ _id : "us", name : "United States", exports : { foods : [ { name : "bacon", tasty : true }, { name : "burgers" } ] } })
WriteResult({ "nInserted" : 1 })
> db.countries.insert({ _id : "mx", name : "Mexico", exports : { foods : [ { name : "salsa", tasty : true, condiment : true } ] } })
WriteResult({ "nInserted" : 1 })
> db.countries.find()
{ "_id" : "ca", "name" : "Canada", "exports" : { "foods" : [ { "name" : "bacon", "tasty" : false }, { "name" : "syrup", "tasty" : true } ] } }
{ "_id" : "us", "name" : "United States", "exports" : { "foods" : [ { "name" : "bacon", "tasty" : true }, { "name" : "burgers" } ] } }
{ "_id" : "mx", "name" : "Mexico", "exports" : { "foods" : [ { "name" : "salsa", "tasty" : true, "condiment" : true } ] } }
> print( db.countries.count() )
3
最終的なコレクション内のドキュメントは3つなので、countした時にも3が返される。
それでは、tastyなベーコンを輸出している国を検索する。
> db.countries.find(
... { 'exports.foods.name' : 'bacon', 'exports.foods.tasty' : true },
... { _id : 0, name : 1 }
... )
{ "name" : "Canada" }
{ "name" : "United States" }
これは、求めている結果とは違うものである。カナダはtastyなベーコンを輸出している国ではない。ここで、$elemMatchが役に立つ。これは、ドキュメント(あるいはネストしたドキュメント)をすべての条件にマッチさせるというものである。
> db.countries.find(
... {
... 'exports.foods' : {
... $elemMatch : {
... name : 'bacon',
... tasty : true
... }
... }
... },
... { _id : 0, name : 1 }
... )
{ "name" : "United States" }
$elemMatchの条件で演算子を使うことも出来る。例えば、香辛料の入った美味しい食べ物を輸出している国を検索してみる。
> db.countries.find(
... {
... 'exports.foods' : {
... $elemMatch : {
... tasty : true,
... condiment : { $exists : true }
... }
... }
... },
... { _id : 0, name : 1 }
... )
{ "name" : "Mexico" }
ブール演算子
これまでの条件はすべて暗黙的にAND演算をしていた。国名が「United States」で_idが「mx」の国を検索すると、Mongoは何も結果を返さない。
> db.countries.find(
... { _id : "mx", name : "United States" },
... { _id : 1 }
... )
しかし、いずれかを検索する$orを使えば、2つの結果が返ってくる。これは、OR A Bのような前置表記法を考えるとわかりやすい。
> db.countries.find(
... {
... $or : [
... { _id : "mx" },
... { name : "United States" }
... ]
... },
... { _id : 1 }
... )
{ "_id" : "us" }
{ "_id" : "mx" }
このようなコマンドは多くあるので、以下にそのコマンド表を記述する。
コマンド | 説明 |
---|---|
$regex | PCRE互換の正規表現にマッチする(または前述のように//で囲んで使う) |
$ne | 不等 |
$lt | 未満 |
$lte | 以下 |
$gt | より大きい |
$gte | 以上 |
$exists | フィールドの存在確認 |
$all | 配列のすべての要素にマッチ |
$in | 配列の任意の要素にマッチ |
$nin | 配列の任意の要素にマッチしない |
$elemMatch | ネストしたドキュメントの配列のすべてのフィールドにマッチ |
$or | or |
$nor | Not or |
$size | 任意のサイズの配列にマッチ |
$mod | 余り |
$type | フィールドが任意のデータ型であればマッチ |
$not | 任意の演算子の否定 |
更新
問題が発生した。Ney YorkとPunxsutawneyは一意だが、Portlandはオレゴン州・メイン州・テキサス州のどれのことだかわからない。townsコレクションを更新して、州を追加しよう。
update(criteria,operation)関数には、2つの引数が必要である。1つ目はクエリ条件で、find()に渡すオブジェクトと同じものである。2つ目は、マッチしたドキュメントを置き換えるオブジェクトか、変更操作である。ここでは、stateフィールドに文字列「OR」を$setしている。
> db.towns.update( { "name" : "Portland" }, { $set : { "state" : "OR" } } );
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
本書では、nameではなく、ObjectIDでPortlandを指定しているが、面倒なのでnameで指定したほうが良いだろう。
また、$setコマンドを使用せずにupdateしてしまうと、マッチしたドキュメント全体を別のドキュメントに置き換えてしまうので、注意しよう。例えば、下記のようなことだ。
> db.towns.update( { "name" : "Portland" }, { "state" : "OR" } );
更新が成功したことを確認するには、find()してみると良い。
> db.towns.findOne( { "name" : "Portland" } )
{
"_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"
},
"state" : "OR"
}
このように、stateにORが追加されている。
また、値を$setする以外にも色々できる。なかでも$inc(数値の加算)は便利である。Portlandのpopulation(人口)を1,000人増加させてみる。
> db.towns.update( { "name" : "Portland" }, { $inc : {
... population : 1000 }
... }
... )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.towns.findOne( { "name" : "Portland" } )
{
"_id" : ObjectId("55fac75793b946919b1619eb"),
"name" : "Portland",
"population" : 583000,
"last_census" : ISODate("2007-09-20T00:00:00Z"),
"famous_for" : [
"beer",
"food"
],
"mayor" : {
"name" : "Sam Adams",
"party" : "D"
},
"state" : "OR"
}
確認したら、増加している。このようなコマンドも多くあるので、主要なディレクティブを紹介する。
コマンド | 説明 |
---|---|
$set | 任意のフィールドに任意の値を設定する |
$unset | フィールドを削除する |
$inc | 任意のフィールドに任意の数値を加算する |
$pop | 最後(または最初)の要素を配列から削除する |
$push | 配列に値を追加する |
$pushAll | 配列にすべての値を追加する |
$addToSet | $pushと似ているが、値が重複しない |
$pull | 配列からマッチした値を削除する |
$pullAll | 配列からマッチした値をすべて削除する |
参照
前述のように、Mongoには結合がない。分散型なので、結合は非効率な操作なのである。しかし、ドキュメントを相互に参照したほうが便利なこともある。Mongo開発チームは、このような場合に{ $ref : "collection_name", $id : "reference_id" } という構造の使用を推奨している。例えば、townsコレクションを更新して、countriesのドキュメントを参照してみよう。
> db.towns.update(
... { "name" : "Portland" },
... { $set : { country : { $ref : "countries", $id : "us" } } }
... )
> var portland = db.towns.findOne({ "name" : "Portland" }) # townsコレクションからPortlandを読み取る。
> db.countries.findOne({ "name" : portland.country.$id }) # 国名を読み取るには、保存した$idを使ってcountriesコレクションに問い合わせる。
> db[ portland.country.$ref ].findOne({ "name" : portland.country.$id }) # JavaScriptを使えば、参照フィールドに保存されているコレクションの名前を町のドキュメントに問い合わせることが出来る。
最後の2つのクエリは等価なので、好きな方を使えば良い。2つ目の方が、データ駆動のようだ。
また、この章での操作は同じデータベース(book)内に、townsとcountriesの2つのコレクションがないとダメなので、自分のようなミスに気をつけてほしい。
削除
コレクションからドキュメントを削除する方法は簡単らしい。find()をremove()に置き換えれば、条件にマッチしたものが削除される。注意してほしいのは、マッチした要素やサブドキュメントではなく、マッチしたドキュメント全体が削除されてしまうことである。
remove()を実行する前に、find()を実行して条件を確認したほうがいいだろう。それでは、おいしくないベーコンを輸出している国をすべて削除してみる。
> var bad_bacon = {
... 'exports.foods' : {
... $elemMatch : {
... name : 'bacon',
... tasty : false
... }
... }
... }
> db.countries.find( bad_bacon )
{ "_id" : "ca", "name" : "Canada", "exports" : { "foods" : [ { "name" : "bacon", "tasty" : false }, { "name" : "syrup", "tasty" : true } ] } }
> db.countries.remove( bad_bacon )
WriteResult({ "nRemoved" : 1 })
> db.countries.count()
2
まずは、おいしくないベーコンの条件を登録する。その後、検索したらヒットしたので、削除した。
削除後にcountを実行すると、残っている国が2つになっているので、削除成功が確認できた。
コードによる読み取り
最後に、おもしろいクエリオプションを試す。それは、コードである。Mongoでは、ドキュメントを横断したデシジョン関数を実行できる。これを最後に持ってきたのは、最後の手段であるからである。このクエリは非常に遅く、インデックスは使用できず、Mongoも最適化してくれない。しかし、カスタムコードにも素晴らしいと思える時があるという。
例えば、6,000人から600,000人までの人口を検索したいとする。
> db.towns.find( function() {
... return this.population > 6000 && this.population < 600000;
... } )
{ "_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" : 583000, "last_census" : ISODate("2007-09-20T00:00:00Z"), "famous_for" : [ "beer", "food" ], "mayor" : { "name" : "Sam Adams", "party" : "D" }, "state" : "OR", "country" : DBRef("countries", "us") }
Mongoには、デシジョン関数のショートカットも用意されている。
> db.towns.find("this.population > 6000 && this.population < 600000")
{ "_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" : 583000, "last_census" : ISODate("2007-09-20T00:00:00Z"), "famous_for" : [ "beer", "food" ], "mayor" : { "name" : "Sam Adams", "party" : "D" }, "state" : "OR", "country" : DBRef("countries", "us") }
$where句を使えば、他の条件と一緒にカスタムコードが実行できる。この例では、グラウンドホッグという野生動物で有名な町をフィルタしている。
> db.towns.find( {
... $where : "this.population > 6000 && this.population < 600000",
... famous_for : /groundhog/
... } )
{ "_id" : ObjectId("55fac70993b946919b1619ea"), "name" : "Punxsutawney", "population" : 6200, "last_census" : ISODate("2008-01-31T00:00:00Z"), "famous_for" : [ "phil the groundhog" ], "mayor" : { "name" : "Jim Wehrle" } }
注意:この関数はすべてのドキュメントに実行されるが、そのフィールドが存在する保証はない。例えば、「population」フィールドが存在しなかった場合、JavaScriptが適切に実行されずにクエリ全体が失敗してしまう。カスタムJavaScript関数を書く時は、このことに注意しよう。また、カスタムコードを書く前にJavaScriptに慣れておこう。
おわりに
後から、失敗に気づいたのですが、bookの中にcountriesドキュメントをinsertしなきゃでした。
自分もJavaScriptには慣れていないので、カスタムコードを使う方法は、そんなに使わなそうです。
1日分MongoDBに触れてみて、RDBよりも使いやすい感じがしました。JSON形式だと使いやすいですね。ただ、出力の仕方をpprint風にしないと見難いので、そのやり方は2日目の冒頭にでも触れたいと思います。