アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

MongoDB入門

※「MongoDB入門する人向け記事」じゃなくて「自分がMongoDB入門してみた記事」です

色々簡単な操作をしてみたのでメモ。
データベースはSQLServerMySQLしか触ったことないSQL脳なので、概念理解するのが大変そう。
あ、SASも一応触ったことあるけどノーカンで。あれもわからんかった。

SQLで言うところの「データベース」はMongoでも「データベース」、
「テーブル」は「コレクション」、
「レコード/行」は「ドキュメント」、
「フィールド/列」は「フィールド」と呼ぶようです。

mongoシェルの起動

mongo

これだけでシェルが起動。以降はシェルでの操作。
末尾のセミコロンはあってもなくても動く模様?
SQLと同じかな…複数クエリを同時に打つ時にはセミコロンで区切りが必要的な)
とりあえず、なくても良いときは省略してます。

データベース一覧の表示

show dbs

デフォルトで「admin」「local」の2つが入っていました。
管理用と思われるので触れないことにします。adminは空でlocalは何か入っている模様。

データベースの切り替え

use データベース名

既に存在する場合も、まだ存在しない場合も、入力したデータベース名で操作できるようになります。
つまりスペルミスに注意。
タブキーで入力補助…なんていうのもなかったので、切り替え後は↓をとりあえず打ってみる癖を付けておいたほうがいいかもしれない。
尚、ドキュメントを作成するまではデータベースもコレクションも作られないようです。
(尚、ドキュメントを削除した場合に空のコレクションは残るようだったので、一旦ドキュメントを作成→削除することで空のデータベースの作成自体は可能な模様。それが必要かどうかは置いておいて。)

コレクション一覧の表示

show collections

操作中データベースのコレクション一覧が表示されます。
最初のコレクションが作成されると同時に「system.indexes」というのも勝手に増えますが、これはSQLで言うところのテーブルではなく、名前通りインデックス情報の模様?

ドキュメント一覧の表示

db.コレクション名.find()

操作中データベースの指定コレクションに含まれるドキュメント一覧を表示します。
要するに「SELECT * FROM テーブル名」的なやつ。
これも、存在しないコレクション名を指定してもエラーなど出ません。
ただ、こちらはタブキーで入力補助してくれました。長いテーブル名でもちょっと安心。

検索とかの詳しいやつはあとで。
とりあえず次は追加・更新・削除を。

ドキュメントの追加

db.コレクション名.insert({フィールド名1:値1, フィールド名2:値2, ...})

指定したコレクションに指定したフィールド名で指定した値をぶちこみます。
これまたコレクション名やらフィールド名やらのスペルミスに注意。
文字列の場合はクォーテーション(シングルでもダブルでも可)で囲みます。
フィールド名は囲んでも囲まなくてもいいっぽい。

尚、値に「null」を指定すれば普通にnullになります。
フィールド名ごと指定しない場合ももちろん何も入りませんが、やや扱いが違う模様?
参考:http://blog.kjirou.net/p/4030

とりあえず追加して、もう一度findしてみると、追加した値と別に、「_id」というフィールドが追加されていました。値は24桁の16進数(たぶん)です。どうやら自動で付けられるプライマリキー的なもののようですね。
ただし、単純な連番になるわけではない模様。
(試した限りでは、一つのコレクションに続けて複数ドキュメントを追加した際、下位の桁だけは連番っぽかったが、6~8桁目が異なっていた。また、一旦mongoシェルを終了・開始して追加してみると全然違う値が入った)

また、配列を入れることも可能です。
子要素にもフィールド名を付ける場合

db.コレクション名.insert({親フィールド名1:{子フィールド名1:子値1, 子フィールド名2:子値2, ...}})

値だけを入れる場合

db.コレクション名.insert({親フィールド名1:[子値1, 子値2, ...]})

地味にカッコが別になっていることに注意。
(これは間違えてもエラーになってくれますが)

いやーこれはおいしい。
例えばユーザごとの好きな色(複数可)を保存するのに、SQLだと以下のようにテーブルを分けていたところですが

user_id name
1 Alice
2 Bobby
3 Cathy
user_id color
1 red
1 blue
2 green
3 orange
3 pink

Mongoなら
{ "user_id" : 1, "name" : "Alice", "color" : [ "red, blue"] }
{ "user_id" : 2, "name" : "Bobby", "color" : [ "green" ] }
{ "user_id" : 3, "name" : "Cathy", "color" : [ "orange", "pink" ] }
で済ませることができますからね。

(まあ、SQLでもデータの使い方によってはカンマ区切りの文字列で保存するなり、フィールドを複数用意するなりという方法も取れますが…あまりカッコよくないので…)

ドキュメントを更新① saveを使う方法

