ゆっくりのんびり。

いの (@inox_ee) です

【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大先生に聞いてみると、こちらの記事が見つかりました。以下その記事より引用。

qiita.com

さっき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)になってしまった。進捗産めたのはいいけど、翌日に響くからみんなは早く寝ようね。睡眠大切。