MongoDB超入門

More than 1 year has passed since last update.


概要

MongoDBの薄い本を読みながらMongoDBを勉強し始めたので、自分の理解を試すために書きます。

簡単なCRUDの説明になります。

また、以下の内容で簡単なMongoDBの使い方がわかった方は、[Groovy] MongoDBを簡単に扱えるイケてる言語Groovy -基本的なCRUD-を見ていただければ、プログラミング言語Apache GroovyからMongoDBを扱う方法も確認していただけます。


インストール

Synapticパッケージマネージャからインストールします。

mongodb というパッケージを選択してインストールしてください。

インストール構成はまぁ普通な感じでしょうか??

重要そうなファイルは以下のように配置されています。


  • /usr/bin/mongo

  • /usr/bin/mongod

  • /etc/mongodb.conf


    • /var/lib/mongodb (confで設定されている)

    • /var/log/mongodb/mongodb.log(confで設定されている)



  • /etc/init.d/mongodb


起動と停止

起動:sudo service mongodb start

停止:sudo service mongodb stop

ちなみに停止についてはmongoのシェルからのコマンド(db.shutdownServer())でも可能。

kkuwana% mongo

MongoDB shell version: 2.4.9
connecting to: test
> use admin
switched to db admin
> db.shutdownServer()
Thu Jul 10 09:59:42.070 DBClientCursor::init call() failed
server should be down...
Thu Jul 10 09:59:42.073 trying reconnect to 127.0.0.1:27017
Thu Jul 10 09:59:42.076 reconnect 127.0.0.1:27017 failed couldn't connect to server 127.0.0.1:27017
>
Thu Jul 10 09:59:44.193 trying reconnect to 127.0.0.1:27017
Thu Jul 10 09:59:44.194 reconnect 127.0.0.1:27017 failed couldn't connect to server 127.0.0.1:27017
>
> exit
bye


事始め


mongoシェルの起動

インストールが済んだら、まずはコンソールでmongoとだけ入力します。

すると、以下のようなメッセージが出力され、mongoのシェルに入ります。

kkuwana% mongo

MongoDB shell version: 2.4.9
connecting to: test
>

ここで勉強用のデータベースを作成します。

use studyと入力してください。

> use study

switched to db study
>

なんとコレだけで新しいデータベースに接続出来ました!

db.stats()と入力すると現在のデータベースの状態が確認できます。

> db.stats()

{
"db" : "study",
"collections" : 0,
"objects" : 0,
"avgObjSize" : 0,
"dataSize" : 0,
"storageSize" : 0,
"numExtents" : 0,
"indexes" : 0,
"indexSize" : 0,
"fileSize" : 0,
"nsSizeMB" : 0,
"dataFileVersion" : {

},
"ok" : 1
}
>

0ばっかり並んでいます。

では、テストデータを投入します。

何も考えずに以下のコードをコピペしてください。

db.user.insert({name:'mr.a', age:10, gender:'m', hobbies:['programming']});

db.user.insert({name:'mr.b', age:20, gender:'m', hobbies:['vi']});
db.user.insert({name:'ms.c', age:30, gender:'f', hobbies:['programming', 'vi']});
db.user.insert({name:'ms.d', age:40, gender:'f', hobbies:['cooking']});

これで、studyデータベースuserコレクションが作成され、そこに4件のドキュメントが保存されました!

mongoシェルでは、データの操作にJSON形式のコマンドを利用します。

基本構文はdb.コレクション名.コマンド名(オプション)となります。


簡単な用語

上記でいきなりコレクションやドキュメントといった言葉が出てきました。

厳密には異なりますが、イメージとしては以下のような対比になります。

MongoDBとRDBの用語比較(厳密には異なる)

MongoDB
RDB

データベース
データベース

コレクション
テーブル

ドキュメント
行(レコード)

フィールド


findコマンド - ドキュメント(レコード)の取得

では早速データの取得をしてみましょう!ドキュメントの取得にはfindコマンドを利用します。

SQLでいうところのSELECT文ですね。

今回作成したコレクション名はuserなのでコマンドはdb.user.find()にようになります。

早速実行してみましょう!

> db.user.find()

