Claude CodeでNoSQL/MongoDB設計を実装する実践ガイド
Claude CodeでMongoDBをアクセスパターンから設計し、スキーマ検証・インデックス・集計・テストまで実装する手順を解説。
Claude CodeでMongoDBを設計するときの出発点
NoSQL/MongoDBとは、行と列の固定表ではなく、JSONに近いドキュメント単位でデータを保存するDBです。RDBのように先に正規化した表を並べるのではなく、「アプリがどの画面で、どの単位のデータを、どの順番で読むか」から設計するのが基本になります。
Claude CodeにMongoDB実装を任せるときも、最初に「コレクションを作って」ではなく、アクセスパターン、更新頻度、集計要件、権限境界、将来のデータ量を渡すべきです。薄い記事や一般論では「MongoDBは柔軟」としか言えませんが、現場で失敗するのは柔軟さそのものです。埋め込みすぎるとドキュメントが肥大化し、参照に寄せすぎると毎回の結合が重くなり、インデックスを雑に増やすと書き込みが遅くなります。
この記事では、Claude Codeを「コード生成器」ではなく「設計レビュー担当」として使い、MongoDBのデータモデリング、embeddingとreferenceの判断、インデックス、aggregation pipeline、必要なときだけ使うtransaction、validation schema、seed/test、rollout checklistまでを一つの流れで整理します。実装は公式のMongoDB Node.js Driverを使い、ローカルで動かせるコードにしています。
先にユースケースを3つ以上に分ける
MongoDBのスキーマは、業務の読み方が違えば正解も変わります。Claude Codeには、最初に次のような表を作らせると判断が安定します。
| ユースケース | よく読む単位 | 設計の方向性 | 主なリスク |
|---|---|---|---|
| ECの注文履歴 | ユーザー別の注文一覧、注文詳細、月次売上 | 注文に商品名・価格を埋め込み、商品IDは参照として残す | 商品名変更を過去注文に反映してしまう |
| SaaSの監査ログ | 組織別、ユーザー別、期間別のイベント | 追記中心のドキュメント、複合インデックス、TTLを検討 | 全件スキャン、ログ肥大化 |
| CMSの記事管理 | slugで1件取得、カテゴリ別一覧、公開状態別一覧 | slugとstatusを明示し、一覧用インデックスを作る | 下書きや内部メモの露出 |
| サポートチケット | 顧客別一覧、ステータス別キュー、担当者別対応 | コメントは同時に読む範囲まで埋め込み、巨大化する添付は分離 | コメント配列が伸び続ける |
この時点で、Claude Codeによるデータベース設計やAPI開発ガイドの観点も混ぜます。APIが「注文一覧には商品名だけ必要」なのか、「商品詳細の最新情報も必要」なのかで、埋め込みと参照の判断が変わるからです。RDBやORM前提の設計と比較したい場合は、Prisma ORM活用とSQL最適化も合わせて読むと違いが見えます。
Claude Codeに渡すプロンプト
最初のプロンプトは、コレクション名ではなくアクセスパターンから始めます。
あなたはMongoDBの設計レビュー担当です。
次のEC注文機能について、アクセスパターンから先にデータモデルを設計してください。
要件:
- ユーザーは自分の注文一覧を新しい順に見る
- 注文詳細では購入時点の商品名、価格、カテゴリを表示する
- 商品マスタの価格変更は過去注文に影響させない
- 管理画面ではstatus別、月別、カテゴリ別に売上を集計する
- 注文作成時に在庫減算と決済記録も行うが、transactionは本当に必要な箇所だけに限定する
- ローカルで動くseed/testと、explainで確認するインデックスを出す
出力してほしいもの:
1. embeddingとreferenceの判断表
2. MongoDB Node.js Driverで動く実装
3. validation schema
4. index一覧と理由
5. aggregation pipeline
6. rollout checklist
Claude Codeの回答で確認するポイントは、「よく一緒に読むデータを埋め込む」「独立して頻繁に更新されるデータは参照にする」「集計で必要な属性を注文時点のスナップショットとして残す」の3つです。EC注文なら、items.name、items.price、items.categoryは注文時点の事実なので埋め込みます。一方、商品マスタそのものはproductIdで参照を残します。
ローカルで動く最小セット
まずMongoDBを起動します。Dockerが使える環境なら次のコマンドで十分です。
docker run --name mongo-claude-demo -p 27017:27017 -d mongo:8
Node.js側は公式ドライバを使います。
npm init -y
npm install mongodb
npm install -D tsx typescript
mkdir -p src
次のファイルは、validation schema、index、seed、aggregation、簡単なexplain確認までを一度に実行します。
// src/mongodb-workflow.ts
import { MongoClient, ObjectId } from "mongodb";
type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";
type Product = {
_id?: ObjectId;
name: string;
category: string;
currentPrice: number;
};
type OrderItem = {
productId: ObjectId;
name: string;
category: string;
price: number;
quantity: number;
};
type Order = {
userId: ObjectId;
status: OrderStatus;
items: OrderItem[];
totalAmount: number;
createdAt: Date;
updatedAt: Date;
};
const uri = process.env.MONGODB_URI ?? "mongodb://localhost:27017";
const client = new MongoClient(uri);
async function main() {
await client.connect();
const db = client.db("claude_code_shop");
await db.dropDatabase();
await db.createCollection<Order>("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["userId", "status", "items", "totalAmount", "createdAt", "updatedAt"],
properties: {
userId: { bsonType: "objectId" },
status: { enum: ["pending", "paid", "shipped", "cancelled"] },
totalAmount: { bsonType: ["int", "long", "double", "decimal"], minimum: 0 },
createdAt: { bsonType: "date" },
updatedAt: { bsonType: "date" },
items: {
bsonType: "array",
minItems: 1,
items: {
bsonType: "object",
required: ["productId", "name", "category", "price", "quantity"],
properties: {
productId: { bsonType: "objectId" },
name: { bsonType: "string" },
category: { bsonType: "string" },
price: { bsonType: ["int", "long", "double", "decimal"], minimum: 0 },
quantity: { bsonType: "int", minimum: 1 }
}
}
}
}
}
}
});
const products = db.collection<Product>("products");
const orders = db.collection<Order>("orders");
await Promise.all([
orders.createIndex({ userId: 1, createdAt: -1 }, { name: "orders_by_user_recent" }),
orders.createIndex({ status: 1, createdAt: -1 }, { name: "orders_by_status_recent" }),
orders.createIndex({ "items.category": 1, createdAt: -1 }, { name: "orders_by_category_month" }),
products.createIndex({ category: 1, name: 1 }, { name: "products_by_category_name" })
]);
const productResult = await products.insertMany([
{ name: "Claude Code Workshop", category: "training", currentPrice: 48000 },
{ name: "MongoDB Review Template", category: "template", currentPrice: 9800 },
{ name: "Backend Consultation", category: "consultation", currentPrice: 120000 }
]);
const [workshopId, templateId, consultationId] = Object.values(productResult.insertedIds);
const userId = new ObjectId();
const now = new Date("2026-06-01T09:00:00.000Z");
await orders.insertMany([
{
userId,
status: "paid",
items: [
{ productId: workshopId, name: "Claude Code Workshop", category: "training", price: 48000, quantity: 1 },
{ productId: templateId, name: "MongoDB Review Template", category: "template", price: 9800, quantity: 2 }
],
totalAmount: 67600,
createdAt: now,
updatedAt: now
},
{
userId,
status: "shipped",
items: [
{ productId: consultationId, name: "Backend Consultation", category: "consultation", price: 120000, quantity: 1 }
],
totalAmount: 120000,
createdAt: new Date("2026-05-21T10:00:00.000Z"),
updatedAt: now
}
]);
const recentOrders = await orders
.find({ userId })
.sort({ createdAt: -1 })
.limit(10)
.toArray();
const revenueByCategory = await orders
.aggregate([
{ $match: { status: { $in: ["paid", "shipped"] } } },
{ $unwind: "$items" },
{
$group: {
_id: {
month: { $dateToString: { format: "%Y-%m", date: "$createdAt" } },
category: "$items.category"
},
revenue: { $sum: { $multiply: ["$items.price", "$items.quantity"] } },
quantity: { $sum: "$items.quantity" }
}
},
{ $sort: { "_id.month": 1, revenue: -1 } }
])
.toArray();
const explain = await orders
.find({ userId })
.sort({ createdAt: -1 })
.limit(10)
.explain("executionStats");
const examined = explain.executionStats?.totalDocsExamined ?? 0;
if (recentOrders.length !== 2) throw new Error("seed failed");
if (revenueByCategory.length === 0) throw new Error("aggregation failed");
if (examined > 2) throw new Error(`index check failed: examined ${examined} docs`);
console.log(JSON.stringify({ recentOrders, revenueByCategory, examined }, null, 2));
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await client.close();
});
実行します。
npx tsx src/mongodb-workflow.ts
ここで重要なのは、コードが「動く」だけではなく、設計意図も検証している点です。orders_by_user_recentが効いていれば、ユーザー別注文一覧で余計なドキュメントを大量に読みません。$matchをpipelineの先頭に置くことで、集計対象も先に絞れます。Claude Codeには、この実行結果とexplainを貼り付けて「スキャンが増える条件はないか」「index順序はこのアクセスパターンに合っているか」をレビューさせます。
embeddingとreferenceの判断
MongoDB設計で一番迷うのは、データを埋め込むか参照するかです。判断は好みではなく、読み方と更新頻度で決めます。
| 判断軸 | 埋め込みが向く | 参照が向く |
|---|---|---|
| 読み方 | 常に親と一緒に読む | 単独で読むことが多い |
| 更新頻度 | 親と同じタイミングで変わる、または過去時点を保存したい | 独立して頻繁に変わる |
| サイズ | 配列が予測可能な上限に収まる | 配列が無制限に伸びる |
| 整合性 | 多少の重複を許容できる | 一つの真実を厳密に守りたい |
注文履歴では商品名と価格を埋め込むべきです。2026年6月1日に購入した価格は、翌月の商品マスタ変更で変わってはいけません。一方、商品ページや在庫管理は商品マスタを参照します。サポートチケットなら、最新の数十件のコメントは埋め込み、添付ファイルや全文検索用の本文は別コレクションに逃がす設計が現実的です。
Claude Codeには、「最大配列長」「1ドキュメントの想定サイズ」「過去データの意味」「更新衝突の許容度」を必ず聞かせます。MongoDBにはドキュメントサイズの上限があるため、無制限に増えるコメント、ログ、通知、明細を全部埋め込む設計は危険です。
インデックスはクエリから逆算する
インデックスは、検索条件、ソート、ページングをセットで考えます。今回の注文一覧はfind({ userId }).sort({ createdAt: -1 })なので、{ userId: 1, createdAt: -1 }が自然です。status別の管理画面は{ status: 1, createdAt: -1 }、カテゴリ別の月次集計は{ "items.category": 1, createdAt: -1 }が候補になります。
ただし、インデックスは多ければよいわけではありません。書き込みのたびにインデックスも更新されるため、低頻度の管理画面だけのために巨大な複合インデックスを増やすと、本番の注文作成が遅くなります。Claude Codeには、各インデックスに「対応するアクセスパターン」「使うクエリ」「消してよい条件」を書かせると、後から棚卸しできます。
公式の考え方はMongoDB Data ModelingとMongoDB Indexesを確認してください。特に複合インデックスは、等価条件、範囲条件、ソートの順序が効き方に関わります。
Aggregation Pipelineは早く絞って小さく流す
Aggregation Pipelineは、MongoDB内でデータを段階的に変換・集計する仕組みです。便利ですが、APIリクエストごとに重いpipelineを走らせると、アプリ全体のボトルネックになります。
基本は次の順番です。
$matchで対象期間やstatusを先に絞る- 必要な配列だけ
$unwindする $groupで集計する$sortや$limitは用途に合わせて最後に置く- 頻繁に使う重い集計は、夜間バッチや集計済みコレクションを検討する
今回のコードでは、注文にカテゴリのスナップショットを埋め込んでいるため、売上集計で商品コレクションへの$lookupを避けています。これは「正規化されていないから雑」なのではなく、「注文時点のカテゴリで売上を見る」という要件に合わせた意図的な非正規化です。Aggregationの詳細はMongoDB Aggregation Operationsが公式リファレンスです。
transactionは必要な境界だけに使う
MongoDBにはtransactionがありますが、すべての書き込みをtransactionで包む設計は重くなりがちです。注文作成、在庫減算、決済記録のように「一部だけ成功すると業務的に壊れる」境界では有効です。一方、閲覧数の加算、通知作成、検索インデックス同期のように再試行や後続処理で回復できるものは、必ずしも同じtransactionに入れません。
Atlasやreplica set環境では、次のような形で必要な範囲だけを囲みます。
import { MongoClient, ObjectId } from "mongodb";
export async function markOrderPaid(client: MongoClient, orderId: ObjectId, paymentId: string) {
const session = client.startSession();
try {
await session.withTransaction(async () => {
const db = client.db("claude_code_shop");
const orders = db.collection("orders");
const payments = db.collection("payments");
await orders.updateOne(
{ _id: orderId, status: "pending" },
{ $set: { status: "paid", updatedAt: new Date() } },
{ session }
);
await payments.insertOne(
{ orderId, paymentId, status: "captured", createdAt: new Date() },
{ session }
);
});
} finally {
await session.endSession();
}
}
transactionの制約や使いどころはMongoDB Transactionsを確認してください。ローカルの単体MongoDBコンテナではtransactionが使えない構成もあるため、テスト環境はAtlasかreplica setに合わせます。
よくある失敗例と落とし穴
一つ目は、RDBの正規化をそのまま持ち込むことです。すべてをusers、products、orders、order_itemsに分け、毎回アプリ側で結合すると、MongoDBを使う利点が薄れます。注文詳細で常に必要な商品名や購入時価格は、注文に残した方が自然です。
二つ目は、逆に何でも埋め込むことです。コメント、ログ、通知、閲覧履歴のように増え続ける配列を親ドキュメントに入れ続けると、更新競合やサイズ上限に近づきます。Claude Codeには「この配列は1年後に最大何件か」を必ず見積もらせます。
三つ目は、validation schemaを省略することです。MongoDBは柔軟ですが、公開サービスでstatus: "paied"のようなタイポや、quantity: 0のような不正値を許すと集計が壊れます。アプリ側の型だけでなく、DB側にも最低限の検証を置きます。
四つ目は、indexを作っただけで安心することです。explain("executionStats")を見なければ、本当に使われているか分かりません。Claude CodeにtotalDocsExamined、totalKeysExamined、winningPlanを貼り、想定と違う理由を説明させます。
五つ目は、aggregationを画面表示のたびに重くすることです。月次売上のような数字は、毎回リアルタイムである必要がない場合があります。管理画面ならキャッシュや集計済みコレクションを使う方が安定します。
本番投入前のrollout checklist
- 主要アクセスパターンを5から10個に絞り、各クエリのindexを説明できる
db.createCollectionまたはマイグレーションでvalidation schemaを適用している- seedデータで一覧、詳細、集計、異常系を再現できる
explainで全件スキャンがないことを確認している- transactionを使う処理と使わない処理の境界が文書化されている
- バックアップ、リストア、TTL、監査ログ、個人情報削除の運用が決まっている
- 既存RDBやPrismaとの二重書き込み期間がある場合、切り戻し手順がある
- APIレスポンスに内部フィールドや未公開データが混ざらない
- 負荷が高いaggregationはキャッシュ、バッチ、集計済みコレクションを検討している
MongoDBのNode.js実装はMongoDB Node.js Driverが一次情報です。ドライバのバージョン差、接続プール、エラー処理、型定義は公式を見てからClaude Codeに反映させると、古いAPIを混ぜにくくなります。
ClaudeCodeLabの研修・相談につなげるなら
MongoDBの設計は、サンプルコードよりも「どのデータを一緒に読むか」を言語化する部分が難所です。ClaudeCodeLabでは、Claude Code研修、CLAUDE.md整備、既存APIのデータモデルレビュー、MongoDBやSQLの性能診断までまとめて扱えます。自社の注文、記事、ログ、チケット機能を題材にすると、研修だけでなく実装レビューとしても効果が出ます。
まとめ
Claude CodeでMongoDB開発を進めるなら、最初にアクセスパターンを洗い出し、embeddingとreferenceを判断し、indexとaggregationをコードで検証する流れが重要です。柔軟なDBだからこそ、validation schema、seed/test、explain、rollout checklistを省略しない方が、後からの修正コストを抑えられます。
この記事で紹介した内容を実際に試した結果、最も効果があったのは「注文時点の事実を埋め込み、最新マスタは参照する」と最初に決めたことでした。Claude Codeにexplain結果まで渡してレビューさせると、インデックスの追加よりも、クエリの順番や集計の粒度を直した方がよい場面が見つかりやすく、AdSense向けの記事としても単なる情報まとめではなく実装経験に基づいた内容にできます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。