var 変数名 = db.コレクション名.find(条件).toArray()[0]
変数名.フィールド名 = 変更後の値
db.コレクション名.save(変数名)

こう、しれっと変数とか使えるのすごいですね。
変数に条件に合ったドキュメントの0個目を格納し、
それを上書きし、
その変数の内容でドキュメントを上書き…という感じのようです。

↓ちなみにこんな感じで試してみました。
var doc = db.test.find({user_id : 2}).toArray()[0]
doc.name = "Belle"
db.test.save(doc)

ドキュメントを更新② updateを使う方法

db.コレクション名.update(条件, 変更後のデータ(丸ごと)[, upsertするかどうか(true/false)[, 条件に合致した行をすべて更新するかどうか(true/false)]])

こっちのが文法としては分かりやすくはありますね。
ただ、このままだと、変更後のデータはすべて書く必要がある模様。

例えば↓のデータについて(_idは省略)
{ "user_id" : 2, "name" : "Belle", "color" : [ "green" ] }

↓とやると
db.test.update({user_id:2}, {user_id:2, name:"Bobby"})

↓となってしまいます(colorが消える!)
{ "user_id" : 2, "name" : "Bobby" }

また、「upsertするかどうか」をtrueにすると、条件に合うドキュメントがなかった場合にINSERTされます。
falseであれば、条件に合うものがなければ何も更新されません。デフォルトはfalseです。

「条件に合致した行をすべて更新するかどうか」は、これをtrueにするだけでなく、書き方を少し変える必要があるようです。

db.test.update({user_id:2}, {$set:{user_id:2, name:"Bobby", color:["light-green"]}}, false, true)

変更内容を「{$set: ~ }」で囲んでいます。
これで、user_id=2のドキュメントが複数あれば、すべて指定条件で上書きされます。
尚、これもデフォルトはfalseです。falseであれば最初の1レコードのみが更新されるようです。

変更後のデータをすべて書くとかめんどくさすぎるだろ…と思ったら、修飾子を付けることで色々便利な使い方ができるようです。

ドキュメントを更新③ updateで一部のフィールドだけ更新

db.コレクション名.update(条件, {$set:変更内容})