{ "_id" : ObjectId("53be9d74b7fe1603319861e8"), "name" : "mr.a", "age" : 10, "gender" : "m", "hobbies" : [ "programming" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861e9"), "name" : "mr.b", "age" : 20, "gender" : "m", "hobbies" : [ "vi" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861ea"), "name" : "ms.c", "age" : 30, "gender" : "f", "hobbies" : [ "programming", "vi" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861eb"), "name" : "ms.d", "age" : 40, "gender" : "f", "hobbies" : [ "cooking" ] }

全件見事に取得出来ました!

"_id" : ObjectId("53be9d74b7fe1603319861e8")というのはMongoDBが自動で付け足してくれているものです。

RDBMSでいうところの主キーに該当します。なので、通常のフィールドとは異なる特殊なフィールとになります。

では実際に条件を指定して検索結果を絞り込み。。。の前に、そのためには以下のクエリセレクタについて知っておく必要があります。


クエリセレクタ

検索条件に渡す{フィールド名:値}のことです。

例えば、nameフィールドがhogeという値のドキュメントを検索したい場合は{name:'hoge'}というクエリセレクタになります。

クエリセレクタを渡さない場合はすべてのドキュメントが取得されます。

また、SQLのような比較演算子が使えます。

以下が代表的な比較演算子です。

比較演算子

http://docs.mongodb.org/manual/reference/operator/#AdvancedQueries

演算子
MongoDB
概要
サンプル

<
$lt
右辺より小さい
{ age:{$lt:100} }

<=
$lte
右辺以下
{ age:{$lte:100} }

>
$gt
右辺より大きい
{ age:{$gt:100} }

>=
$gte
右辺以上
{ age:{$gte:100} }

!=
$ne
等しくない
{ name:{$ne:'mr.a'} }

該当なし
$exists
フィールド(列)自体の存在チェック
db.mycol.find({ hoge:{$exists:false} })

OR
$or
$orで指定する条件のいずれかを満たすものを抽出
db.mycol.find({$or:[{loves:'apple'},{loves:'energon'}]})


早速取得してみる。

こういったものは実際に手を動かしていくとわかりやすいです。

例えば、名前がms.cの人を抽出してみます。

> db.user.find( {name:'ms.c'} )

{ "_id" : ObjectId("53be9d74b7fe1603319861ea"), "name" : "ms.c", "age" : 30, "gender" : "f", "hobbies" : [ "programming", "vi" ] }

取得出来ました。

では、年齢が20歳以上の人を抽出してみます。

> db.user.find({age:{$gte:20}})

{ "_id" : ObjectId("53be9d74b7fe1603319861e9"), "name" : "mr.b", "age" : 20, "gender" : "m", "hobbies" : [ "vi" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861ea"), "name" : "ms.c", "age" : 30, "gender" : "f", "hobbies" : [ "programming", "vi" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861eb"), "name" : "ms.d", "age" : 40, "gender" : "f", "hobbies" : [ "cooking" ] }

問題ないですね。

なお、複数のクエリセレクタを指定することで、複数条件での絞り込みも当然可能です!

その場合、すべてのクエリセレクタの条件に合致するドキュメントが抽出されます。

SQLでいうところのAND条件ですね

20歳以上の男性を抽出しようとすると以下のようになります。

> db.user.find({age:{$gte:20}, gender:'m'})

{ "_id" : ObjectId("53be9d74b7fe1603319861e9"), "name" : "mr.b", "age" : 20, "gender" : "m", "hobbies" : [ "vi" ] }

できました!

でもじゃあSQLでいうところのORはどうするの?という話になりますね。

そのために上の表にある$or比較演算子を利用します。

$or比較演算子には、配列で、複数のクエリセレクタを渡してあげることになります。

例えば、年齢が20、もしくは40の人を抽出するには以下のようにします。

> db.user.find({$or:[ {age:20}, {age:40} ]})

{ "_id" : ObjectId("53be9d74b7fe1603319861e9"), "name" : "mr.b", "age" : 20, "gender" : "m", "hobbies" : [ "vi" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861eb"), "name" : "ms.d", "age" : 40, "gender" : "f", "hobbies" : [ "cooking" ] }

#ついでに更にそこから男性のみ抽出するには以下のようにする。
> db.user.find({$or:[ {age:20}, {age:40} ], gender:'m'})
{ "_id" : ObjectId("53be9d74b7fe1603319861e9"), "name" : "mr.b", "age" : 20, "gender" : "m", "hobbies" : [ "vi" ] }

簡単ですね!


配列

MongoDBでは配列もお手軽に操作できます。

今回の例で言うとhobbiesというフィールドが配列になっています。

では、programmingが趣味だという人を抽出してみます。

> db.user.find({hobbies:'programming'})

{ "_id" : ObjectId("53be9d74b7fe1603319861e8"), "name" : "mr.a", "age" : 10, "gender" : "m", "hobbies" : [ "programming" ] }
{ "_id" : ObjectId("53be9d74b7fe1603319861ea"), "name" : "ms.c", "age" : 30, "gender" : "f", "hobbies" : [ "programming", "vi" ] }

なんと配列にも関わらず他のフィールド同様の検索ができました!

非常に便利ですね。


取得結果のフィールドを指定する。

取得結果が毎回長くてうんざりですね。今までのドキュメントの取得の仕方は、SQLで言うところのSELECT * FROM ...と同じです。

そこで、MongoDBでは、findの第2引数に取得するフィールド名を指定することができます。

形式は、フィールド名:0位外の何かになります。

> db.user.find({}, {name:1})

{ "_id" : ObjectId("53be9d74b7fe1603319861e8"), "name" : "mr.a" }
{ "_id" : ObjectId("53be9d74b7fe1603319861e9"), "name" : "mr.b" }
{ "_id" : ObjectId("53be9d74b7fe1603319861ea"), "name" : "ms.c" }
{ "_id" : ObjectId("53be9d74b7fe1603319861eb"), "name" : "ms.d" }
>

MongoDBはお節介にも_idとかあったほうが一意にデータを特定できるから便利っしょ?という意味合いからなのか、指定しなくても一緒に取得されてしまいます。

idも表示したくない場合は、{id:0} を指定してあげます。

> db.user.find({}, {name:1, _id:0})

{ "name" : "mr.a" }
{ "name" : "mr.b" }
{ "name" : "ms.c" }
{ "name" : "ms.d" }
>

右辺(値)の数字は0かどうかが重要なようです。

0を指定されたフィールドは表示されないので、長ったらしいObjectIdのみ非表示にしたい場合は{_id:0}のみ指定すればOKです。

これで基本的なデータの取得ができるようになりました!


updateコマンド - ドキュメント(レコード)の更新

findの代わりにupdateコマンドを使うと、ドキュメントの更新ができます。

オプションとして、更新するドキュメントの条件を第1引数に渡して、第2引数に更新する内容を渡してあげます。

db.コレクション名.update({検索条件}, {更新内容})

updateコマンドの第1引数の検索条件の書き方は、findコマンドの検索条件(クエリセレクタ)と全く同じです。

じゃ簡単だな〜。。。と言いたいところですが、1点注意です。

MongoDBのupdateは、第2引数で渡した内容で上書き保存します。

コレは罠です。非常に危険な罠です。

例えばmr.aさんの性別を第3の性別(X)に更新します。

以下のように実行すると全く意図しない結果になってしまいます。

> db.user.update({name:'mr.a'}, {gender:'X'})

> db.user.find({}, {name:1, gender:1, _id:0})
{ "gender" : "X" }
{ "name" : "mr.b", "gender" : "m" }
{ "name" : "ms.c", "gender" : "f" }
{ "name" : "ms.d", "gender" : "f" }

mr.aさんのレコードが消えてしまいました!

実はこれ、mr.aさんの性別を更新したというよりも、名前がmr.aというドキュメントをgender:Xというドキュメントで上書きしたということになります。

なので、mr.aさんのObjectId自体は残っています。そこに紐づくドキュメントが上書きされた形です。

コマンド自体は正しいので当然エラーにはなりません。

じゃあどうするんだよ。。。という話ですが、そのために、$setという修飾子を利用する必要があります。

本来は、$setを使って以下のようにする必要があったわけです。

> db.user.update({name:'mr.a'}, {$set:{gender:'X'}})

これで、第1引数の検索条件に合致したドキュメントの、genderがXに更新されます。

更新するフィールドが複数ある場合もカンマで区切ってあげるだけです。

> db.user.update({name:'mr.a'}, {$set:{gender:'XXX', age:100}})

> db.user.find({}, {name:1, gender:1, _id:0})
{ "gender" : "XXX", "name" : "mr.a" }
{ "name" : "mr.b", "gender" : "m" }
{ "name" : "ms.c", "gender" : "f" }
{ "name" : "ms.d", "gender" : "f" }
>


存在しないフィールドを指定してドキュメントをアップデート

結構重要な点だと思いますが、MongoDBでは、RDBMSと違って簡単にフィールド(カラム、列)の追加ができます。

というのも、コレクション(テーブル)自体にフィールド情報が定義されているわけではなく、あくまでドキュメント(レコード、行)単位にフィールド情報が保持されているためです。

なので、あるドキュメントだけ他のドキュメントと異なるフィールドを持つというようなことが簡単に出来てしまいます!

> db.user.update({name:'mr.b'}, {$set:{hoge:111}})

今までやってきたように普通にupdateコマンドで存在しないフィールドを追加することができます。

上記のコマンドを実行すると、名前がmr.bの人のレコードに、hoge:111という新しいフィールドが追加されます。

コレはデータの新規登録(insertコマンド)の際も同様です。

$exists修飾子を使うと特定のフィールドを持っている/持っていないレコードの抽出ができます。

SQLでのEXISTSはデータをチェックするものですが、MongoDBの$existsはフィールドをチェックするものです。

#フィールド"hoge"を持つものを取得する。

> db.user.find({ hoge:{$exists:true} }, {_id:0})
{ "age" : 20, "gender" : "m", "hobbies" : [ "vi" ], "hoge" : 111, "name" : "mr.b" }
>
>
#フィールド"hoge"を持たないものを取得する。
> db.user.find({ hoge:{$exists:false} }, {_id:0})
{ "name" : "mr.a", "age" : 10, "gender" : "m", "hobbies" : [ "programming" ] }
{ "name" : "ms.c", "age" : 30, "gender" : "f", "hobbies" : [ "programming", "vi" ] }
{ "name" : "ms.d", "age" : 40, "gender" : "f", "hobbies" : [ "cooking" ] }
>


upsert

データが存在しなければINSERT、存在すればUPDATEというあれです。。

使い方は簡単で、updateコマンドの第3引数にtrueを渡すだけ。

db.counter.update( {type:'success'} ,{$inc:{count:1}}, true)

データが存在しない、というのには2つの意味があって、 コレクションが無い場合 と、 該当ドキュメントが無い場合 の2点。

どちらもなければ自動的に作成されるし、あればUPDATEされる。

今回使っている$inc修飾子は、渡されたクエリセレクタのキーの値を、渡された値分インクリメントする、というものです。

実際に実行すると以下のようになります。

> db.counter.update( {type:'error'} ,{$inc:{count:1}}, true)

> db.counter.update( {type:'error'} ,{$inc:{count:1}}, true)
> db.counter.update( {type:'error'} ,{$inc:{count:1}}, true)
> db.counter.update( {type:'success'} ,{$inc:{count:1}}, true)
> db.counter.find()
{ "_id" : ObjectId("53beb256f63db8ea07699841"), "count" : 3, "type" : "error" }
{ "_id" : ObjectId("53beb2caf63db8ea07699842"), "count" : 1, "type" : "success" }
>


複数件同時更新

コレに関しても実は罠があって、例えば全員の性別をXに変更するためには、db.user.update({}, {$set:{gender:'X'}})だけではダメです。

コレだと、MongoDBが最初に見つけた1件のみしか更新されません。

同時に2件以上の該当レコードを一気に更新するには、第4引数にtrueを渡してあげる必要があります。

第3引数にはupsertが絡むので注意。

複数件の更新ということは、基本的に存在しないレコードを考慮する必要など無いはずなので、単純に第3引数はfalse(デフォルト)、第4引数をtrueにすればいいと思います。

> db.user.update({}, {$set:{gender:'X'}}, false, true)

> db.user.find({}, {_id:0})
{ "name" : "mr.a", "age" : 10, "gender" : "X", "hobbies" : [ "programming" ] }
{ "name" : "mr.b", "age" : 20, "gender" : "X", "hobbies" : [ "vi" ] }
{ "name" : "ms.c", "age" : 30, "gender" : "X", "hobbies" : [ "programming", "vi" ] }
{ "name" : "ms.d", "age" : 40, "gender" : "X", "hobbies" : [ "cooking" ] }
>


removeコマンド - ドキュメント(レコード)の削除

ドキュメントの削除です。db.user.remove()という形になります。

引数にクエリセレクタを渡せるので、削除する条件を指定することができます。

例えば、30歳以上の人のドキュメントを削除するには以下のようにします。

> db.user.remove({age:{$gte:30}})

> db.user.find({},{_id:0})
{ "name" : "mr.a", "age" : 10, "gender" : "X", "hobbies" : [ "programming" ] }
{ "name" : "mr.b", "age" : 20, "gender" : "X", "hobbies" : [ "vi" ] }
>


insertコマンド - ドキュメント(レコード)の挿入

コレは今回のサンプルデータを準備する際にすでに使ったので省略


その他


現在接続中のデータベースの情報

db.stats()


sort

{フィールド:1(昇順)}{フィールド:ー1(降順)}

> db.user.find({},{_id:0}).sort({age:1})

{ "name" : "mr.a", "age" : 10, "gender" : "X", "hobbies" : [ "programming" ] }
{ "name" : "mr.b", "age" : 20, "gender" : "X", "hobbies" : [ "vi" ] }
>
>
>
> db.user.find({},{_id:0}).sort({age:-1})
{ "name" : "mr.b", "age" : 20, "gender" : "X", "hobbies" : [ "vi" ] }
{ "name" : "mr.a", "age" : 10, "gender" : "X", "hobbies" : [ "programming" ] }


count

count()

> db.user.find({}).count()

2
> db.user.count()
2

カウント自体に検索条件を渡せるみたいだけど、Groovy大好きな自分としては、あくまでcountはあくまでカウントだけで、findの時点で絞ったほうがスマートだと思う。