MongoDB: Mongoose応用パターン:パフォーマンスと柔軟性

最終更新:2026-08-26

Mongoose応用パターン—populatediscriminatorleanaggregateなどの高度な機能をマスターする。

1. 学習内容


100%
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の動作原理:

100%
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.userIdpopulateすると、1(注文クエリ)+ 100(ユーザークエリ)= 101件のクエリが発生します。Mongooseは自動的にこれを$inバッチクエリに最適化(1+1=2クエリ)しますが、ネストしたpopulate操作は追加クエリを生成する可能性があります。

使用シーン: 1対1関係(注文に紐づくユーザー取得など)にはpopulateを使用。複雑な条件のバッチ関連には$lookupを使用。表示専用にはleanpopulateを組み合わせる。

(1) 基本的なpopulate

JAVASCRIPT
// === 基本的な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の動作原理:

100%
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が件名と内容を共有、それぞれチャネル固有設定を持つ)。

JAVASCRIPT
// === 基本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()のパフォーマンス差の原理:

100%
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()を追加。

JAVASCRIPT
// === 通常クエリ: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側に転送して処理する必要がないため、より良いパフォーマンスを発揮します。

aggregatepopulateの選び方:

シーン 推奨ソリューション 理由
注文 + ユーザー名を検索 populate 単純な関係、コードが簡潔
カテゴリごとの平均価格を計算 aggregate グループ化が必要な計算
結合 + グループ + ソート aggregate + $lookup 一度の操作で完了
多段ネスト結合 aggregate + 複数$lookup N+1を回避
多次元結果を返す aggregate + $facet 一度に複数ビューを返す

集計パイプライン実行プロセス:

100%
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
JAVASCRIPT
// === 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のみでのソートはサポートしません。

JAVASCRIPT
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.allfindcountを並列実行。⑤ limit 100件を設定。

JAVASCRIPT
// === 最適化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. 総合実践演習

JAVASCRIPT
// === 最適化された一覧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パフォーマンス最適化

JAVASCRIPT
// === シーン: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の総合的実践

JAVASCRIPT
// === 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ダッシュボード(難易度 ⭐⭐)

JAVASCRIPT
// シーン: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を含む

❓ よくある質問

Q populate$lookupはいつ使い分けるべき?
A 小規模データセット(アプリケーション層での柔軟性)にはpopulate、大規模データセット(データベース層での効率)には$lookupを使用。
Q discriminatorとコレクションの違いは?
A discriminatorは同じコレクションを共有("role"フィールドで区別)、独立コレクションは物理的に分離。
Q lean()の後にsave()を呼び出せる?
A いいえ。lean()はMongooseメソッドを持たないプレーンオブジェクトを返します。save()が必要な場合は、再度ドキュメントをクエリするか、ドキュメントのメソッドを使用してください。

📖 まとめ


📝 練習問題

  1. 基礎問題(⭐): populate結合クエリを実装(Order + User + Product)。
  2. 基礎問題(⭐): lean()で商品一覧APIを最適化し、パフォーマンス差を比較。
  3. 応用問題(⭐⭐): discriminatorでUser、Customer、Adminの3ロールを実装。
  4. 応用問題(⭐⭐): bulkWriteで商品在庫をバルク更新(在庫切れ対応)。
  5. チャレンジ(⭐⭐⭐): Mongoose応用APIを完全実装(populate + lean + aggregate + discriminator)。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%