Menu

Node.jsでMongoDBを使う:find、insert、updateの例

公式のmongodbドライバーでNode.jsからMongoDBを使います。MongoClientで接続し、コレクションのドキュメントを追加、検索、更新、削除します。すべてasync/awaitで書けます。

このページのコードはエディタで実行できます。編集してすぐに結果を確認できます。

Node.jsからMongoDBを使うには、公式のドライバーをインストールし(npm install mongodb)、new MongoClient(uri) で接続し、client.db('name') でデータベースを、db.collection('name') でコレクションを選びます。どの操作(insertOne、find、updateOne、deleteOne)もPromiseを返すので、await します。ドキュメントは普通のJavaScriptのオブジェクトとして入り、出てきます。

[ 'Dune', 'Neuromancer', 'Hyperion' ] が表示されます。shop データベースも books コレクションも、それまで存在しませんでした。MongoDBは最初の挿入でそれらを作り、toArray() は find() のカーソルが指すすべてのドキュメントを読み込みます。

フィルターとprojectionでドキュメントを検索する

フィルターは、欲しいドキュメントを表すオブジェクトです。普通の値は完全一致で、$gt、$lt、$in、$regex のような演算子はネストしたオブジェクトに書きます。第2引数の projection で、返すフィールドを選びます。

recent には1960年より後の新しい2冊、HyperionとNeuromancerがこの順で、title と year だけを持って入っています。findOne は最初にマッチしたもの(または null)を返し、countDocuments は価格が10以上の本を 2 冊と数えます。$gt: 1960 を $gt: 1985 に変えると、Hyperionだけが残ります。

更新と削除

updateOne(filter, update) は、最初にマッチしたドキュメントを変更します。更新には演算子を使います。$set はフィールドを置き換え、$inc は数値に足し、$unset はフィールドを削除し、$push は配列に追加します。updateMany と deleteMany は、マッチしたすべてに作用します。

更新は1つのドキュメントにマッチして変更したので、Duneは価格が 10、在庫が 2 になり、在庫のない本を削除すると残りは 1 になります。更新には演算子が必須です。$set なしで { price: 10 } を渡すと、ドライバーはそれを拒否します(ドキュメント全体を置き換えるのは replaceOne です)。

ObjectIdとインデックス

すべてのドキュメントは _id を持ちます。自分で設定しなければ、ドライバーが ObjectId を作ります。文字列として届いたid(URLからなど)でドキュメントを探すには、new ObjectId(id) で包みます。インデックスを張るとそのフィールドの検索が速くなり、ユニークインデックスならMongoDBは重複を拒否します。

ObjectId は文字列にすると16進数の24文字で、普通の文字列のまま検索しても何も見つからないので、その行は null を表示します。2回目の挿入はユニークインデックスに違反し、コード 11000 で失敗します。「メールアドレスがすでに使われている」はこうやって検出します。

Express APIでMongoDBを使う

接続はサーバーの起動時に一度だけ行い、コレクションを共有しましょう。リクエストのたびに新しい接続を開いてはいけません。ドライバーは接続のプールを持ち、それを使い回します。

GET /books は2冊とも返し、それぞれに _id が付いています。idを1つコピーして GET /books/<id> に入れれば1冊を取得でき、{"title": "Foundation", "year": 1951} のようなJSONボディで POST /books を送ってから、もう一度一覧を取得することもできます。

実際のアプリでは、接続文字列をコードに書かずに process.env.MONGODB_URI から読みます。

ドライバーの上にスキーマとバリデーションが欲しいなら、Mongoose(npm install mongoose)がドライバーを包んでくれます。

const Book = mongoose.model('Book', new mongoose.Schema({ title: String, year: Number }));
await Book.create({ title: 'Dune', year: 1965 });

よくある間違い

toArray() を忘れる。 find() が返すのはドキュメントではなくカーソルです。そのままログに出すとカーソルのオブジェクトが表示されます。toArray() を呼ぶか、for await で回しましょう。

リクエストのたびに接続する。 new MongoClient はそれぞれ自分のプールを開きます。起動時にクライアントを1つ作り、どこでもそれを使い回しましょう。

スクリプトでクライアントを閉じない。 開いたままのクライアントは、コードが終わった後もプロセスを生かし続けます。スクリプトの最後で await client.close() を呼びましょう。サーバーは動いている間ずっと開いたままにしておきます。

よくある質問

mongodbドライバーとMongooseのどちらを使うべきですか?

ドライバーは公式の薄い層で、普通のオブジェクトとMongoDB独自のクエリ言語で作業します。Mongooseはその上にスキーマ、バリデーション、モデルを足すので、すべてのドキュメントをチェックしたいときに役立ちます。どちらも同じデータベースとやり取りし、Mongooseは内部でドライバーを使っています。

先にデータベースやコレクションを作る必要はありますか?

ありません。MongoDBは、初めてドキュメントを挿入したときに両方を作ります。client.db('shop') と db.collection('books') はハンドルを返すだけです。

接続文字列はどこに置けばいいですか?

MONGODB_URI のような環境変数に置き、process.env.MONGODB_URI で読みます。ホスティングされたデータベースの接続文字列にはパスワードが含まれるので、リポジトリには決してコミットしてはいけません。

idで検索してもfindがドキュメントを返さないのはなぜですか?

_id は文字列ではなく ObjectId として保存されているので、{ _id: '65f1...' } は何にもマッチしません。先に変換しましょう。{ _id: new ObjectId(id) } です。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める