Node.jsからMongoDBを使うには、公式のドライバーをインストールし(npm install mongodb)、new MongoClient(uri) で接続し、client.db('name') でデータベースを、db.collection('name') でコレクションを選びます。どの操作(insertOne、find、updateOne、deleteOne)もPromiseを返すので、await します。ドキュメントは普通のJavaScriptのオブジェクトとして入り、出てきます。
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const books = client.db('shop').collection('books');
await books.insertMany([
{ title: 'Dune', author: 'Herbert', year: 1965, price: 12 },
{ title: 'Neuromancer', author: 'Gibson', year: 1984, price: 9 },
{ title: 'Hyperion', author: 'Simmons', year: 1989, price: 11 },
]);
const all = await books.find().toArray();
console.log(all.map((b) => b.title));
await client.close();Master Node.js the hands-on way
Interactive lessons, real projects, and instant feedback.
START NOW
[ 'Dune', 'Neuromancer', 'Hyperion' ] が表示されます。shop データベースも books コレクションも、それまで存在しませんでした。MongoDBは最初の挿入でそれらを作り、toArray() は find() のカーソルが指すすべてのドキュメントを読み込みます。
フィルターとprojectionでドキュメントを検索する
フィルターは、欲しいドキュメントを表すオブジェクトです。普通の値は完全一致で、$gt、$lt、$in、$regex のような演算子はネストしたオブジェクトに書きます。第2引数の projection で、返すフィールドを選びます。
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const books = client.db('shop').collection('books');
await books.insertMany([
{ title: 'Dune', author: 'Herbert', year: 1965, price: 12 },
{ title: 'Neuromancer', author: 'Gibson', year: 1984, price: 9 },
{ title: 'Hyperion', author: 'Simmons', year: 1989, price: 11 },
{ title: 'Foundation', author: 'Asimov', year: 1951, price: 8 },
]);
const recent = await books
.find({ year: { $gt: 1960 } }, { projection: { _id: 0, title: 1, year: 1 } })
.sort({ year: -1 })
.limit(2)
.toArray();
recent.forEach((b) => console.log(b));
const cheap = await books.findOne({ price: { $lt: 10 }, author: 'Asimov' });
console.log(cheap.title);
console.log(await books.countDocuments({ price: { $gte: 10 } }));
await client.close();
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 は、マッチしたすべてに作用します。
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const books = client.db('shop').collection('books');
await books.insertMany([
{ title: 'Dune', price: 12, stock: 3 },
{ title: 'Neuromancer', price: 9, stock: 0 },
]);
const res = await books.updateOne(
{ title: 'Dune' },
{ $set: { price: 10 }, $inc: { stock: -1 } }
);
console.log(res.matchedCount, res.modifiedCount);
const dune = await books.findOne({ title: 'Dune' });
console.log(dune.price, dune.stock);
const del = await books.deleteOne({ stock: 0 });
console.log('deleted', del.deletedCount, 'left', await books.countDocuments());
await client.close();
更新は1つのドキュメントにマッチして変更したので、Duneは価格が 10、在庫が 2 になり、在庫のない本を削除すると残りは 1 になります。更新には演算子が必須です。$set なしで { price: 10 } を渡すと、ドライバーはそれを拒否します(ドキュメント全体を置き換えるのは replaceOne です)。
ObjectIdとインデックス
すべてのドキュメントは _id を持ちます。自分で設定しなければ、ドライバーが ObjectId を作ります。文字列として届いたid(URLからなど)でドキュメントを探すには、new ObjectId(id) で包みます。インデックスを張るとそのフィールドの検索が速くなり、ユニークインデックスならMongoDBは重複を拒否します。
import { MongoClient, ObjectId } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const users = client.db('app').collection('users');
await users.createIndex({ email: 1 }, { unique: true });
const { insertedId } = await users.insertOne({ email: 'ada@example.com' });
const idString = insertedId.toString();
console.log(idString.length);
const found = await users.findOne({ _id: new ObjectId(idString) });
console.log(found.email);
console.log(await users.findOne({ _id: idString }));
try {
await users.insertOne({ email: 'ada@example.com' });
} catch (err) {
console.log('duplicate, error code', err.code);
}
await client.close();
ObjectId は文字列にすると16進数の24文字で、普通の文字列のまま検索しても何も見つからないので、その行は null を表示します。2回目の挿入はユニークインデックスに違反し、コード 11000 で失敗します。「メールアドレスがすでに使われている」はこうやって検出します。
Express APIでMongoDBを使う
接続はサーバーの起動時に一度だけ行い、コレクションを共有しましょう。リクエストのたびに新しい接続を開いてはいけません。ドライバーは接続のプールを持ち、それを使い回します。
// GET /books
import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const books = client.db('shop').collection('books');
// Setup: seed two books
await books.insertMany([
{ title: 'Dune', year: 1965 },
{ title: 'Hyperion', year: 1989 },
]);
const app = express();
app.use(express.json());
app.get('/books', async (req, res) => {
const list = await books.find().sort({ year: 1 }).toArray();
res.json(list);
});
app.get('/books/:id', async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(400).json({ error: 'Bad id' });
}
const book = await books.findOne({ _id: new ObjectId(req.params.id) });
if (!book) return res.status(404).json({ error: 'Not found' });
res.json(book);
});
app.post('/books', async (req, res) => {
const { insertedId } = await books.insertOne({ title: req.body.title, year: req.body.year });
res.status(201).json({ _id: insertedId });
});
app.listen(3000);
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() を呼びましょう。サーバーは動いている間ずっと開いたままにしておきます。