データベースがアプリケーションの設計に準拠しているようにするには、インデックスを戦略的に作成して、インデックスのプロパティとスキーマ検証を組み合わせます。
このタスクについて
ユーザーの金銭状況を要約するアプリケーションを考えてみましょう。アプリケーションのメイン ページには、アプリケーションと同期されたユーザーのIDとすべての金融アカウントの残高が表示されます。
アプリケーションはユーザー情報を users
というコレクションに保存します。users
コレクションには、次のスキーマを持つドキュメントが含まれています。
db.users.insertOne( { _id: 1, name: { first: "john", last: "smith" }, accounts: [ { balance: 500, bank: "abc", number: "123" }, { balance: 2500, bank: "universal bank", number: "9029481" } ] } )
アプリケーションには、次のルールが必要です。
ユーザーはアプリケーションに登録し、金融アカウントを同期できません。
ユーザーは
bank
フィールドとnumber
フィールドでアカウントを識別します。1 つのユーザーが 2 人の異なるユーザーの同じアカウントを登録することはできません。
ユーザーは、同じユーザーの同じアカウントを複数回登録することはできません。
ドキュメントをアプリケーションのルールに限定するようにデータベースを設計するには、次の手順を使用してデータベースのユニークインデックスとスキーマ検証を組み合わせます。
手順
マルチプロパティインデックスの作成
アプリケーションのルールを適用するには、次の特性を持つ accounts.bank
フィールドと accounts.number
フィールドにインデックスを作成します。
bank
フィールドとnumber
フィールドが繰り返されないようにするには、インデックスを一意のものにします。複数のフィールドのインデックスを作成するには、インデックスを 複合 にします。
配列内のドキュメントのインデックスを作成するには、型のインデックスをマルチキーにします。
したがって、次の仕様とオプションを持つ複合マルチキーユニークインデックスを作成します。
const specification = { "accounts.bank": 1, "accounts.number": 1 }; const options = { name: "Unique Account", unique: true }; db.users.createIndex(specification, options); // Unique Account
を作成します partialFilterExpression
現在の状態のインデックスは、すべてのドキュメントをインデックス化します。ただし、この実装では、accounts.bank
フィールドまたは accounts.number
フィールドが欠落しているドキュメントを挿入するとエラーが発生する可能性があります。
例、次のデータを users
コレクションに挿入してみてください。
const user1 = { _id: 1, name: { first: "john", last: "smith" } }; const user2 = { _id: 2, name: { first: "john", last: "appleseed" } }; const account1 = { balance: 500, bank: "abc", number: "123" }; db.users.insertOne(user1); db.users.insertOne(user2);
{ acknowledged: true, insertedId: 1 } MongoServerError: E11000 duplicate key error collection: test.users index: Unique Account dup key: { accounts.bank: null, accounts.number: null }
指定されたフィールドが 1 つ以上欠落しているドキュメントを インデックス付きコレクションに挿入しようとすると、 MongoDB は次の処理を実行します。
挿入されたドキュメントに欠落しているフィールドを入力します
は、その値を に設定します
null
インデックスにエントリを追加する
accounts.bank
フィールドと accounts.number
フィールドなしで user1
を挿入すると、 MongoDB はそれらを null
に設定し、ユニークインデックスエントリを追加します。user2
など、いずれのフィールドも欠落している後続の挿入では、重複キー エラーが発生します。
これを回避するには、 部分フィルター式を使用して、両方のフィールドを含むドキュメントのみがインデックスに含まれるようにします。詳細については、一意の制約を持つ部分インデックス を参照してください。次のオプションを使用してインデックスを再作成します。
const specification = { "accounts.bank": 1, "accounts.number": 1 }; const optionsV2 = { name: "Unique Account V2", partialFilterExpression: { "accounts.bank": { $exists: true }, "accounts.number": { $exists: true } }, unique: true }; db.users.drop( {} ); // Delete previous documents and indexes definitions db.users.createIndex(specification, optionsV2); // Unique Account V2
accounts.bank
と accounts.number
フィールドを含まない 2 人のユーザーを挿入して、新しいインデックス定義をテストします。
db.users.insertOne(user1); db.users.insertOne(user2);
{ acknowledged: true, insertedId: 1 } { acknowledged: true, insertedId: 2 }
データベースの実装をテストする
2 人の異なるユーザーと同じアカウントを登録できないことを確認するには、次のコードをテストします。
/* Cleaning the collection */ db.users.deleteMany( {} ); // Delete only documents, keep indexes definitions db.users.insertMany( [user1, user2] ); /* Test */ db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } ); db.users.updateOne( { _id: user2._id }, { $push: { accounts: account1 } } );
{ acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 } MongoServerError: E11000 duplicate key error collection: test.users index: Unique Account V2 dup key: { accounts.bank: "abc", accounts.number: "123" }
2 番目の updateOne
コマンドは正しくエラーを返します。これは、2 人の別々のユーザーに同じアカウントを追加することはできないためです。
データベースで同じユーザーに対して同じアカウントを複数回追加することはできないことをテストします。
/* Cleaning the collection */ db.users.deleteMany( {} ); // Delete only documents, keep indexes definitions db.users.insertMany( [user1, user2] ); // Re-insert test documents /* Test */ db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } ); db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } ); db.users.findOne( { _id: user1._id } );
{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } } { acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 } { acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 } _id: 1, name: { first: 'john', last: 'smith' }, accounts: [ { balance: 500, bank: 'abc', number: '123' }, { balance: 500, bank: 'abc', number: '123' } ]
返されたコードは、データベースが同じユーザーに同じアカウントを複数回誤って追加することを示しています。このエラーは、 MongoDBインデックスが、同じドキュメントを指す同じキー値を持つ厳密に等しいエントリを重複させないために発生します。
ユーザーに 2 回目に account1
を挿入すると、 MongoDB はインデックスエントリを作成しないため、重複する値はありません。アプリケーション設計を効果的に実装するには、同じユーザーに同じアカウントを複数回追加しようとすると、データベースはエラーを返す必要があります。
スキーマ検証を設定する
アプリケーションが同じユーザーに同じアカウントを複数回追加することを拒否するようにするには、スキーマバリデーションを実装します。次のコードでは、$expr
演算子を使用して、配列内の項目が一意であるかどうかを確認するための式を記述します。
const accountsSet = { $setIntersection: { $map: { input: "$accounts", in: { bank: "$$this.bank", number: "$$this.number" } } } }; const uniqueAccounts = { $eq: [ { $size: "$accounts" }, { $size: accountsSet } ] }; const accountsValidator = { $expr: { $cond: { if: { $isArray: "$accounts" }, then: uniqueAccounts, else: true } } };
{ $isArray: "$accounts" }
が true
の場合、ドキュメントには accounts
配列が存在し、 MongoDB はuniqueAccounts
検証ロジックを適用します。ドキュメントがロジックを渡す場合は有効です。
式は、元の$setIntersection
uniqueAccounts
配列のサイズとaccounts
accountsSet
のマッピング バージョンの によって作成されたaccounts
のサイズを比較します。
$map
関数は、accounts
配列の各エントリを変換して、accounts.bank
フィールドとaccounts.number
フィールドのみを含めます。$setIntersection
関数は、マップされた配列をセットとして扱い重複を削除します。$eq
関数は、元のaccounts
配列と排除されたaccountsSet
のサイズを比較します。
両方のサイズが等しく、すべてのエントリが accounts.bank
と accounts.number
によって一意である場合、検証は true
を返します。重複しない場合、重複が存在し、検証はエラーで失敗します。
スキーマ検証をテストして、同じユーザーに同じアカウントを複数回追加することはデータベースで許可されていないことを確認できます。
/* Cleaning the collection */ db.users.drop( {} ); // Delete documents and indexes definitions db.runCommand( { collMod: "users", // update collection to use schema validation validator: accountsValidator } ); db.users.insertMany( [user1, user2] ); /* Test */ db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } ); db.users.updateOne( { _id: user1._id }, { $push: { accounts: account1 } } );
MongoServerError: Document failed validation Additional information: { failingDocumentId: 1, details: { operatorName: '$expr', specifiedAs: { '$expr': { '$cond': { if: { '$and': '$accounts' }, then: { '$eq': [ [Object], [Object] ] }, else: true } } }, reason: 'expression did not match', expressionResult: false } }
2 番目の updateOne()
コマンドは Document failed validation
エラーを返し、同じユーザーに同じアカウントを複数回追加しようとした場合、データベースが拒否されたことを示します。