Database Manual / Indexes / Strategies

Unique Indexes and Schema Validation唯一索引和模式验证

To ensure your database adheres to your application design, you can strategically create indexes to combine index properties with schema validation.为了确保数据库符合应用程序设计,您可以策略性地创建索引,将索引属性与模式验证相结合。

About this Task关于此任务

Consider an application that summarizes a user’s finances. The main page of the application displays the user’s ID and the balances on all of their banking accounts synced with the application.考虑一个总结用户财务状况的应用程序。应用程序的主页显示用户的ID以及与应用程序同步的所有银行账户的余额。

The application stores its user information in a collection called users. The users collection contains documents with the following schema:应用程序将其用户信息存储在名为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" }
]
} )

The application requires the following rules:应用程序需要以下规则:

  • A user can register in the application and not sync a bank account.用户可以在应用程序中注册,而不同步银行帐户。
  • A user identifies an account by its bank and number fields.用户通过banknumber字段识别帐户。
  • A user cannot register the same account for two different users.一个用户不能为两个不同的用户注册同一个帐户。
  • A user cannot register the same account multiple times for the same user.用户不能为同一用户多次注册同一帐户。

To design your database so that it confines its documents to the application’s rules, combine a unique index and schema validation on your database using the following procedure.要设计数据库,使其将文档限制在应用程序的规则范围内,请使用以下过程在数据库上组合唯一索引和模式验证。

Steps步骤

1

Create a multi-property index创建多属性索引

To enforce the application’s rules, create an index on the accounts.bank and accounts.number fields with the following characteristics:要执行应用程序的规则,请在accounts.bankaccounts.number字段上创建具有以下特征的索引:

  • To ensure the bank and number fields do not repeat, make the index unique.为确保banknumber字段不重复,请使索引唯一
  • To allow indexing of multiple fields, make the index compound.要允许对多个字段进行索引,请使索引复合
  • To allow indexing of documents inside an array, make the index of the type multikey.要允许对数组中的文档进行索引,请将索引类型设置为多键

You therefore create a compound multikey unique index with the following specification and options:因此,您可以使用以下规范和选项创建复合多键唯一索引:

const specification = { "accounts.bank": 1, "accounts.number": 1 };
const options = { name: "Unique Account", unique: true };

db.users.createIndex(specification, options); // Unique Account唯一帐户
2

Create a 创建partialFilterExpression

The index in its current state indexes all documents. However, this implementation can cause errors when you insert documents missing the accounts.bank or accounts.number fields.当前状态的索引对所有文档进行索引。但是,当您插入缺少accounts.bankaccounts.number字段的文档时,此实现可能会导致错误。

For example, try to insert the following data into the users collection:例如,尝试将以下数据插入到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 }

When you try to insert a document that is missing one or more specified fields into an indexed collection, MongoDB:当您尝试将缺少一个或多个指定字段的文档插入索引集合时,MongoDB:

  • populates the missing fields into the inserted document将缺失的字段填充到插入的文档中
  • sets their values to null将它们的值设置为null
  • adds an entry to the index向索引中添加条目

When you insert user1 without the accounts.bank and accounts.number fields, MongoDB sets them to null and adds a unique index entry. Any later insert that also lacks either field, such as user2, causes a duplicate key error.当你插入没有accounts.bankaccounts.number字段的user1时,MongoDB会将它们设置为null并添加一个唯一的索引条目。任何后续插入也缺少任一字段,如user2,都会导致重复键错误。

To avoid this, use a partial filter expression so the index only includes documents that contain both fields. For more information, see Partial Index with Unique Constraint. Recreate the index using the following options:为了避免这种情况,请使用部分筛选表达式,这样索引就只包括包含这两个字段的文档。有关详细信息,请参阅具有唯一约束的部分索引。使用以下选项重新创建索引:

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唯一帐户V2

Test out your new index definition by inserting two users that do not contain the fields accounts.bank and accounts.number:通过插入两个不包含accounts.bankaccounts.number字段的用户来测试新的索引定义:

db.users.insertOne(user1);
db.users.insertOne(user2);
{ acknowledged: true, insertedId: 1 }
{ acknowledged: true, insertedId: 2 }
3

Test your database implementation测试数据库实现

To ensure that you cannot register the same account for two different users, test the following code:为了确保您不能为两个不同的用户注册同一个帐户,请测试以下代码:

/* 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" }

The second updateOne command correctly returns an error, since you cannot add the same account for two separate users.第二个updateOne命令正确返回错误,因为您不能为两个单独的用户添加相同的帐户。

Test that the database does not allow you to add the same account multiple times for the same user:测试数据库是否允许您为同一用户多次添加同一帐户:

/* 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' }
]

The returned code shows that the database incorrectly adds the same account multiple times to the same user. This error occurs because MongoDB indexes do not duplicate strictly equal entries with the same key values pointing to the same document.返回的代码显示数据库错误地将同一帐户多次添加到同一用户。发生此错误是因为MongoDB索引不会重复具有指向同一文档的相同键值的严格相等条目。

When you insert account1 for the second time on the user, MongoDB does not create an index entry, so there are no duplicate values on it. To effectively implement your application design, your database should return an error if you attempt to add the same account multiple times to the same user.当您在用户上第二次插入account1时,MongoDB不会创建索引条目,因此上面没有重复的值。为了有效地实现应用程序设计,如果您尝试将同一帐户多次添加到同一用户,数据库应该返回错误。

4

Set up schema validation设置架构验证

To make your application reject adding the same account multiple times to the same user, implement Schema Validation. 要使应用程序拒绝将同一帐户多次添加到同一用户,请实现架构验证The following code uses the $expr operator to write an expression to verify if the items inside an array are unique:以下代码使用$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
}
}
};

When { $isArray: "$accounts" } is true, then the accounts array exists in a document, and MongoDB applies the uniqueAccounts validation logic. If the document passes the logic, it is valid.{ $isArray: "$accounts" }true时,accounts数组存在于文档中,MongoDB应用uniqueAccounts验证逻辑。如果文档通过了逻辑,则它是有效的。

The uniqueAccounts expression compares the size of the original accounts array to the size of accountsSet, which is created by the $setIntersection of a mapped version of accounts:uniqueAccounts表达式将原始accounts数组的大小与accountsSet的大小进行比较,accountsSet是由映射版本的accounts$setIntersection创建的:

  • The $map function transforms each entry in the accounts array to include only the accounts.bank and accounts.number fields.$map函数将accounts数组中的每个条目转换为仅包含account.bankaccount.number字段。
  • The $setIntersection function removes duplicates by treating the mapped array as a set.$setIntersection函数通过将映射数组视为一个集合来删除重复项。
  • The $eq function compares the size of the original accounts array and the deduplicated accountsSet.$eq函数比较原始accounts数组和已消除重复的accountsSet的大小。

If both sizes are equal, all entries are unique by accounts.bank and accounts.number, then the validation returns true. If not, duplicates are present, and validation fails with an error.如果两个大小相等,则所有条目在accounts.bankaccounts.number上都是唯一的,则验证返回true。如果没有,则存在重复项,验证失败并出现错误。

You can test out your schema validation to ensure your database does not allow adding the same account multiple times to the same user:您可以测试模式验证,以确保数据库不允许将同一帐户多次添加到同一用户:

/* 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
}
}

The second updateOne() command returns a Document failed validation error, indicating that the database now rejects any attempt to add the same account multiple times to the same user.第二个updateOne()命令返回Document failed validation错误,表示数据库现在拒绝向同一用户多次添加同一帐户的任何尝试。