为确保数据库符合应用程序设计,您可以策略性地创建索引,将索引属性与模式验证相结合。
关于此任务
考虑一个汇总用户财务状况的应用程序。应用程序的主页显示用户的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
字段标识帐户。一个用户不能为两个不同用户注册同一帐户。
一个用户不能为同一用户多次注册同一帐户。
要设计数据库,使其文档遵守应用程序的规则,请使用以下过程在数据库上结合使用唯一索引和模式验证。
步骤
创建多属性索引
要实施应用程序的规则,请在具有以下特征的 accounts.bank
和 accounts.number
字段上创建索引:
为确保
bank
和number
字段不重复,请将索引 设置为唯一。要允许对大量内的文档索引,请将索引为 multikey 类型。
因此,您可以使用以下规范和选项创建复合多键唯一索引:
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 }
当您尝试将缺少一个或多个指定字段的文档插入索引集合时, 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
的用户来测试新的索引定义:
db.users.insertOne(user1); db.users.insertOne(user2);
{ acknowledged: true, insertedId: 1 } { acknowledged: true, insertedId: 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" }
第二个 updateOne
命令正确返回错误,因为您无法为两个单独的用户添加相同的帐户。
测试数据库是否允许为同一用户多次添加同一帐户:
/* 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索引不会复制具有指向同一文档的相同键值的完全相等的条目。
当您对用户第二次插入 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
验证逻辑。如果文档通过逻辑,则有效。
uniqueAccounts
表达式将原始 大量的大小与accounts
accountsSet
的大小进行比较,后者由$setIntersection
的映射版本的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 } }
第二个 updateOne()
命令返回 Document failed validation
错误,表示数据库现在拒绝任何将同一帐户多次添加到同一用户的尝试。