Mongoose の使い方完全ガイド — MongoDB ODMの定番ライブラリ
一言でいうと
MongooseはMongoDB用のODM(Object Document Mapper)ライブラリで、スキーマ定義・バリデーション・型安全なクエリ構築をNode.js上で実現します。MongoDBのドキュメントをJavaScript/TypeScriptのオブジェクトとして直感的に操作できるようにする、事実上の標準ライブラリです。
どんな時に使う?
- MongoDBを使ったWebアプリケーション開発 — Express/Fastify/NestJSなどと組み合わせて、データ層をスキーマベースで構築したい場合
- ドキュメント構造にバリデーションやデフォルト値を持たせたい場合 — MongoDBはスキーマレスですが、アプリケーション側でデータの整合性を担保したいケースは多いです
- リレーション的なデータ参照が必要な場合 —
populateを使ったドキュメント間の擬似JOINで、RDBライクなデータ取得を実現したい場合
インストール
事前にNode.jsとMongoDBのインストールが必要です。
# npm
npm install mongoose
# yarn
yarn add mongoose
# pnpm
pnpm add mongoose
TypeScriptの型定義はパッケージに同梱されているため、@types/mongoose は不要です。
基本的な使い方
最もよく使うパターンとして、接続→スキーマ定義→モデル作成→CRUD操作の一連の流れを示します。
import mongoose, { Schema, model, InferSchemaType } from 'mongoose';
// 1. MongoDBに接続
await mongoose.connect('mongodb://127.0.0.1:27017/my_app');
// 2. スキーマを定義
const userSchema = new Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
age: { type: Number, min: 0 },
role: { type: String, enum: ['admin', 'user', 'editor'], default: 'user' },
createdAt: { type: Date, default: Date.now },
});
// 3. スキーマから型を推論(TypeScript)
type User = InferSchemaType<typeof userSchema>;
// 4. モデルを作成
const UserModel = model<User>('User', userSchema);
// 5. ドキュメントを作成・保存
const newUser = await UserModel.create({
name: '田中太郎',
email: 'tanaka@example.com',
age: 30,
});
console.log(newUser._id); // ObjectId が自動付与される
// 6. ドキュメントを検索
const users = await UserModel.find({ role: 'user' });
console.log(users);
// 7. 接続を閉じる
await mongoose.disconnect();
ポイント: Mongooseは接続完了前でもクエリをバッファリングしてくれるため、
connectの完了を待たずにモデル定義やクエリ実行のコードを書いても動作します。ただし、本番環境では接続エラーのハンドリングを必ず行いましょう。
よく使うAPI
1. mongoose.connect() — データベース接続
import mongoose from 'mongoose';
// 基本的な接続
await mongoose.connect('mongodb://127.0.0.1:27017/my_app');
// オプション付き接続
await mongoose.connect('mongodb://127.0.0.1:27017/my_app', {
maxPoolSize: 10, // コネクションプールの最大数
serverSelectionTimeoutMS: 5000, // サーバー選択タイムアウト
socketTimeoutMS: 45000, // ソケットタイムアウト
});
// 接続イベントの監視
mongoose.connection.on('connected', () => console.log('MongoDB connected'));
mongoose.connection.on('error', (err) => console.error('MongoDB error:', err));
mongoose.connection.on('disconnected', () => console.log('MongoDB disconnected'));
2. Schema — スキーマ定義とバリデーション
import { Schema, model, Types } from 'mongoose';
const blogPostSchema = new Schema({
title: {
type: String,
required: [true, 'タイトルは必須です'],
maxlength: [200, 'タイトルは200文字以内にしてください'],
trim: true,
},
body: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
tags: [{ type: String, lowercase: true }],
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft',
},
viewCount: { type: Number, default: 0, min: 0 },
metadata: {
likes: { type: Number, default: 0 },
shares: { type: Number, default: 0 },
},
}, {
timestamps: true, // createdAt, updatedAt を自動付与
collection: 'blog_posts', // コレクション名を明示的に指定
});
// インデックスの定義
blogPostSchema.index({ title: 'text', body: 'text' }); // テキスト検索用
blogPostSchema.index({ author: 1, createdAt: -1 }); // 複合インデックス
// 仮想プロパティ
blogPostSchema.virtual('summary').get(function () {
return this.body?.substring(0, 100) + '...';
});
// インスタンスメソッド
blogPostSchema.methods.publish = function () {
this.status = 'published';
return this.save();
};
// 静的メソッド
blogPostSchema.statics.findByAuthor = function (authorId: Types.ObjectId) {
return this.find({ author: authorId }).sort({ createdAt: -1 });
};
const BlogPost = model('BlogPost', blogPostSchema);
3. CRUD操作 — 作成・読取・更新・削除
// --- Create ---
// 単一ドキュメント作成
const post = await BlogPost.create({
title: 'Mongoose入門',
body: 'MongooseはMongoDB用のODMです...',
author: userId,
tags: ['mongodb', 'nodejs'],
});
// 複数ドキュメント一括作成
await BlogPost.insertMany([
{ title: '記事1', body: '本文1', author: userId },
{ title: '記事2', body: '本文2', author: userId },
]);
// --- Read ---
// 条件検索
const posts = await BlogPost.find({ status: 'published' })
.sort({ createdAt: -1 })
.limit(10)
.skip(0)
.select('title author createdAt') // 取得フィールドを限定
.lean(); // プレーンオブジェクトとして取得(高速)
// IDで1件取得
const single = await BlogPost.findById('64a1b2c3d4e5f6a7b8c9d0e1');
// 条件で1件取得
const latest = await BlogPost.findOne({ status: 'published' })
.sort({ createdAt: -1 });
// 件数取得
const count = await BlogPost.countDocuments({ status: 'published' });
// --- Update ---
// 1件更新(更新後のドキュメントを返す)
const updated = await BlogPost.findByIdAndUpdate(
postId,
{ $set: { title: '更新後のタイトル' }, $inc: { viewCount: 1 } },
{ new: true, runValidators: true } // new: true で更新後を返す
);
// 複数件更新
await BlogPost.updateMany(
{ status: 'draft', createdAt: { $lt: new Date('2024-01-01') } },
{ $set: { status: 'archived' } }
);
// --- Delete ---
await BlogPost.findByIdAndDelete(postId);
await BlogPost.deleteMany({ status: 'archived' });
4. populate() — ドキュメント間の参照解決(擬似JOIN)
const postSchema = new Schema({
title: String,
author: { type: Schema.Types.ObjectId, ref: 'User' },
comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }],
});
const commentSchema = new Schema({
body: String,
author: { type: Schema.Types.ObjectId, ref: 'User' },
post: { type: Schema.Types.ObjectId, ref: 'BlogPost' },
});
// 基本的なpopulate
const postWithAuthor = await BlogPost.findById(postId)
.populate('author'); // author フィールドが User ドキュメントに展開される
console.log(postWithAuthor?.author); // { _id: ..., name: '田中太郎', email: '...' }
// フィールドを限定してpopulate
const postLimited = await BlogPost.findById(postId)
.populate('author', 'name email'); // name と email のみ取得
// ネストしたpopulate
const postFull = await BlogPost.findById(postId)
.populate('author')
.populate({
path: 'comments',
populate: { path: 'author', select: 'name' }, // コメントの投稿者も展開
});
5. middleware(フック) — pre/post処理
const orderSchema = new Schema({
items: [{ product: String, quantity: Number, price: Number }],
totalPrice: Number,
status: { type: String, default: 'pending' },
});
// save前に合計金額を自動計算
orderSchema.pre('save', function (next) {
this.totalPrice = this.items.reduce(
(sum, item) => sum + item.quantity * item.price,
0
);
next();
});
// 削除後にログ出力
orderSchema.post('findOneAndDelete', function (doc) {
if (doc) {
console.log(`Order ${doc._id} was deleted`);
}
});
// findクエリに対するミドルウェア
orderSchema.pre('find', function () {
// デフォルトで status が 'cancelled' のものを除外
this.where({ status: { $ne: 'cancelled' } });
});
const Order = model('Order', orderSchema);
類似パッケージとの比較
| 特徴 | Mongoose | MongoDB Node.js Driver | Prisma (MongoDB) | TypeORM (MongoDB) |
|---|---|---|---|---|
| 種別 | ODM | 公式ドライバ | ORM/ODM | ORM |
| スキーマ定義 | ✅ 独自Schema | ❌ なし | ✅ Prisma Schema | ✅ デコレータ |
| TypeScript型推論 | ✅ InferSchemaType | ✅ ネイティブ | ✅ 自動生成 | ⚠️ 部分的 |
| バリデーション | ✅ 組み込み | ❌ なし | ✅ 組み込み | ⚠️ 限定的 |
| ミドルウェア | ✅ pre/post | ❌ なし | ✅ あり | ✅ Subscriber |
| populate(JOIN) | ✅ 強力 | ❌ 手動で$lookup | ✅ リレーション | ⚠️ 限定的 |
| 学習コスト | 中 | 低 | 中 | 高 |
| エコシステム | ◎ 非常に豊富 | ○ 標準 | ○ 成長中 | △ MongoDB対応は限定的 |
| パフォーマンス | ○ | ◎(最速) | ○ | △ |
選定の目安:
- スキーマ管理・バリデーションを重視 → Mongoose
- 最大限のパフォーマンス・低レベル制御 → MongoDB Node.js Driver
- RDB含むマルチDB対応・型安全最優先 → Prisma
注意点・Tips
1. lean() でパフォーマンスを改善する
// ❌ Mongooseドキュメントインスタンスを返す(メソッド付き、重い)
const docs = await UserModel.find({});
// ✅ プレーンなJavaScriptオブジェクトを返す(読み取り専用なら高速)
const docs = await UserModel.find({}).lean();
lean() を使うとMongooseのドキュメントインスタンスではなくプレーンオブジェクトが返るため、メモリ使用量が減り処理が高速になります。ただし、save() や仮想プロパティなどのMongoose機能は使えなくなります。
2. runValidators を忘れない
// ❌ update系メソッドはデフォルトでバリデーションが走らない
await UserModel.findByIdAndUpdate(id, { age: -5 }); // min: 0 のバリデーションが無視される
// ✅ runValidators: true を明示する
await UserModel.findByIdAndUpdate(id, { age: -5 }, { runValidators: true });
3. 接続管理のベストプラクティス
// アプリケーション起動時に1回だけ接続
async function bootstrap() {
try {
await mongoose.connect(process.env.MONGODB_URI!);
console.log('MongoDB connected');
} catch (err) {
console.error('MongoDB connection error:', err);
process.exit(1);
}
// グレースフルシャットダウン
process.on('SIGINT', async () => {
await mongoose.disconnect();
process.exit(0);
});
}
4. TypeScriptでの型定義パターン
import { Schema, model, Model, Hy