例えば
db.test.update({user_id:2}, {$set:{name:"Belle", color:["yellow", "green"]}})
という感じ。
あっこの$setというの、さっき出てきたやつと一緒ですね。
ちなみに、例によって存在しないフィールドを指定するとそのフィールド名で追加されてしまいますので、やはりスペルミスには(ry

ドキュメントを更新④ updateであるフィールドの値をインクリメント

db.コレクション名.update(条件, {$inc:{フィールド名:インクリメントする数}})

文字列フィールドに使ったりすると普通にエラーになります。
ただし存在しないフィールドを指定するとそのフィールド名で追加されてし(ry

ドキュメントを更新⑤ フィールド自体を削除

db.コレクション名.update(条件, {$unset:{削除するフィールド名: 何か適当な値}})

何故か値を指定する必要あり。なんでや。

ドキュメントを更新⑥ 配列フィールドに値を追加

db.コレクション名.update(条件, {$push:{追加先フィールド名: 追加する値}})
db.コレクション名.update(条件, {$pushAll:{追加先フィールド名: 追加する値の配列}})

前者は値を1つだけ追加する方法。
また、「$push」の代わりに「$addToSet」とすると、重複になる場合に追加しません。
後者がまとめて追加です。
配列の子要素としてさらに配列ができるわけではありません。
…が!
ここで「$pushAll」を「$addToSet」に変えてみると、子要素として配列が追加されました。マジかよ!
複数追加(ただし重複する場合は無視)」しようと思ったら、1つずつ投入する必要が…?

ドキュメントを更新⑦ 配列フィールドから値を削除

db.コレクション名.update(条件, {$pop: {フィールド名: 末尾or先頭(正負の整数)}})

正の値で末尾、負の値で先頭が削除されます。
(例えば1で末尾、-1で先頭。何番目かを選ぶことはできない)

db.コレクション名.update(条件, {$pull: {フィールド名: 値}})

特定の値を削除する場合はこちら。

ドキュメントを検索① 取得するフィールドを指定

db.コレクション名.find({}, 表示/非表示のフィールド指定)

「表示/非表示のフィールド指定」というのは、以下の例のように指定します。

  • 「_id」と「hoge」を出す → {hoge:1}
  • 「_id」は出さず「hoge」のみ出す → {_id:0, hoge:1}
  • 「_id」以外の全フィールドを出す → {_id:0}

ドキュメントを検索② 取得するドキュメントの条件を指定

db.コレクション名.find(条件)

はい、ここまできたらまあ想像は付きました。
案の定、間違ってても何も言ってくれないのでスペルミスにはry

配列も検索可能です。
ちょっと複雑なので実際の例で書きます。

// 以下のコレクションtestに対して検索("_id"は省略)
// { "id" : 1, "arr" : [ "a" ] }
// { "id" : 2, "arr" : [ "a", "b" ] }
// { "id" : 3, "arr" : [ "a", "b", "c" ] }
// { "id" : 4, "arr" : { "hoge" : "a", "fuga" : "b", "piyo" : "c" } }
// { "id" : 5, "arr" : { "hoge" : "a", "fuga" : "b" } }
// { "id" : 6, "arr" : { "hoge" : "a" } }

// id=2 のみヒット(完全一致で検索。順序も合っている必要あり)
db.test.find({arr:["a", "b"]})

// id=2,3 がヒット(部分一致で検索)
db.test.find({arr:"b"})

// id=5 のみヒット(完全一致で検索。順序も合っている必要あり)
db.test.find({arr:{hoge:"a", fuga:"b"}})

// id=4,5 がヒット(部分一致で検索。「arr.fuga」部分はクォート必須)
db.test.find({"arr.fuga":"b"})

す、すげー! 超便利だこれ!
(さっきから配列処理にしか感動してない気がしますが)すげー!
完全一致で検索する際は順序も関わってくるので、
insertでもfindでもプログラム側でソートしておくとか、工夫はいるかもですね。

ドキュメントを検索③ AND/OR検索

こちらもさっきの例を用いて。

// AND検索:id=2 のみヒット(id=2 かつ arrに"b"が含まれる)
db.test.find({id:2, arr:"b"})

// OR検索:id=2,4,5 がヒット(id=2 もしくは arr.fuga="b")
db.test.find({$or:[{id:2}, {"arr.fuga":"b"}]})

// ANDとORの合わせ技検索:id=4 がヒット((d=2 もしくは arr.fuga="b") かつ arr.piyo="c")
db.test.find({"$or": [{id:2}, {"arr.fuga":"b"}], "arr.piyo":"c"})

AND検索は条件をカンマ区切りで複数書くだけ。
OR検索はちょっと面倒ですね。
あまり複雑な条件だと面倒そう。

ドキュメントを検索④ 取得するドキュメントの条件を指定(完全一致じゃない比較いろいろ)

// 非等価(フィールド ≠ 値)
db.コレクション名.find({フィールド名:{$ne:値}})

// 範囲指定①(フィールド > 値)
db.コレクション名.find({フィールド名:{$gt:値}})

// 範囲指定②(フィールド ≧ 値)
db.コレクション名.find({フィールド名:{$gte:値}})

// 範囲指定③(フィールド < 値)
db.コレクション名.find({フィールド名:{$lt:値}})

// 範囲指定④(フィールド ≦ 値)
db.コレクション名.find({フィールド名:{$lte:値}})

// 部分一致(LIKE検索的なやつ)
db.コレクション名.find({フィールド名: /正規表現/})

// いずれかに一致(SQLでのIN句)
db.コレクション名.find({フィールド名: {$in: [値1, 値2, ...]}})

// 値があるドキュメントの検索(SQLでの IS NOT NULL)
db.コレクション名.find({フィールド名:{$exists:true}})

// 値がないドキュメントの検索(SQLでの IS NULL)
db.コレクション名.find({フィールド名:{$exists:false}})

ドキュメントを検索⑤ ソートの指定

db.コレクション名.find(条件).sort({フィールド名:昇順降順の指定})

昇順降順の指定は1/-1で行います。
↓のような感じですね。
db.test.find().sort({id:1})
db.test.find().sort({id:-1})

ドキュメントを検索⑥ ちょっとした集計系

// ドキュメント件数を取得(条件がない場合はfind省略可)
db.コレクション名.find(条件).count()

// 特定のフィールドのうち重複した値を除いたものを取得
db.コレクション名.distinct("フィールド名")

// 特定のフィールドのうち重複した値を除いたものの件数を取得
db.コレクション名.distinct("フィールド名").length

ドキュメントを検索⑦ LIMIT,OFFSETの指定

db.コレクション名.find(条件).skip(オフセット).limit(取得件数)

skip,limitはいずれか片方のみ指定することもできます。

ドキュメントを削除

db.コレクション名.remove(条件)

こちらは難しいことをしなくとも、条件に合致するドキュメントが複数あれば全部削除されます。
また、きちんと調べていないのですが、「$atomic」という修飾子があるようです。
条件に「, $atomic:true」という記述を追加するのですが…
一部の削除に失敗した場合はいずれも削除しない、ということですかね?

また、全件削除したい場合、条件を「{}」とします。

コレクションを削除

db.コレクション名.drop()

データベースを削除

db.dropDatabase()