MongoDB: スキーマバリデーション:データベース層でのデータ検証
スキーマバリデーションはデータベース層でのデータ検証です—アプリケーション層がなくても無効なデータをフィルタリングできます。
データベースレベルバリデーションの独自の価値: Mongooseバリデータがあるのにスキーマバリデーションが必要な理由は?Mongooseバリデーションはアプリケーション層でのみ有効だからです—1. マルチアプリケーション統合:複数のマイクロサービス(Node.js/Python/Go)が同じMongoDBデータベースに書き込む場合、Node.jsサービスのみMongooseバリデーションを使用し、他は使用しない;2. 直接データベース操作:運用チームがMongoシェルでデータを修復したり、ETLスクリプトがデータベースに直接書き込むとMongooseをバイパス;3. 多層防御:アプリケーション層バリデーションにバグがあっても、データベース層がインターセプト可能。スキーマバリデーションは最後の防御線として機能—通常はトリガーされない(アプリケーション層が既にインターセプト済み)が、例外的な状況でデータ整合性を保護。
スキーマバリデーションの制限と回避策: MongoDBスキーマバリデーションには明確な制限があります—1. クロスフィールドバリデーションをサポートしない(例:「endDate > startDate」)、これはアプリケーション層で処理必要;2. 非同期バリデーションをサポートしない(例:「ユーザー名は一意である必要がある」、データベース検索が必要)、一意インデックスで処理必要;3. 条件付きバリデーションをサポートしない(例:「type='book'の場合、authorは必須」)、アプリケーション層で処理必要;4. $jsonSchemaは全MongoDB演算子をサポートしない(例:$regexは制限される)。したがって、スキーマバリデーションはアプリケーション層バリデーションを完全に置き換えることはできません—正しいアプローチは、アプリケーション層が包括的なバリデーション(ユーザーフレンドリーなエラーメッセージ、クロスフィールドロジック、非同期バリデーション)を処理し、データベース層がフォールバックバリデーション(必須フィールド、データ型、範囲、一意性)を処理することです。
1. 学習内容
- $jsonSchemaバリデータ
- バリデータアクション(error / warn)
- validationLevel(strict / moderate)
- Mongooseスキーマ vs MongoDB $jsonSchemaの比較
- スキーマ進化戦略
graph LR
A[クライアント側ドキュメント挿入] --> B{mongo<br/>スキーマバリデーション}
B -->|validationLevel<br/>strict/moderate| C{バリデーションルール}
C -->|bsonType| D[型チェック]
C -->|required| E[必須チェック]
C -->|pattern| F[正規表現バリデーション]
C -->|enum| G[列挙値チェック]
C -->|minLength| H[長さチェック]
D --> I{通過?}
E --> I
F --> I
G --> I
H --> I
I -->|Yes + action=error| J[✅ 挿入成功]
I -->|No + action=error| K[❌ 拒否 + エラー送出]
I -->|No + action=warn| L[⚠️ 許可 + 警告]
style J fill:#d4edda
style K fill:#f8d7da
2. $jsonSchemaバリデータ
概念説明: $jsonSchemaはMongoDB 3.6+で導入されたJSONスキーマ仕様に基づくドキュメントスキーマバリデーション言語です。データベースレベルでドキュメントが満たすべき構造ルール—フィールド型、必須フィールド、値範囲、正規表現パターンなど—を定義できます。アプリケーション層バリデーションとは異なり、$jsonSchemaはMongoDBエンジンによって強制され、任意のクライアント(Python、Java、Node.js)がデータを書き込む際に従う必要があります。
動作原理: バリデータ付きでコレクションを作成する際、MongoDBは$jsonSchemaルールをコレクションメタデータに保存します。挿入または更新操作ごとに、エンジンは書き込み前に自動的にドキュメントがルールを満たすかバリデートします。バリデーション失敗時、システムはvalidationActionに基づいてエラーをスローして操作を拒否するか、警告をログに記録するかを決定します。
$jsonSchemaコアキーワード:
| キーワード | 機能 | 例 |
|---|---|---|
bsonType |
BSON型指定 | 'string', 'int', 'object', 'array' |
required |
必須フィールドリスト | ['email', 'username'] |
properties |
フィールドレベルルール定義 | { email: { bsonType: 'string' } } |
pattern |
正規表現バリデーション | '^.+@.+$'(メール形式) |
enum |
列挙値 | ['customer', 'admin'] |
minimum / maximum |
値範囲 | minimum: 0, maximum: 150 |
minLength / maxLength |
文字列長 | minLength: 3, maxLength: 30 |
items |
配列要素のルール | { bsonType: 'string' } |
minItems |
配列の最小長 | minItems: 1 |
$jsonSchemaとJSONスキーマの関係: MongoDBの$jsonSchemaはJSONスキーマDraft 4仕様に基づいていますが、いくつかの重要な違いがあります—1. bsonTypeをtypeの代わりに使用(JSONスキーマはint、double、decimal、objectIdなどのBSON型を区別しないため);2. additionalPropertiesはデフォルトでtrue(未定義フィールドを許可、JSONスキーマDraft 4のデフォルトと異なる);3. $ref参照をサポートしない(全ルールをインライン定義必要);4. formatをサポートしない(email、uri、date-timeなど、正規表現を使用必要)。これらの違いを理解することで「JSONスキーマチュートリアルのコードをそのままコピーしてエラーになる」混乱を回避できます。
$jsonSchemaでのネストバリデーション: $jsonSchemaはネストオブジェクトと配列の再帰的バリデーションをサポートします—1. ネストオブジェクトの場合、propertiesとrequiredでサブ構造を定義(例:address: {bsonType: 'object', required: ['city'], properties: {city: {bsonType: 'string'}}});2. 配列の場合、itemsで要素ルールを定義(例:tags: {bsonType: 'array', items: {bsonType: 'string'}}で全配列要素が文字列であることをバリデート);3. ネスト深度に厳密な制限はないが、過度なネストはバリデーションパフォーマンスと可読性に影響—3レベルを超えるネストは別コレクションへの分割を検討。ネストバリデーションはドキュメントモデルの核心的利点—SQLが関連データをバリデートするためにマルチテーブルJOINが必要なのに対し、MongoDBは単一パスでドキュメント全体をバリデート。
使用用途:
- 重要データ構造の保護(ユーザー、注文、支払い記録)
- マルチ言語マイクロサービスの共有データベースでの統一バリデーション
- 脏データの書き込み防止(アプリケーション層バリデーションが漏れた場合のフォールバック)
- 適さないケース: 非同期処理を必要とするバリデーション、複数コレクションにまたがるバリデーション、複雑なビジネスロジックを含むバリデーション
$jsonSchema仕様とJSONスキーマの関係: MongoDBの$jsonSchemaはJSONスキーマDraft 4仕様に基づいていますが、BSON拡張を含みます—typeをbsonTypeに置き換え(MongoDBは型にJSONではなくBSONを使用するため)、objectId、decimal、dateなどのBSON固有型を追加。この関係を理解することは重要:1. bsonType: 'string'はJSONスキーマのtype: 'string'に対応;2. bsonType: 'int'に直接の対応はない(JSONにはnumberのみ);3. required、properties、pattern、enumはJSONスキーマと同一。
バージョン移行戦略: スキーマバリデーションの変更にはバージョニング戦略が必要—1. オプションフィールド追加:低リスク、「moderate」+「warn」で直接デプロイ;2. 必須フィールド追加:中程度リスク、まず「オプション」に設定→データ移行→その後「必須」に設定;3. フィールド型変更:高リスク、デュアル書き込みフィールド→移行→切り替え→古いフィールド削除;4. 値範囲の縮小:中程度リスク、まず「moderate」と「warn」で観察→広範な違反がないことを確認→「error」に切り替え。各変更で古いルールを記録、必要に応じてcollModでロールバック。
// === バリデーション付きコレクション作成 ===
db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: {
bsonType: 'string',
pattern: '^.+@.+$',
maxLength: 100
},
username: {
bsonType: 'string',
minLength: 3,
maxLength: 30
},
age: {
bsonType: 'int',
minimum: 0,
maximum: 150
},
role: {
enum: ['customer', 'admin', 'moderator']
},
isActive: {
bsonType: 'bool'
}
}
}
},
validationLevel: 'strict',
validationAction: 'error'
});
要点分析:
bsonTypeJSONスキーマのtypeとは異なり、MongoDBはBSON型名を使用('number'ではなく'int'など)requiredはトップレベルキーワードで、値はフィールド名の配列、どのプロパティにも属さない- ネストドキュメントは
propertiesで定義、配列要素はitemsで定義
ネストバリデーションの設計パターン: $jsonSchemaのネストバリデーションには3つの設計パターンがあります:1. 完全インラインパターン(addressとitemがorderの$jsonSchema内に直接ネスト、構造は明確だがコードが冗長);2. 変数抽出パターン(addressSchemaとitemSchemaをJavaScript変数として定義し、メインスキーマで参照、コード再利用性は良いがアプリケーション層での管理が必要);3. ハイブリッドモード(コアフィールドはインライン、再利用可能なサブ構造は変数として抽出)。本番環境ではモード3を推奨—住所と注文明細は複数コレクションで再利用される可能性があるため(注文とユーザーの両方に住所がある)、独立した変数として抽出することで冗長定義を削減。
配列バリデーションのエッジケース: $jsonSchema配列バリデーションにはいくつかのエラーを起こしやすいエッジケースがあります—1. minItemsとmaxItemsはドキュメント数ではなく配列長をチェック(空配列[]はminItems: 0を通過するがminItems: 1は失敗);2. itemsは全要素のルールを定義(「最初の3要素が異なる型」というタプルバリデーションはサポートしない、JSONスキーマDraft 4はサポートするがMongoDBはしない);3. uniqueItems: trueは配列要素の一意性をチェックするが、ネストオブジェクトでは期待通りに動作しない可能性(オブジェクトは参照で比較され、深さで比較されない);4. 空配列 vs null配列—空配列[]はbsonType: 'array'バリデーションを通過するが、nullは通過しない(bsonType: ['array', 'null']でnullを許可必要)。
▶ サンプル 1:ネストドキュメント + 配列バリデーション
// ShopHub注文コレクション:ネスト住所 + 注文明細配列のバリデーション
db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['userId', 'items', 'total', 'address'],
properties: {
userId: { bsonType: 'objectId' },
items: {
bsonType: 'array',
minItems: 1,
items: {
bsonType: 'object',
required: ['productId', 'qty', 'price'],
properties: {
productId: { bsonType: 'objectId' },
qty: { bsonType: 'int', minimum: 1 },
price: { bsonType: 'decimal', minimum: 0 }
}
}
},
address: {
bsonType: 'object',
required: ['street', 'city', 'zipCode'],
properties: {
street: { bsonType: 'string', minLength: 1 },
city: { bsonType: 'string' },
zipCode: { bsonType: 'string', pattern: '^[0-9]{5,10}$' }
}
},
total: { bsonType: 'decimal', minimum: 0 }
}
}
},
validationLevel: 'moderate',
validationAction: 'error'
});
// テスト:有効な注文
db.orders.insertOne({
userId: ObjectId(),
items: [{ productId: ObjectId(), qty: Int32(2), price: Decimal128('29.99') }],
address: { street: '123 Main St', city: 'Seattle', zipCode: '98101' },
total: Decimal128('59.98')
});
出力:
TEXT 📖 参照専用{ acknowledged: true, insertedId: ObjectId('...') }
// テスト:空のitems配列
db.orders.insertOne({
userId: ObjectId(),
items: [],
address: { street: '123 Main St', city: 'Seattle', zipCode: '98101' },
total: Decimal128('0')
});
出力:
TEXT 📖 参照専用MongoError: Document failed validation: minItems: 1
3. validationAction
概念説明: validationActionはバリデーションチェック失敗時のMongoDBの動作を制御します—厳密に操作を拒否するか(error)、警告付きで許可するか(warn)。これはデータ整合性とビジネス継続性の間の重要なトレードオフです。
動作原理:
errorモード:バリデーション失敗時にDocumentFailedValidationエラーをスロー、書き込み操作はロールバック、クライアントは例外を受信warnモード:バリデーション失敗しても書き込みは成功、ただしmongodログに警告メッセージを記録、段階的ロールアウトに適用
"warn"モードの運用価値: "warn"モードの核心的価値は「新ルールのゼロリスクデプロイ」です—新バリデーションルールを追加する際、まず"warn"に設定し、1–2週間ログを監視し、既存の書き込み操作がどれだけ拒否されるかを追跡します。違反率が<1%なら、ルールは安全と見なされerrorモードに切り替え可能;違反率が>5%なら、ルール調整または履歴データのクリーンアップが先に必要。この段階的デプロイ戦略は「デプロイ直後に広範なエラーが発生」という本番インシデントを防ぎます。"warn"ログをクエリするには、db.adminCommand({getLog: 'global'})を使用し、キーワードDocumentFailedValidationでフィルタリング。
"warn"ログの監視ソリューション: "warn"モードログはプロアクティブな監視が必要—1. ログフィルタリング:MongoDB "warn"ログは他のログと混在、Filebeat/Fluentdで収集後、キーワード"DocumentFailedValidation"でフィルタリング;2. アラート設定:1時間でN件のバリデーション警告を検出した場合、アラートをトリガー(Prometheus/GrafanaまたはCloudWatchで設定可能);3. 定期レビュー:毎日"warn"ログをレビューし、最も一般的なバリデーション失敗パターンを分析;4. 収束追跡:"warn"モードは一時的であるべき—2週間以内に"error"モードに移行するか、ルールを調整、"warn"モードで長期間運用するとログノイズが蓄積し重要な問題が見逃される可能性。
graph LR
A[新バリデーションルール追加] --> B[action: 'warn']
B --> C[1-2週間ログ監視]
C --> D{違反率}
D -->|< 1%| E[✅ 'error'モードに切り替え]
D -->|> 5%| F[❌ ルール調整/データクリーンアップ]
D -->|1-5%| G[🔍 分析継続]
style E fill:#d4edda
style F fill:#f8d7da
style G fill:#fff3cd
"warn"ログの検索: MongoDB "warn"ログはmongodログファイルに保存され、標準的なログ分析ツールで検索可能:
// === 管理コマンドでログを取得 ===
db.adminCommand({ getLog: 'global' });
// === ログでDocumentFailedValidationを検索 ===
// Filebeat/Fluentdで収集後、Elasticsearchまたはgrepでフィルタリング
// grep "DocumentFailedValidation" /var/log/mongodb/mongod.log
| validationAction | 動作 | 使用用途 |
|---|---|---|
error |
拒否 + 例外送出 | 本番環境、データ整合性優先 |
warn |
許可 + ログ記録 | 段階的ロールアウト、新ルール検証 |
// === strict + error(デフォルト) ===
db.createCollection('users', {
validator: { ... },
validationLevel: 'strict',
validationAction: 'error' // 違反で拒否
});
// === moderate + warn(段階的ロールアウト) ===
db.createCollection('users', {
validator: { ... },
validationLevel: 'moderate',
validationAction: 'warn' // 違反でも許可、ログに記録
});
4. validationLevel
概念説明: validationLevelはバリデーションルールをどの程度厳密に適用するかを制御します—既存ドキュメントに遡及して適用するか、新規挿入・更新のみに適用するか。これはスキーマ進化において重要な設定です。
動作原理:
strictモード:全ての挿入と更新にバリデーションを適用、更新時にバリデーション失敗すると拒否moderateモード:既存ドキュメントがバリデーションを満たす場合のみ更新に適用、既存ドキュメントがバリデーションを満たさない場合は更新時バリデーションをスキップ(新規挿入には常に適用)
"moderate"モードの価値: "moderate"モードの核心的価値は「履歴データをブロックせずに新ルールをデプロイ」です—新バリデーションルールを追加した場合、既存ドキュメントの多くが新しいルールに違反している可能性があります。"strict"モードではこれらのドキュメントは一切更新できなくなります(他のフィールドを変更しても)。"moderate"モードでは、バリデーションを満たさない既存ドキュメントは引き続き更新可能—新ルールから「除外」されます。これにより、データ移行とバリデーションロールアウトを並行して進め、データ移行が完了したら"strict"モードに切り替え可能。
"moderate" + "warn"の段階的ロールアウト戦略: 新スキーマバリデーションをデプロイする場合、"moderate" + "warn"の組み合わせが最も安全なアプローチです—1. ステップ1:"moderate" + "warn"でデプロイ(既存ドキュメントは更新可能、違反はログに記録);2. ステップ2:違反ログを分析し、最も一般的な失敗パターンを特定;3. ステップ3:データ移行スクリプトを作成し、違反ドキュメントをバッチ修正;4. ステップ4:違反率が<1%になったら"error"に切り替え;5. ステップ5:移行完了後、"strict"に切り替え。この5ステッププロセスは「スキーマ変更が本番障害を引き起こす」リスクを排除します。
graph TB
A[新バリデーションルール] --> B{validationLevel}
B -->|strict| C[全更新に適用<br/>既存違反ドキュメントは更新不可]
B -->|moderate| D[既存適合ドキュメントのみ適用<br/>違反ドキュメントは更新可能]
C --> E[データ移行完了後<br/>に使用]
D --> F[データ移行中<br/>に使用]
style C fill:#fff3cd
style D fill:#d4edda
| validationLevel | 適用範囲 | 使用用途 |
|---|---|---|
strict |
全挿入 + 更新 | 本番環境(デフォルト) |
moderate |
適合ドキュメントへの更新 + 新規挿入 | 段階的スキーマ進化 |
// === moderateモード例 ===
db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email'],
properties: { email: { bsonType: 'string' } }
}
},
validationLevel: 'moderate' // 既存ドキュメントは緩い検証
});
// 既存ドキュメントがemailを持たない場合でも更新可能
db.users.updateOne(
{ _id: ObjectId('...') },
{ $set: { name: 'Alice' } } // emailなしでも成功
);
5. Mongooseスキーマ vs MongoDB $jsonSchema
概念概要: MongooseスキーマとMongoDB $jsonSchemaは異なる層でバリデーションを行います—Mongooseはアプリケーション層(Node.jsプロセス内)、$jsonSchemaはデータベース層(MongoDBエンジン内)。両者は競合するものではなく、補完的です。
役割の比較:
| 次元 | Mongooseスキーマ | MongoDB $jsonSchema |
|---|---|---|
| バリデーション場所 | アプリケーション層 | データベース層 |
| エラーメッセージ | カスタム可能(ユーザーフレンドリー) | 固定(技術的) |
| クロスフィールドバリデーション | サポート | 非サポート |
| 非同期バリデーション | サポート | 非サポート |
| 多言語クライアント保護 | 不可 | 可能 |
| 直接データベース操作保護 | 不可 | 可能 |
Mongooseと$jsonSchemaの使い分け: 正しいアプローチは「アプリケーション層が包括的なバリデーションを処理し、データベース層がフォールバックバリデーションを処理」です—1. Mongooseバリデーション:ユーザーフレンドリーなエラーメッセージ(例:「パスワードは8文字以上必要」)、クロスフィールドロジック(例:「終了日は開始日より後である必要がある」)、非同期検証(例:「ユーザー名は既に使用されています」)、複雑なビジネスルール(例:「VIPユーザーは20%割引適用」);2. $jsonSchemaバリデーション:必須フィールドチェック(required: ['email', 'username'])、型チェック(bsonType: 'string')、値範囲制限(minimum: 0, maximum: 150)、一意性制約(unique: trueインデックスと組み合わせ)。この多層防御戦略により、アプリケーション層で漏れた場合でもデータベース層がデータ整合性を保護。
Mongooseスキーマから$jsonSchemaへの変換: Mongooseスキーマから$jsonSchemaへの自動変換ツールはありませんが、手動変換は難しくありません—1. String → bsonType: 'string';2. Number → bsonType: 'int'またはbsonType: 'double';3. Boolean → bsonType: 'bool';4. Date → bsonType: 'date';5. ObjectId → bsonType: 'objectId';6. required: true → required: ['fieldName']に追加;7. min/max → minimum/maximum;8. match → pattern;9. enum → enum。ただし、Mongooseのvalidateカスタムバリデータ、非同期バリデータ、クロスフィールドバリデータは$jsonSchemaで表現できません—これらはアプリケーション層で維持必要。
// === Mongooseスキーマ ===
const userSchema = new mongoose.Schema({
email: { type: String, required: true, match: /^.+@.+$/ },
username: { type: String, required: true, minlength: 3, maxlength: 30 },
age: { type: Number, min: 0, max: 150 },
role: { type: String, enum: ['customer', 'admin'] }
});
// === 対応する$jsonSchema ===
{
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'],
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3, maxLength: 30 },
age: { bsonType: 'int', minimum: 0, maximum: 150 },
role: { enum: ['customer', 'admin'] }
}
}
}
}
6. スキーマ進化戦略
概念説明: スキーマ進化とは、バリデーションルールが時間とともに変化する必要があることを意味します—新フィールドの追加、型の変更、制約の強化など。MongoDBはスキーマレスですが、バリデーションルールは「スキーマ」を定義するため、進化戦略が必要です。
進化パターン:
| 変更タイプ | リスク | 推奨アプローチ |
|---|---|---|
| オプションフィールド追加 | 低 | 直接追加(moderate + warn推奨) |
| 必須フィールド追加 | 中 | デフォルト値で移行→必須に設定 |
| 型変更 | 高 | デュアル書き込み→移行→切り替え |
| 値範囲の縮小 | 中 | moderate + warnで観察→errorに切り替え |
スキーマ進化の5ステッププロセス: 安全なスキーマ進化のための5ステップ—1. ステップ1:moderate + warnで新バリデーションをデプロイ(既存ドキュメントへの影響を最小化);2. ステップ2:バリデーション警告ログを監視し、違反パターンを特定(db.adminCommand({getLog: 'global'})でログ取得);3. ステップ3:データ移行スクリプトを実行し、違反ドキュメントをバッチ修正(db.collection.updateMany()でバッチ更新);4. ステップ4:違反率が<1%になったらaction: 'error'に切り替え(collModコマンドを使用);5. ステップ5:全ドキュメントが適合したらvalidationLevel: 'strict'に切り替え。このプロセスは「デプロイ → 即座失敗」を「デプロイ → 観察 → 移行 → 厳格適用」に変えます。
graph LR
A[新バリデーション要件] --> B[moderate + warn]
B --> C[ログ監視<br/>違反パターン特定]
C --> D[データ移行]
D --> E[違反率 < 1% ?]
E -->|いいえ| D
E -->|はい| F[action: 'error']
F --> G[移行完了 ?]
G -->|いいえ| H[移行継続]
H --> G
G -->|はい| I[validationLevel: 'strict']
style B fill:#fff3cd
style F fill:#cce5ff
style I fill:#d4edda
新必須フィールドの追加: 必須フィールドの追加は最も一般的なスキーマ進化シナリオです—1. ステップ1:moderate + warnでコレクションを作成、新フィールドはrequiredに追加しない;2. ステップ2:アプリケーションコードを更新し、新フィールドを常に書き込むようにする;3. ステップ3:データ移行スクリプトで既存ドキュメントに新フィールドを追加(db.users.updateMany({newField: {$exists: false}}, {$set: {newField: defaultValue}}));4. ステップ4:全ドキュメントが新フィールドを持ったらrequiredに追加;5. ステップ5:validationLevel: 'strict'に切り替え。このアプローチは「新フィールドを必須にした瞬間、既存ドキュメントが全て更新不可になる」問題を回避。
型の変更: フィールド型の変更は最もリスクの高いスキーマ進化です—1. ステップ1:新フィールド(fieldV2)を追加し、新フォーマットで書き込み(fieldV1とfieldV2を同時に維持—デュアル書き込み);2. ステップ2:データ移行スクリプトでfieldV1をfieldV2に変換(db.collection.updateMany({fieldV2: {$exists: false}}, [{$set: {fieldV2: {$toDecimal: '$fieldV1'}}}])););3. ステップ3:アプリケーションコードをfieldV2に切り替え;4. ステップ4:fieldV1への参照を全て削除したら、fieldV1を削除。このデュアル書き込みアプローチは「型変更の瞬間、半分のドキュメントが無効になる」問題を回避。
// === ステップ1:moderate + warnでデプロイ ===
db.runCommand({
collMod: 'users',
validator: { /* 新バリデーションルール */ },
validationLevel: 'moderate',
validationAction: 'warn'
});
// === ステップ2:データ移行 ===
db.users.updateMany(
{ newField: { $exists: false } },
{ $set: { newField: 'default_value' } }
);
// === ステップ3:errorに切り替え ===
db.runCommand({
collMod: 'users',
validationAction: 'error'
});
// === ステップ4:strictに切り替え ===
db.runCommand({
collMod: 'users',
validationLevel: 'strict'
});
7. 総合実践トレーニング
概念概要: この総合演習では、ECシステムの注文コレクションを例に、完全なスキーマバリデーション設計を実践します。ネストドキュメント、配列、バリデーションレベル、アクションの全要素を組み合わせて、本番グレードのバリデーション戦略を構築します。
ShopHub注文バリデーション設計:
// 完全な注文コレクションバリデーション
db.createCollection('orders', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['orderNumber', 'userId', 'items', 'status', 'total'],
properties: {
orderNumber: {
bsonType: 'string',
pattern: '^ORD-[0-9]{6}-[0-9]{3}$',
description: '注文番号形式:ORD-YYYYMM-NNN'
},
userId: {
bsonType: 'objectId',
description: '有効なユーザーIDである必要がある'
},
items: {
bsonType: 'array',
minItems: 1,
items: {
bsonType: 'object',
required: ['productId', 'qty', 'price'],
properties: {
productId: { bsonType: 'objectId' },
qty: { bsonType: 'int', minimum: 1 },
price: { bsonType: 'decimal', minimum: 0 }
}
}
},
status: {
enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'],
description: '有効な注文ステータス'
},
total: {
bsonType: 'decimal',
minimum: 0,
description: '合計金額は0以上である必要がある'
},
shippingAddress: {
bsonType: 'object',
required: ['street', 'city', 'zipCode', 'country'],
properties: {
street: { bsonType: 'string', minLength: 1 },
city: { bsonType: 'string', minLength: 1 },
zipCode: { bsonType: 'string', pattern: '^[0-9]{5,10}$' },
country: { bsonType: 'string', minLength: 2 }
}
}
}
}
},
validationLevel: 'strict',
validationAction: 'error'
});
▶ サンプル 2:スキーマバリデーションの実践
// シナリオ:ShopHub注文システムでデータ整合性を確保
// 有効な注文を挿入
db.orders.insertOne({
orderNumber: 'ORD-202607-001',
userId: ObjectId(),
items: [
{ productId: ObjectId(), qty: Int32(2), price: Decimal128('29.99') }
],
status: 'paid',
total: Decimal128('59.98'),
shippingAddress: {
street: '123 Main St',
city: 'Seattle',
zipCode: '98101',
country: 'US'
}
});
出力:
TEXT 📖 参照専用{ acknowledged: true, insertedId: ObjectId('...') }
// 無効な注文を挿入(itemsが空)
db.orders.insertOne({
orderNumber: 'ORD-202607-002',
userId: ObjectId(),
items: [], // 空配列はminItems: 1に違反
status: 'paid',
total: Decimal128('0')
});
出力:
TEXT 📖 参照専用MongoError: Document failed validation
// 無効なステータスで挿入
db.orders.insertOne({
orderNumber: 'ORD-202607-003',
userId: ObjectId(),
items: [{ productId: ObjectId(), qty: Int32(1), price: Decimal128('10') }],
status: 'invalid_status', // enumにない
total: Decimal128('10')
});
出力:
TEXT 📖 参照専用MongoError: Document failed validation
▶ サンプル 3:スキーマ進化の段階的ロールアウト(難易度 ⭐⭐⭐)
// TechCorp:ユーザーコレクションに新必須フィールドを追加する戦略
// === ステップ1:新フィールドをオプションとして追加(moderate + warn)===
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username'], // phoneNumberはまだ必須にしない
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3 },
phoneNumber: { // 新フィールド(オプション)
bsonType: 'string',
pattern: '^\\+?[1-9]\\d{1,14}$',
description: '国際形式の電話番号(E.164)'
},
phoneVerified: { bsonType: 'bool' }
}
}
},
validationLevel: 'moderate', // 既存ドキュメントは更新可能
validationAction: 'warn' // 違反は警告のみ
});
// === ステップ2:ログ監視(1週間)===
// MongoDBログでDocumentFailedValidation警告を監視
// grep "DocumentFailedValidation" /var/log/mongodb/mongod.log | grep phoneNumber
// === ステップ3:データ移行スクリプト ===
db.users.updateMany(
{ phoneNumber: { $exists: false } },
{ $set: { phoneNumber: null, phoneVerified: false } }
);
// === ステップ4:phoneNumberを必須に変更(errorモード)===
db.runCommand({
collMod: 'users',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'username', 'phoneNumber'], // 必須に追加
properties: {
email: { bsonType: 'string', pattern: '^.+@.+$' },
username: { bsonType: 'string', minLength: 3 },
phoneNumber: { bsonType: 'string' },
phoneVerified: { bsonType: 'bool' }
}
}
},
validationLevel: 'moderate',
validationAction: 'error' // エラーに切り替え
});
// === ステップ5:全データ移行完了後、strictモードに ===
db.runCommand({
collMod: 'users',
validationLevel: 'strict' // 全更新に適用
});
// === 移行進捗確認 ===
db.users.aggregate([
{
$group: {
_id: null,
total: { $sum: 1 },
withPhone: { $sum: { $cond: [{ $ifNull: ['$phoneNumber', false] }, 1, 0] } },
verified: { $sum: { $cond: ['$phoneVerified', 1, 0] } }
}
}
]);
出力:
TEXT 📖 参照専用5ステップで安全にスキーマ進化:moderate + warnで観察→データ移行→errorに切り替え→strict適用。新必須フィールド追加による本番障害を防止。
❓ よくある質問
db.runCommand({collMod: 'collection', validator: {}})で空バリデータに設定。collModコマンドを使用、ただしvalidationLevel: 'moderate'を推奨(既存ドキュメントをブロックしないため)。errInfoフィールドにバリデーション失敗の詳細が含まれる(MongoDB 5.0+)。📖 まとめ
- $jsonSchema:JSONスキーマベースのドキュメントバリデーション
- validationAction:error(拒否)またはwarn(許可 + ログ)
- validationLevel:strict(全更新)またはmoderate(適合ドキュメントのみ)
- Mongoose + $jsonSchema:多層防御戦略
- スキーマ進化:moderate + warnで段階的ロールアウト
知識ネットワーク: スキーマバリデーションはMongoDBデータ整合性の最後の防御線です—Mongooseバリデーションと組み合わせて多層防御を実現。moderate + warnで新ルールを安全にデプロイし、データ移行後にstrict + errorに切り替え。
📝 練習問題
- 基本問題(⭐):
usersコレクションにバリデーションを作成(email必須、年齢0-150)。 - 基本問題(⭐):
moderate+warnモードの違いを説明。 - 応用問題(⭐⭐):ネストドキュメントと配列を含むバリデーションを設計。
- 応用問題(⭐⭐):新必須フィールドを追加するスキーマ進化戦略を設計。
- チャレンジ問題(⭐⭐⭐):完全なEC注文システムのバリデーションスキームを設計、Mongooseスキーマとの対応関係を文書化。