Docs 菜单
Docs 主页
/
数据库手册
/ /

唯一索引和模式验证

为确保数据库符合应用程序设计,您可以策略性地创建索引,将索引属性与模式验证相结合。

考虑一个汇总用户财务状况的应用程序。应用程序的主页显示用户的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" }
]
} )

该应用程序需要以下规则:

  • 用户可以在应用程序中注册但不同步银行帐户。

  • 用户通过 banknumber 字段标识帐户。

  • 一个用户不能为两个不同用户注册同一帐户。

  • 一个用户不能为同一用户多次注册同一帐户。

要设计数据库,使其文档遵守应用程序的规则,请使用以下过程在数据库上结合使用唯一索引和模式验证。

1

要实施应用程序的规则,请在具有以下特征的 accounts.bankaccounts.number 字段上创建索引:

  • 为确保banknumber 字段不重复,请将索引 设置为唯一。

  • 要允许为多个字段索引,请创建索引索引。

  • 要允许对大量内的文档索引,请将索引为 multikey 类型。

因此,您可以使用以下规范和选项创建复合多键唯一索引:

const specification = { "accounts.bank": 1, "accounts.number": 1 };
const options = { name: "Unique Account", unique: true };
db.users.createIndex(specification, options); // Unique Account
2

当前状态下的索引会对所有文档进行索引。但是,当您插入缺少 accounts.bankaccounts.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.bankaccounts.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.bankaccounts.number 的用户来测试新的索引定义:

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

为确保无法为两个不同用户注册同一帐户,请测试以下代码:

/* 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不会创建索引项,因此其上没有重复值。为了有效地实现应用程序设计,如果您尝试将同一帐户多次添加到同一用户,数据库应返回错误。

4

要使应用程序拒绝将同一帐户多次添加到同一用户,实现模式验证。以下代码使用$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.bankaccounts.number 字段。

  • $setIntersection函数通过将映射的大量视为一个设立来删除重复项。

  • $eq函数会比较原始accounts 大量和去重后accountsSet 的大小。

如果两个大小相等,则根据 accounts.bankaccounts.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 错误,表示数据库现在拒绝任何将同一帐户多次添加到同一用户的尝试。

后退

确保查询选择性

获得技能徽章

免费掌握“索引设计基础知识”!

了解详情

在此页面上