MongoDB: Mongooseスキーマ設計と埋め込み vs 参照

最終更新:2026-08-26

スキーマ設計はMongoDB開発の基礎であり、パフォーマンスと保守性を決定づける重要な要素です。

1. このレッスンで学ぶこと


100%
graph TB
    A[スキーマ設計] --> B[埋め込み<br/>埋め込み]
    A --> C[参照<br/>参照]

    B --> B1[1対1 関係]
    B --> B2[1対少関係]
    B --> B3[頻繁に一緒に読み取る]

    C --> C1[1対多関係]
    C --> C2[多対多関係]
    C --> C3[頻繁に更新する]

    style B fill:#d4edda
    style C fill:#cce5ff


2. 埋め込みドキュメント vs 参照

概念説明: MongoDBはスキーマレスですが、実務では明確なスキーマ設計が不可欠です。埋め込みドキュメントは親ドキュメント内に子データを格納し、参照は他のコレクションへのポインタ(通常はObjectId)を格納します。

動作原理: 埋め込みドキュメントは単一のドキュメントとして保存され、1回の読み取りですべての関連データを取得できます。参照は2つ以上のコレクションにデータを分散させ、$lookupやMongooseのpopulate()で結合します。

比較分析:

次元 埋め込み 参照
読み取りパフォーマンス ⭐⭐⭐ 高速(1回のクエリ) ⭐⭐ 追加クエリ必要
書き込みパフォーマンス ⭐⭐ 親更新が必要 ⭐⭐⭐ 独立更新可能
データ整合性 ⭐⭐⭐ 原子性保証 ⭐⭐ 分散データ
ストレージ 重複可能性 正規化済み
柔軟性 ⭐⭐ 構造固定 ⭐⭐⭐ 独立クエリ可能

選択基準:

シナリオ 推奨案 理由
1対1関係 埋め込み シンプル、高速
1対少数(<100) 埋め込み 1回読み取りで完結
1対多数(不定) 参照 ドキュメントサイズ制限回避
多対多 参照 双方向の柔軟なクエリ
頻繁に更新 参照 独立更新で競合回避
頻繁に一緒に読み取る 埋め込み パフォーマンス最適
JAVASCRIPT
// === 埋め込みパターン:ユーザー内に住所を埋め込み ===
const userSchema = new mongoose.Schema({
  username: String,
  email: String,
  addresses: [{              // 埋め込みドキュメント配列
    street: String,
    city: String,
    zipCode: String,
    isDefault: Boolean
  }]
});

// === 参照パターン:注文がユーザーを参照 ===
const orderSchema = new mongoose.Schema({
  orderNumber: String,
  userId: {                  // 参照フィールド
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  total: Number,
  status: String
});


3. 関係モデリング

(1) 1対1関係

概念説明: 1つのエンティティが別の1つのエンティティと関連付く関係。例:ユーザーとプロフィール、製品と詳細仕様。

埋め込み案(推奨): 1回の読み取りで全データ取得、パフォーマンス最適。

JAVASCRIPT
// === 1対1 埋め込みパターン ===
const userSchema = new mongoose.Schema({
  username: String,
  email: String,
  profile: {                 // 埋め込みドキュメント
    avatar: String,
    bio: String,
    website: String
  }
});

// 使用例
const user = await User.findById(userId);
console.log(user.profile.bio);  // 追加クエリ不要

(2) 1対多関係

概念説明: 1つの親が複数の子を持つ関係。例:ユーザーと注文、ブログとコメント。

少件数(<100)→埋め込み: 配列で格納、1回読み取りで完結。

多件数(不定)→参照: 子コレクションが親を参照。

JAVASCRIPT
// === 1対少:カテゴリに製品を埋め込み ===
const categorySchema = new mongoose.Schema({
  name: String,
  slug: String,
  products: [{
    sku: String,
    title: String,
    price: Number
  }]
});

// === 1対多:注文がユーザーを参照 ===
const orderSchema = new mongoose.Schema({
  orderNumber: String,
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  items: [orderItemSchema],
  total: Number
});

// populateで結合
const orders = await Order.find()
  .populate('userId', 'username email');

(3) 多対多関係

概念説明: 複数のエンティティが相互に関連付く関係。例:学生とコース、製品とタグ。

参照配列パターン: 双方に参照配列を持つ。

JAVASCRIPT
// === 多対多:製品とタグ ===
const productSchema = new mongoose.Schema({
  sku: String,
  title: String,
  tagIds: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Tag'
  }]
});

const tagSchema = new mongoose.Schema({
  name: String,
  slug: String,
  productIds: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Product'
  }]
});

// 双方向populate
const product = await Product.findById(productId)
  .populate('tagIds');


4. mongoose Schema基本

概念説明: mongoose Schemaはドキュメント構造を定義し、型検証、デフォルト値、インデックス、ミドルウェアなどの機能を提供します。

Schema定義の基本構文:

JAVASCRIPT
const productSchema = new mongoose.Schema({
  // 基本型
  sku: {
    type: String,
    required: [true, 'SKU is required'],
    unique: true,
    trim: true,
    uppercase: true
  },
  title: {
    type: String,
    required: true,
    maxlength: [200, 'Title cannot exceed 200 characters']
  },
  price: {
    type: Number,
    required: true,
    min: [0, 'Price cannot be negative']
  },
  category: {
    type: String,
    enum: ['Electronics', 'Books', 'Clothing', 'Home']
  },
  isActive: {
    type: Boolean,
    default: true
  },
  tags: [String],             // 文字列配列
  metadata: {                 // 埋め込みドキュメント
    views: { type: Number, default: 0 },
    likes: { type: Number, default: 0 }
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// インデックス作成
productSchema.index({ title: 'text', description: 'text' });

// モデル作成
const Product = mongoose.model('Product', productSchema);


5. スキーマ設計のベストプラクティス

▶ サンプル 1:eコマース注文スキーマ(難易度 ⭐)

JAVASCRIPT
// ShopHub Eコマース:注文とアイテムのスキーマ設計
const orderItemSchema = new mongoose.Schema({
  productId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Product',
    required: true
  },
  sku: String,              // 冗長フィールド(高速表示用)
  title: String,            // 注文時点の商品名
  price: Number,            // 注文時点の価格
  qty: { type: Number, min: 1 }
});

const orderSchema = new mongoose.Schema({
  orderNumber: { type: String, unique: true },
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  items: [orderItemSchema],  // 埋め込み配列
  total: { type: Number, min: 0 },
  status: {
    type: String,
    enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'],
    default: 'pending'
  },
  shippingAddress: {         // 埋め込み(注文時に固定)
    street: String,
    city: String,
    zipCode: String,
    country: String
  },
  createdAt: { type: Date, default: Date.now }
});

// 複合インデックス
orderSchema.index({ userId: 1, createdAt: -1 });
orderSchema.index({ status: 1 });

const Order = mongoose.model('Order', orderSchema);

出力:

TEXT 📖 参照専用
注文スキーマは、アイテムを埋め込み(注文時点のデータ固定)、配送先住所を埋め込み(変更不可)、ユーザーを参照(最新情報取得)の設計。

▶ サンプル 2:ブログ記事とコメント(難易度 ⭐⭐)

JAVASCRIPT
// TechBlog:記事とコメントの1対多関係
const commentSchema = new mongoose.Schema({
  author: { type: String, required: true },
  content: { type: String, required: true, maxlength: 1000 },
  createdAt: { type: Date, default: Date.now }
});

const articleSchema = new mongoose.Schema({
  title: { type: String, required: true },
  slug: { type: String, unique: true },
  content: String,
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  comments: [commentSchema],    // 埋め込み:コメント数が有限
  tags: [{ type: String }],      // 多対多:タグは参照でも可
  viewCount: { type: Number, default: 0 },
  isPublished: { type: Boolean, default: false },
  publishedAt: Date,
  createdAt: { type: Date, default: Date.now }
});

// テキストインデックス
articleSchema.index({ title: 'text', content: 'text' });

const Article = mongoose.model('Article', articleSchema);

// コメント追加
const article = await Article.findByIdAndUpdate(
  articleId,
  {
    $push: {
      comments: {
        author: 'Alice',
        content: 'Great article!'
      }
    }
  },
  { new: true }
);

出力:

TEXT 📖 参照専用
記事スキーマは、コメントを埋め込み配列として格納。$pushでコメント追加、1回の読み取りで記事とコメントを同時に取得可能。

▶ サンプル 3:多対多関係のソーシャルアプリ(難易度 ⭐⭐⭐)

JAVASCRIPT
// SocialApp:ユーザーとフォロー関係(多対多)
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  avatar: String,
  bio: { type: String, maxlength: 200 },
  // フォローしているユーザー(参照配列)
  following: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }],
  // フォロワー(参照配列)
  followers: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }],
  createdAt: { type: Date, default: Date.now }
});

// 複合インデックス
userSchema.index({ username: 1 });
userSchema.index({ email: 1 });

// フォロー追加メソッド
userSchema.methods.follow = async function(targetUserId) {
  if (this._id.equals(targetUserId)) {
    throw new Error('自分自身をフォローできません');
  }
  
  const targetUser = await this.constructor.findById(targetUserId);
  if (!targetUser) {
    throw new Error('ユーザーが見つかりません');
  }
  
  // 既にフォロー済みかチェック
  const alreadyFollowing = this.following.some(id => id.equals(targetUserId));
  if (alreadyFollowing) {
    throw new Error('既にフォローしています');
  }
  
  // 双方向に追加
  this.following.push(targetUserId);
  targetUser.followers.push(this._id);
  
  await Promise.all([this.save(), targetUser.save()]);
};

const User = mongoose.model('User', userSchema);

// 使用例:ユーザー情報とフォロワーを取得
const user = await User.findById(userId)
  .populate('following', 'username avatar')
  .populate('followers', 'username avatar');

出力:

TEXT 📖 参照専用
多対多関係では双方に参照配列を持つパターン。follow()メソッドで一貫性を維持し、populate()でフォロワー/フォロー中ユーザー情報を取得。

❓ よくある質問

Q 埋め込みと参照どちらを選ぶべき?
A 「一緒に読み取るなら埋め込み、独立して更新するなら参照」が基本原則。
Q 埋め込みドキュメントの最大サイズは?
A MongoDBのドキュメントサイズ制限は16MB。埋め込み配列が大きくなる場合は参照を検討。
Q 多対多関係で双方向参照は必須?
A いいえ。クエリパターンに応じて片方向だけで十分な場合が多い。
Q 冗長フィールドを保存すべき?
A 高速表示のために、注文時点の商品名・価格などは冗長保存が一般的。

📖 まとめ


📝 練習問題

  1. 基本問題(⭐):ユーザースキーマにプロフィール情報(アバター、自己紹介)を埋め込み定義せよ。
  2. 基本問題(⭐):製品スキーマにカテゴリへの参照フィールドを追加せよ。
  3. 応用問題(⭐⭐):ブログシステムの記事とコメントのスキーマを設計せよ。
  4. 応用問題(⭐⭐):多対多関係(学生とコース)のスキーマを設計せよ。
  5. チャレンジ問題(⭐⭐⭐):eコマースの注文システム(注文、アイテム、配送先、ユーザー)の完全なスキーマを設計せよ。
Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%