【mongoDB+Node.js】mongoDBの公式Docを読んで軽く触ってみた(つもりが思わぬ沼にハマった話)
はじめに
GraphQLの話をするって前回のブログで宣言したのにちょっと寄り道してmongoDBのお話です。ちょっと触ってみた感想としては、やはりNoSQLは新たに言語を取得する必要がない(今回私はNode.jsを使いました)ので学習コストが低く、それでいて多アクセスからの負荷耐性が強い(だった気がする。違うかも)とくれば人気が出るのも納得です。
今回は筆者のメモ書き程度の記事なので、いつも以上に正確さが疎かに、そしてそれ以上にです/ます調がテキトーになっています。間違い等ございましたらコメントにて指摘いただけると幸いです。
mongoDBのインストール
mongoDBの公式サイトからインストーラをダウンロードする。インストールの途中でInstall MongoD as a Service(公式Doc)と言われたが、Windows Serviceについて何も知らなかったのでとりあえずチェックを外してインストールした。
windowsの場合、ローカルで動かすにはまずデータベースのディレクトリを作る必要がある。
$ cd C:\ $ mkdir \data\db
これでおk。
次に今作ったディレクトリにデータベースのパスを通す(".\MongoDB\Server\4.0\bin"をPATHに追加したものとする)。
$ mongod --dbpath "C:\data\db"
ここで実行するのはmongod.exe
であることに注意。よく調べてないけどmongod
がデータべース自体を起動する実行ファイル、mongo
はデータベースに接続するためのMongo Shellを起動するためのものっぽい。
これで設定は完了。mongod
を起動すると、waiting for connections on port 27017(初期状態)
と出てくるはずである。
mongoDBのデータ形式
mongoDBはデータをBSON(JSONのBinary形式)で記録している。
構造自体はJSONと同じ。
{ field1: value1, field2: value2, ... filedN: valueN }
filed
はstring型。.
や$
も一応使える。
value
は様々な型になる。ObjectId
, Object
, Date type
, array of string
, NumberLong type
などなど。
データ構造
先ほど挙げた{ fieldN: valueN }
のひとまとめを、documentという(たぶん)。そしてこれらの集合をcollectionと呼び、RDBでいうtableにあたる(たぶん)。
mongoDBはこの collection を多数集めたものでひとつのデータベースを構築している。
図示するとこんな感じ
db --- collection --- documents --- documents --- ... --- collection --- documents --- documents --- ... --- ... db --- collection --- ...
mongoDB + Node.jsでCRUD
公式ドキュメントを読んだので簡単にまとめてみる。特に発展的なことは何もしていないです。
準備
npmでmongodb
をインストールしておく
$ npm i -l mongodb
データベースへの接続の仕方は以下の通り。
const mongodb = require("mongodb") const Client = mongodb.MongoClient Client.connect("mongodb://127.0.0.1:27017/myDb", (err, db) => { // ここに以下のCRUDを書いていく })
CREATE = Insert Documents
最も簡単なのはCollection.insertOne()
を用いる方法。
awit db.collection('inventory').insertOne({ item: 'canvas', qty: 100, tags: ['cotton'], size: {h: 28, w: 35.5, uom: 'cm'} });
ちなみに戻り値はPromise
型。また複数のdocumentsを挿入するCollection.insertMany()
も存在する。
READ = Query Documents
documentsを読むAPIとしてはCollection.find()
を用いることができる。
次の例1のように空のオブジェクトを渡すと、inventorycollectionにあるdocumentsすべてを呼び出すことができる。例2はANDを、例3はさらにORのクエリを用いている。
// example 1 const cursor = db.collection('inventory').find({}) // example 2 const cursor = db.collection('inventory').find({ status: "A", qty: { $lt: 30 } }) // example 3 const cursor = db.collection('inventory').find({ status: "A", $or: [{ qrt: { $lt: 30 } }, { item: { $regex: '^p' } }] })
ここで$lt
とあるが、これはless thanを表す演算子。$
+αのかたちはクエリやアップデートの演算子によく見られる(詳細はこちら)。
ちなみにこのメソッドはSQLの次の文に対応しているっぽい。 ```sql
example 1
SELECT * FROM inventory
example 2
SELECT * FROM inventory WHERE status = "A" AND qty < 30
example 3
SELECT * FROM inventory WHERE status = "A" AND ( qty < 30 OR item LIKE "p%" ) ``` SQLの文法は分からないがまぁそんな感じがする。
UPDATE = Update Documents
Collection.updateOne(filter, update[, options, callback])
を使っていきます。insertOne()
やfind()
と異なり、第一引数にfliterを渡し、更新するdocumentsを選択します。そして第二引数に更新内容を書きます。
また、updateの文にはupdate operatorを使い、更新内容を記述します。
// 以下のようなinventorycollectionが存在するとする。 // { // item: 'notebook', // qty: 50, // size: { h: 8.5, w: 11, uom: 'in' }, // status: 'P' // }, // { // item: 'paper', // qty: 100, // size: { h: 8.5, w: 11, uom: 'in' }, // status: 'D' // }, // { // item: 'planner', // qty: 75, // size: { h: 22.85, w: 30, uom: 'cm' }, // status: 'D' // } await db.collection("inventory").updateOne( { item: "paper" }, { $set: { 'size.uom': 'cm', status: 'P'}, $currentDate: {lastModified: true} } )
ちなみにupdateOne()
が用いられたとき、filterによって複数のdocumentsが抽出された場合は一番初めにfilterされたdocumentがupdateの対象となる。
もちろん複数のdocumentsを更新するupdateMany()
もあり、また書き込みではなく置き換えを行うreplaceOne()
も存在する。
※1 書き込みの操作はAtomicallyに行われるらしい。OSで勉強したことが活きてくるネ!
※2 _id fieldの書き換えは禁止されているらしい。
※3 optionにupsert: true
を加えておくと、filteringされたdocumentsがあればそれを更新し、見つからなければ新規に作成する。UPDATE + INSERTの造語(?)らしい。
DELETE = Delete Documents
URLはremoveなんですね笑 最後はDeleteです。Collection.deleteOne(filter[, options, callback])
またはCollection.deleteMany(filter[, options, callback])
を使えばおk。これはfilterを引数に渡してやることで、該当のdocumentsを削除するというもの。
「よっしゃ!サンプルプログラム動かすぞ!」→いのでん「エラーだと?貴様この野郎......」
サンプルコードが動かない事態にいのでんが激怒した理由がやばすぎる...ブログの読者も動揺を隠せない最悪な事態に一同驚愕!!!
はい、執筆現在深夜2時のテンションでお送りしておりますが、上記のとおりサンプルコードそのままではエラーを吐いて動きません。
そのコードがこちら。
// Example of a simple insertMany operation var MongoClient = require('mongodb').MongoClient, test = require('assert'); MongoClient.connect('mongodb://localhost:27017/test', function(err, db) { // Get the collection var col = db.collection('insert_many'); col.insertMany([{a:1}, {a:2}], function(err, r) { test.equal(null, err); test.equal(2, r.insertedCount); // Finish up test db.close(); }); });
これを実行すると、以下のように怒られます。
C:\ (略) \node_modules\mongodb\lib\operations\mongo_client_ops.js:474 throw err; ^ TypeError: db.collection is not a function at insertDocs (C:\ (略) \test_mongodb.js:20:8) at Client.connect (C:\ (略) \test_mongodb.js:9:5) at result (C:\ (略) \node_modules\mongodb\lib\utils.js:414:17) at executeCallback (C:\ (略) \node_modules\mongodb\lib\utils.js:406:9) at err (C:\ (略) \node_modules\mongodb\lib\operations\mongo_client_ops.js:294:5) at connectCallback (C:\ (略) \node_modules\mongodb\lib\operations\mongo_client_ops.js:249:5) at process.nextTick (C:\ (略) \node_modules\mongodb\lib\operations\mongo_client_ops.js:471:7) at process._tickCallback (internal/process/next_tick.js:61:11)
どうやらdb.collection()
が原因らしい。筆者はjavascriptが最もまともに書ける言語と自称している割に、Promiseを深く理解しきれていないのできっと自分の書き方が悪いんだろう...とはじめは思いましたが、このエラーはどう考えても変数db
が予想されるものと確実に異なります。諦めてgoogle大先生に聞いてみると、こちらの記事が見つかりました。以下その記事より引用。
さっきnpmインストールしていつも通り接続しようとしたら TypeError: db.collection is not a function になるので調べてみたら、3系から仕様が変わったみたいです。
MongoClient.connect now returns a Client instead of a DB http://mongodb.github.io/node-mongodb-native/3.0/upgrade-migration/main/
今のversion、4.0なんですが、、、
ということで、MongoClient.connectの返り値が変更されていたようですね。
(以下のコードも先ほどの記事から引用させていただいております。)
MongoClient.connect(_url, (err, client) => { // callbackに渡されるオブジェクトが変わった // db名を明示的に指定してdbオブジェクトを取得する必要がある const db = client.db('heroku_xxxxxxxx'); db.collection('foobar', (err, collection) => { collection.find().toArray((err, docs) => { : : }); }); });
~3.0とcallbackに渡されるオブジェクトが変更され、dbではなくclientと同じものが返ってくるようになりました。それに伴いcallback関数の中で明示的にdb
名を指定してあげる必要ができます。
ちなみにlocalで実行している場合、MongoClient.db()
の引数にIPv4のかたちで渡すことはできないので"localhost"でおk。
こんな大事な仕様変更、ちゃんとサンプルコードにも反映させておいてくださいよ…
ちょっと引っかかったこと
上記の仕様変更をmongo shellと行き来しながら確認している間、いくつか不明点が生じた。
自分が行ったことを事細かに記しておく。
/* test_mongodb.js内 */ const db = client.db("localhpst") // とtypoしてしまった。この時mongo shellでdbを見てみると
$ mongo > show dbs admin config localhpst # typoしたDBが存在 testDb > db.localhpst.find({}) > --- # empty > db.testDb.find({}) > --- # empty
次に
/* test_mongodb.js内 */ const db = client.db("localhost") // typoを修正。この時
$ mongo > show dbs admin config localhost # 新たにlocalhostが追加されていた localhpst testDb > db.localhost.find({}) --- # empty > db.testDb.find({}) --- # empty!!! # この時移動する前に`db`コマンドでどこにいたのか確認したらよかったと後悔。 > use testDb > db.localhost.find({}) --- # empty > db.testDb.find({}) {"_id" : ObjectId("..."), "a" : 3 } # テスト用にmongo shellからinsertしていたものを発見。個人的にはここで{ "a" : 4 }{ "a" : 5 }が出てくるもの思っていただけに混乱してしまった。 > use localhost > db.localhost.find({}) --- # empty > db.testDb.find({}) {"_id" : ObjectId("..."), "a" : 4 } {"_id" : ObjectId("..."), "a" : 5 } # test_mongodb.js内でinsertしていたもの
(はてなではハイライトされていないかもしれません。読みづらくてごめんなさい。)
要はデータベース名とコレクション名を区別できていなかったため、自分が意図した(結果的に勘違いしていたわけだが)コマンドで、documentsが表示されなかったのである。
どうやらMongoClient.db()
は引数にとったものをUPSERTで受けとるようだ。このメソッドの引数や、mongo shellでのdb
コマンドやshow dbs
で出力されるものは総じてデータベース名であるが、検索で指定するのはコレクション名であり、指定のデータベース下にいることは確保されていなければならない。その意味ではmongoDB v3からの仕様変更は、データベース名を明記する必要性があり確かなメリットが存在するので、手間などではなく改良されたものであることが頷ける。
P.S.
軽い気持ちでmongoDBをインストールしあれこれイジっていたら、こんな時間(am3:34)になってしまった。進捗産めたのはいいけど、翌日に響くからみんなは早く寝ようね。睡眠大切。