MongoDB: Mongoose応用パターン:パフォーマンスと柔軟性
最終更新:2026-08-26
Mongoose応用パターン—populate、discriminator、lean、aggregateなどの高度な機能をマスターする。
1. 学習内容
- Populate結合クエリ($lookupとの比較)
- discriminator
- lean()パフォーマンス最適化
- Model.aggregate()集計パイプライン
- Schemaインデックス宣言
- Mongoose 7.xパフォーマンス最適化のヒント
graph TB
A[Mongoose応用機能] --> B[populate<br/>アプリケーション層JOIN]
A --> C[discriminator<br/>単一コレクション、複数スキーマ]
A --> D[lean<br/>パフォーマンス最適化]
A --> E[aggregate<br/>データベース層集計]
A --> F[Schemaインデックス<br/>宣言的]
B --> B1[複数クエリ<br/>N+1リスク]
C --> C1[roleフィールド<br/>タイプ別分類]
D --> D1[hydrateスキップ<br/>↑5倍パフォーマンス]
E --> E1[$facet/$lookup<br/>1回の操作で多次元返却]
style D fill:#d4edda
2. Populate結合クエリ
populateとは? populateはMongooseの「アプリケーション層JOIN」の実装です。スキーマにObjectId型のrefフィールドがある場合、populateは自動的に追加クエリを実行し、参照されているObjectIdを完全なドキュメントに置き換えます。本質的に2つのクエリを実行:まず主ドキュメントからObjectIdを取得し、次に関連ドキュメントから完全なデータを取得します。
populateの動作原理:
sequenceDiagram
participant App as Node.jsアプリケーション
participant Mongo as MongoDB
App->>Mongo: 1回目のクエリ: Order.find()
Mongo-->>App: [order1, order2, ...]
App->>Mongo: 2回目のクエリ: User.find({_id: {$in: [ObjectId1, ObjectId2, ...]}})
Mongo-->>App: ユーザーを返す
App->>App: マージ: order.userId → userオブジェクト
populate vs $lookupの比較:
| 項目 | populate | $lookup |
|---|---|---|
| 実行層 | アプリケーション層(2+クエリ) | データベース層(1集計) |
| クエリ数 | N+1リスク | 1 |
| 柔軟性 | 中程度(ObjectId参照のみ対応) | 高い(任意条件関連) |
| パフォーマンス | 小規模データセット向き | 大規模データセット推奨 |
| コード簡潔性 | 高い(.populate()1行) |
低い(集計パイプライン構文) |
| 戻り値型 | Mongoose Document | プレーンオブジェクト |
N+1問題: 100件の注文を取得し、各order.userIdにpopulateすると、1(注文クエリ)+ 100(ユーザークエリ)= 101件のクエリが発生します。Mongooseは自動的にこれを$inバッチクエリに最適化(1+1=2クエリ)しますが、ネストしたpopulate操作は追加クエリを生成する可能性があります。
使用シーン: 1対1関係(注文に紐づくユーザー取得など)にはpopulateを使用。複雑な条件のバッチ関連には$lookupを使用。表示専用にはleanとpopulateを組み合わせる。
(1) 基本的なpopulate
// === 基本的なpopulate ===
const user = await User.findById(userId).populate('addresses');
// SELECT u.*, a.* FROM users u LEFT JOIN addresses a ON u._id = a.userId
// === ネストしたpopulate ===
const order = await Order.findById(orderId)
.populate('userId')
.populate({
path: 'items.productId',
select: 'sku title price'
});
// === 条件付きpopulate ===
const orders = await Order.find()
.populate({
path: 'userId',
match: { isActive: true },
select: 'username avatar'
});
| populate vs $lookup | populate | $lookup |
|---|---|---|
| 実行層 | アプリケーション層 | データベース層 |
| クエリ数 | N+1 | 1 |
| 柔軟性 | 中程度 | 高い |
| パフォーマンス | 小規模データセット向き | 大規模データセット推奨 |
3. Discriminator
discriminatorとは? discriminatorはMongooseの「単一コレクション継承」メカニズムです。複数のモデルが同じMongoDBコレクションを共有し、discriminatorキーでドキュメントタイプを区別します。これはオブジェクト指向プログラミングの継承に似ています:基底クラスが共通フィールドを定義し、派生クラスが固有フィールドを拡張し、全インスタンスが同じテーブルに存在します。
Discriminatorの動作原理:
graph TB
subgraph "usersコレクション(単一セット)"
D1["{role: 'Customer', loyaltyPoints: 100, email: 'alice@...'}"]
D2["{role: 'Customer', loyaltyPoints: 50, email: 'bob@...'}"]
D3["{role: 'Admin', permissions: ['manage'], email: 'admin@...'}"]
end
subgraph "Mongooseモデル層"
User[User Model<br/>email + username + passwordHash]
Customer[Customer Model<br/>+ loyaltyPoints + preferredCategories]
Admin[Admin Model<br/>+ permissions + lastLoginAt]
end
User -->|discriminator| Customer
User -->|discriminator| Admin
Customer -->|検索: role='Customer'| D1
Customer -->|検索: role='Customer'| D2
Admin -->|検索: role='Admin'| D3
style User fill:#fff3cd
style Customer fill:#d4edda
style Admin fill:#cce5ff
Discriminator vs 独立コレクション:
| 項目 | Discriminator(単一コレクション) | 独立コレクション |
|---|---|---|
| クエリ方法 | 識別キーで自動フィルタ | コレクションをまたぐクエリ |
| ストレージ効率 | 高い(インデックス共有) | 低い(共通フィールドの重複インデックス) |
| データ整合性 | 自然に整合(同一コレクション内) | 維持が必要(コレクション間更新) |
| インデックスサイズ | 小さい(共通フィールドに1つ) | 大きい(各コレクションに別のインデックス作成) |
| クエリパフォーマンス | 若干遅い(roleでのフィルタ必要) | 速い(コレクションが小さい) |
| スケーラビリティ | 悪い(コレクション拡大) | 良い(独立スケーリング) |
| 使用シーン | フィールド差異が小さい、結合クエリ頻繁 | フィールド差異が大きい、主に独立クエリ |
使用シーン: ユーザーロール(Customer/Admin/Moderatorがemailとpasswordを共有、それぞれ固有フィールドを持つ)。決済方法(CreditCard/PayPal/BankTransferが金額とステータスを共有、それぞれチャネル固有フィールドを持つ)。通知タイプ(Email/SMS/Pushが件名と内容を共有、それぞれチャネル固有設定を持つ)。
// === 基本Userモデル ===
const UserSchema = new mongoose.Schema({
email: String,
username: String,
passwordHash: String
}, { discriminatorKey: 'role' });
const User = mongoose.model('User', UserSchema);
// === Customer Discriminator ===
const Customer = User.discriminator('Customer', new mongoose.Schema({
loyaltyPoints: { type: Number, default: 0 },
preferredCategories: [String]
}));
// === Admin Discriminator ===
const Admin = User.discriminator('Admin', new mongoose.Schema({
permissions: [String],
lastLoginAt: Date
}));
// === 異なるロールを作成 ===
const customer = await Customer.create({
email: 'alice@example.com',
username: 'alice',
passwordHash: '...',
loyaltyPoints: 100,
preferredCategories: ['Electronics']
});
const admin = await Admin.create({
email: 'admin@example.com',
username: 'admin',
passwordHash: '...',
permissions: ['manage_products']
});
// === 検索時、roleで分類 ===
const customers = await Customer.find();
const admins = await Admin.find();
// 全データは同じコレクション(users)にある、discriminatorKeyで分類
使用シーン: 単一コレクションに複数スキーマ(ロールごとに異なるフィールド)。
4. lean()パフォーマンス最適化
lean()とは? lean()はMongooseのパフォーマンス最適化メソッドです。ドキュメントのハイドレート(hydrate)プロセスをスキップし、プレーンなJavaScriptオブジェクトを直接返します。通常のクエリはMongoose Documentを返します(save()やvalidate()などのメソッドを持ち、変更追跡も行います)が、lean()はプレーンオブジェクトを返します(データのみを含み、メソッドは持ちません)。
lean()のパフォーマンス差の原理:
graph LR
subgraph "通常クエリ(leanなし)"
T1[Product.find] --> D1[Mongoose Document<br/>save/validate等を含む<br/>~150ms / 100 docs]
end
subgraph "lean検索"
Q2[MongoDB生BSON] --> H2[JSON.parse直接変換]
H2 --> D2[プレーンオブジェクト<br/>生データ<br/>~30ms / 100 docs]
end
style D1 fill:#f8d7da
style D2 fill:#d4edda
パフォーマンス比較データ(100ドキュメント、Electronicsカテゴリ):
| 操作 | Leanなし | Leanあり | パフォーマンス倍率 |
|---|---|---|---|
| クエリ時間 | ~150 ms | ~30 ms | ↑5倍 |
| メモリ使用量 | ~5MB | ~1MB | ↓5倍 |
| JSON.stringify | ~8 ms | ~2 ms | ↑4倍 |
| save()対応 | ✅ | ❌ | — |
| populate対応 | ✅ | ✅(チェーン呼び出し) | — |
| 変更追跡対応 | ✅ | ❌ | — |
使用ガイドライン: 読み取り専用API(一覧、詳細)にはlean()を使用。save()の呼び出しや変更追跡が必要な場合はlean()を使用しない。populate後に修正が不要な場合はlean()を追加。
// === 通常クエリ:Mongoose Documentを返す ===
const products = await Product.find();
// 各productはMongoose Document(save()等のメソッドを持つ)
// === lean():純粋なJSオブジェクトを返す ===
const products = await Product.find().lean();
// 各productはプレーンオブジェクト、パフォーマンス3-5倍向上
// === 比較テスト ===
console.time('without lean');
const a = await Product.find({ category: 'Electronics' }).limit(100);
console.timeEnd('without lean'); // ~150ms
console.time('with lean');
const b = await Product.find({ category: 'Electronics' }).lean().limit(100);
console.timeEnd('with lean'); // ~30ms
5. Model.aggregate()集計パイプライン
Mongooseの集計パイプライン: Model.aggregate()はMongoDBの集計エンジンを直接呼び出し、データベース層でグループ化、結合、計算を実行します。populateがアプリケーション層で処理するのとは異なり、集計パイプライン内のデータはNode.js側に転送して処理する必要がないため、より良いパフォーマンスを発揮します。
aggregateとpopulateの選び方:
| シーン | 推奨ソリューション | 理由 |
|---|---|---|
| 注文 + ユーザー名を検索 | populate | 単純な関係、コードが簡潔 |
| カテゴリごとの平均価格を計算 | aggregate | グループ化が必要な計算 |
| 結合 + グループ + ソート | aggregate + $lookup | 一度の操作で完了 |
| 多段ネスト結合 | aggregate + 複数$lookup | N+1を回避 |
| 多次元結果を返す | aggregate + $facet | 一度に複数ビューを返す |
集計パイプライン実行プロセス:
graph LR
Input[1,000,000件ドキュメント] -->|"$match"| Filter[フィルタ後: 500,000件]
Filter -->|"$group"| Group[カテゴリでグループ化<br/>5グループ]
Group -->|"$sort"| Sorted[件数でソート]
Sorted -->|"$limit"| Output[トップ5件]
style Input fill:#f8d7da
style Output fill:#d4edda
// === Mongooseで集計を使用 ===
const stats = await Product.aggregate([
{ $match: { isActive: true } },
{ $group: { _id: '$category', count: { $sum: 1 }, avgPrice: { $avg: '$price' } } },
{ $sort: { count: -1 } }
]);
// === aggregate + populate(Mongoose 6+)===
const results = await Order.aggregate([
{ $match: { status: 'paid' } },
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'customer'
}
},
{ $unwind: '$customer' }
]);
6. Schemaインデックス宣言
Mongooseインデックス宣言方法: Mongooseはスキーマ定義内での宣言的インデックス作成をサポートします。フィールドレベルインデックス(index: true)、複合インデックス(Schema.index())、特殊インデックス(テキストインデックス、TTLインデックス、部分インデックス)など。宣言的インデックスの利点は、スキーマと一緒に定義されるため一目で理解しやすいこと。起動時に自動作成されます(autoIndex=true)。
インデックスタイプと使用シーン:
| インデックスタイプ | 宣言方法 | 使用シーン | 特殊パラメータ |
|---|---|---|---|
| 単一フィールドインデックス | { sku: { index: true } } |
等価クエリ、ソート | unique |
| 複合インデックス | Schema.index({a:1, b:-1}) |
複数条件クエリ | ESRルール |
| テキストインデックス | { title: { text: true } } |
全文検索 | weights |
| TTLインデックス | Schema.index({at:1}, {expireAfterSeconds:86400}) |
自動期限切れ | 有効期限 |
| 部分インデックス | partialFilterExpression |
条件付きインデックス | フィルタ条件 |
| 地理インデックス | { loc: { type: '2dsphere' } } |
地理検索 | — |
ESRルール(Equality-Sort-Range): 複合インデックスのフィールド順序は:等価条件 → ソート条件 → 範囲条件に従うべきです。例えば、{ category: 1, price: -1 }はfind({category:'E'}) + sort({price:-1})をサポートしますが、priceのみでのソートはサポートしません。
const ProductSchema = new mongoose.Schema({
sku: { type: String, index: true, unique: true },
title: { type: String, text: true }, // テキストインデックス
price: { type: Number, index: true },
category: { type: String, index: true }
});
// === 複合インデックス ===
ProductSchema.index({ category: 1, price: -1 });
// === 部分インデックス ===
ProductSchema.index(
{ category: 1 },
{ partialFilterExpression: { isActive: true } }
);
// === TTLインデックス ===
ProductSchema.index(
{ createdAt: 1 },
{ expireAfterSeconds: 30 * 24 * 60 * 60 }
);
7. Mongoose 7.xパフォーマンス最適化
Mongooseパフォーマンス最適化の方法論: パフォーマンス最適化は「lean()を追加して終わり」ではなく、接続層 → クエリ層 → アプリケーション層 → デプロイ層にわたる体系的なチューニングです。コア原則は:データ転送量の削減、クエリ数の最小化、シリアライズオーバーヘッドの低減、データベース本来の能力の活用です。
最適化戦略マトリックス:
| 最適化層 | 戦略 | 効果 | 影響度 |
|---|---|---|---|
| 接続層 | maxPoolSizeチューニング | 接続待機時間を削減 | 低い(設定) |
| クエリ層 | 投影select() | データ転送を90%以上削減 | 低い |
| クエリ層 | インデックス + hint() | 全件スキャン回避 | 中程度 |
| クエリ層 | lean() | ハイドレートオーバーヘッド5倍削減 | 低い |
| アプリケーション層 | Promise.all並列化 | 直列待機時間を削減 | 低い |
| アプリケーション層 | bulkWriteバルク操作 | ネットワーク往復10倍以上削減 | 中程度 |
| アプリケーション層 | カーソルストリーミング | メモリオーバーフロー回避 | 中程度 |
| デプロイ層 | autoIndex=false | 起動高速化 | 低い |
| デプロイ層 | 読み書き分離 | プライマリノード負荷軽減 | 高い |
Charlieの最適化実践: TechCorpの商品一覧APIを3秒から50ミリ秒に最適化—① 複合インデックスを追加してCOLLSCANを回避。② select()で5フィールドのみクエリ。③ lean()でhydrateをスキップ。④ Promise.allでfindとcountを並列実行。⑤ limit 100件を設定。
// === 最適化1:autoIndex無効化(本番環境)===
mongoose.connect(uri, { autoIndex: false });
// 起動時に手動でインデックス作成:await Product.syncIndexes();
// === 最適化2:バルク操作 ===
await Product.bulkWrite([
{ updateOne: { filter: { sku: 'A' }, update: { $inc: { stock: -1 } } } },
{ updateOne: { filter: { sku: 'B' }, update: { $inc: { stock: -1 } } } }
]);
// === 最適化3:投影でデータ転送を削減 ===
const products = await Product.find()
.select('sku title price') // 3フィールドのみ検索
.lean();
// === 最適化4:カーソルでストリーム処理 ===
const cursor = Product.find().cursor();
for await (const doc of cursor) {
// 各ドキュメントを処理
}
// === 最適化5:バルクインサート ===
await Product.insertMany(docs, { ordered: false });
8. 総合実践演習
// === 最適化された一覧API ===
app.get('/api/products', async (req, res) => {
const { page = 1, limit = 20, category, search } = req.query;
// 1. クエリを構築
const query = { isActive: true };
if (category) query.category = category;
if (search) query.$text = { $search: search };
// 2. 並列クエリ(find + count)
const [products, total] = await Promise.all([
Product.find(query)
.select('sku title price thumbnail rating') // 投影
.sort({ createdAt: -1 })
.limit(+limit)
.skip((page - 1) * limit)
.lean(), // パフォーマンス最適化
Product.countDocuments(query)
]);
res.json({
success: true,
data: products,
meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) }
});
});
▶ サンプル 1:多段リレーション + Leanパフォーマンス最適化
// === シーン:ShopHub注文詳細API(3層関連)===
const mongoose = require('mongoose');
// スキーマ定義
const AddressSchema = new mongoose.Schema({ city: String, country: String, zipCode: String });
const UserSchema = new mongoose.Schema({
email: String, username: String,
addresses: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Address' }]
});
const ProductSchema = new mongoose.Schema({ sku: String, title: String, price: Number, thumbnail: String });
const OrderSchema = new mongoose.Schema({
orderNumber: String,
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
items: [{ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }, qty: Number, price: Number }],
status: String
}, { timestamps: true });
const Address = mongoose.model('Address', AddressSchema);
const User = mongoose.model('User', UserSchema);
const Product = mongoose.model('Product', ProductSchema);
const Order = mongoose.model('Order', OrderSchema);
// 3層populate + lean
const order = await Order.findById('647f1f77bcf86cd799439001')
.populate({ path: 'userId', select: 'username email',
populate: { path: 'addresses', select: 'city country' }
})
.populate({ path: 'items.productId', select: 'sku title price' })
.lean();
console.log({
orderNumber: order.orderNumber,
customer: order.userId.username,
city: order.userId.addresses[0]?.city,
items: order.items.map(i => `${i.productId.title} x${i.qty}`)
});
// 出力:{ orderNumber: 'ORD-001', customer: 'alice', city: 'San Francisco',
// items: ['Smartphone X x2', 'Laptop Pro x1'] }
出力:
TEXT 📖 参照専用3層の`populate`(Order→User→Address + Order→Product)+ `lean()`パフォーマンス最適化。
▶ サンプル 2:Populate + Discriminator + Leanの総合的実践
// === 1. populate多段リレーション ===
// 注文 + ユーザー + 商品(3層ネスト)
const order = await Order.findById(orderId)
.populate({
path: 'userId',
select: 'username email avatar',
populate: { path: 'addresses', select: 'city country' } // ユーザーの住所
})
.populate({
path: 'items.productId',
select: 'sku title price thumbnail'
})
.lean(); // パフォーマンス最適化
console.log('注文:', {
orderNumber: order.orderNumber,
customer: {
username: order.userId.username,
address: order.userId.addresses[0]?.city
},
items: order.items.map(i => ({
product: i.productId.title,
qty: i.qty,
price: i.price
}))
});
// === 2. discriminator単一コレクション、複数スキーマ ===
// User基底クラス
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
username: String,
passwordHash: String,
createdAt: { type: Date, default: Date.now }
}, { discriminatorKey: 'role' });
const User = mongoose.model('User', UserSchema);
// Customer Discriminator(Userを継承 + 拡張フィールド)
const Customer = User.discriminator('Customer', new mongoose.Schema({
loyaltyPoints: { type: Number, default: 0 },
preferredCategories: [String],
totalSpent: mongoose.Schema.Types.Decimal128
}));
// Admin Discriminator
const Admin = User.discriminator('Admin', new mongoose.Schema({
permissions: [String],
lastLoginAt: Date
}));
// 異なるロールを作成(全てusersコレクションに存在、roleフィールドで区別)
const customer = await Customer.create({
email: 'alice@example.com',
username: 'alice',
passwordHash: '...',
loyaltyPoints: 100,
preferredCategories: ['Electronics']
});
const admin = await Admin.create({
email: 'admin@example.com',
username: 'admin',
passwordHash: '...',
permissions: ['manage_products', 'manage_users']
});
// 検索時、roleで自動フィルタ
const customers = await Customer.find({ loyaltyPoints: { $gt: 50 } });
// 実際のクエリ:{ role: 'Customer', loyaltyPoints: { $gt: 50 } }
// === 3. lean()パフォーマンス最適化比較 ===
console.time('without lean');
const a = await Product.find({ category: 'Electronics' }).limit(100);
console.timeEnd('without lean'); // ~150ms
console.time('with lean');
const b = await Product.find({ category: 'Electronics' }).lean().limit(100);
console.timeEnd('with lean'); // ~30ms(5倍パフォーマンス向上)
// === 4. Model.aggregate()データベース層集計 ===
const stats = await Product.aggregate([
{ $match: { isActive: true } },
{
$facet: {
totalCount: [{ $count: 'count' }],
byCategory: [
{ $group: { _id: '$category', count: { $sum: 1 }, avgPrice: { $avg: '$price' } } },
{ $sort: { count: -1 } }
],
topRated: [
{ $sort: { rating: -1 } },
{ $limit: 5 },
{ $project: { sku: 1, title: 1, rating: 1 } }
]
}
}
]);
// 出力:
// {
// totalCount: [{ count: 1250 }],
// byCategory: [
// { _id: 'Electronics', count: 450, avgPrice: 599 },
// { _id: 'Books', count: 380, avgPrice: 29 },
// ...
// ],
// topRated: [
// { sku: 'PHONE-001', title: 'Smartphone X', rating: 4.9 },
// ...
// ]
// }
出力:
TEXT 📖 参照専用`populate`が多段関連を実現;`discriminator`が単一コレクションで複数ロールをサポート;`lean()`が5倍パフォーマンス向上;`aggregate`がデータベース層で一度の集計で多次元結果を返す。
▶ サンプル 3:populate、lean、aggregateを使ったECダッシュボード(難易度 ⭐⭐)
// シーン:ShopHub管理ダッシュボードで顧客・商品情報付き注文を表示
const mongoose = require('mongoose');
// ref付きスキーマ
const OrderSchema = new mongoose.Schema({
orderNumber: String,
customerId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
items: [{
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
qty: Number,
price: Number
}],
status: { type: String, enum: ['pending', 'paid', 'shipped', 'delivered'] },
total: Number,
createdAt: { type: Date, default: Date.now }
});
const Order = mongoose.model('Order', OrderSchema);
// 方法1:populate(小規模データセット向き)
async function getOrdersWithPopulate() {
return await Order.find({ status: 'paid' })
.populate('customerId', 'username email')
.populate('items.productId', 'sku title')
.sort({ createdAt: -1 })
.limit(20)
.lean(); // leanと組み合わせて読み取り専用
}
// 方法2:aggregate + $lookup(大規模データセット + フィルタ向き)
async function getOrdersWithAggregate() {
return await Order.aggregate([
{ $match: { status: 'paid' } },
{ $sort: { createdAt: -1 } },
{ $limit: 20 },
// 顧客を結合
{
$lookup: {
from: 'users',
localField: 'customerId',
foreignField: '_id',
as: 'customer'
}
},
{ $unwind: '$customer' },
// 商品を結合
{
$lookup: {
from: 'products',
localField: 'items.productId',
foreignField: '_id',
as: 'productDetails'
}
},
// 最終形
{
$project: {
orderNumber: 1,
total: 1,
status: 1,
customer: { username: '$customer.username', email: '$customer.email' },
itemCount: { $size: '$items' }
}
}
]);
}
// 使用例
const orders = await getOrdersWithPopulate();
console.log(`${orders.length}件の注文を顧客情報付きで読み込み`);
出力:
TEXT 📖 参照専用20件の注文を顧客情報付きで読み込み // 各注文にcustomer.username、customer.email、商品情報付きitemsを含む
❓ よくある質問
populateと$lookupはいつ使い分けるべき?populate、大規模データセット(データベース層での効率)には$lookupを使用。lean()の後にsave()を呼び出せる?lean()はMongooseメソッドを持たないプレーンオブジェクトを返します。save()が必要な場合は、再度ドキュメントをクエリするか、ドキュメントのメソッドを使用してください。📖 まとめ
- populate:結合クエリ(N+1リスク)
- discriminator:単一コレクション、複数スキーマ
- lean():パフォーマンス最適化 ↑3–5倍
- Model.aggregate():データベース層集計
- Schemaインデックス宣言
- Mongoose 7.xパフォーマンス最適化のヒント
📝 練習問題
- 基礎問題(⭐):
populate結合クエリを実装(Order + User + Product)。 - 基礎問題(⭐):
lean()で商品一覧APIを最適化し、パフォーマンス差を比較。 - 応用問題(⭐⭐): discriminatorでUser、Customer、Adminの3ロールを実装。
- 応用問題(⭐⭐):
bulkWriteで商品在庫をバルク更新(在庫切れ対応)。 - チャレンジ(⭐⭐⭐): Mongoose応用APIを完全実装(populate + lean + aggregate + discriminator)。