mongoose の使い方

Mongoose MongoDB ODM

v9.4.14.4M/週MITORM / DB
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

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);

類似パッケージとの比較

特徴MongooseMongoDB Node.js DriverPrisma (MongoDB)TypeORM (MongoDB)
種別ODM公式ドライバORM/ODMORM
スキーマ定義✅ 独自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