处理这个问题的方法并不简单,因为将“upserts”与将项目添加到“数组”中很容易导致不希望的结果。它还取决于您是否希望逻辑设置其他字段,例如指示数组中有多少联系人的“计数器”,您只想在分别添加或删除项目时递增/递减。
然而,在最简单的情况下,如果“联系人”仅包含一个奇异值,例如链接到另一个集合的 ObjectId,那么只要不涉及“计数器”,$addToSet 修饰符就可以正常工作:
Client.findOneAndUpdate(
{ "clientName": clientName },
{ "$addToSet": { "contacts": contact } },
{ "upsert": true, "new": true },
function(err,client) {
// handle here
}
);
这一切都很好,因为您只是在测试文档是否与“clientName”匹配,如果不更新它。无论是否匹配,$addToSet 运算符都会处理唯一的“奇异”值,即任何真正唯一的“对象”。
困难出现在你有类似的地方:
{ "firstName": "John", "lastName": "Smith", "age": 37 }
已经在联系人数组中,然后你想做这样的事情:
{ "firstName": "John", "lastName": "Smith", "age": 38 }
您的实际意图是这是“相同的”约翰·史密斯,只是“年龄”没有不同。理想情况下,您只想“更新”该数组条目,而不是创建新数组或新文档。
在您希望更新文档返回的.findOneAndUpdate() 中使用此功能可能很困难。因此,如果您真的不希望修改后的文档作为响应,那么 MongoDB 的 Bulk Operations API 和核心驱动程序在这里是最有帮助的。
考虑语句:
var bulk = Client.collection.initializeOrderedBulkOP();
// First try the upsert and set the array
bulk.find({ "clientName": clientName }).upsert().updateOne({
"$setOnInsert": {
// other valid client info in here
"contacts": [contact]
}
});
// Try to set the array where it exists
bulk.find({
"clientName": clientName,
"contacts": {
"$elemMatch": {
"firstName": contact.firstName,
"lastName": contact.lastName
}
}
}).updateOne({
"$set": { "contacts.$": contact }
});
// Try to "push" the array where it does not exist
bulk.find({
"clientName": clientName,
"contacts": {
"$not": { "$elemMatch": {
"firstName": contact.firstName,
"lastName": contact.lastName
}}
}
}).updateOne({
"$push": { "contacts": contact }
});
bulk.execute(function(err,response) {
// handle in here
});
这很好,因为这里的批量操作意味着这里的所有语句都一次发送到服务器并且只有一个响应。还要注意这里的逻辑意味着这里最多只有两个操作会实际修改任何东西。
在第一种情况下,$setOnInsert 修饰符确保当文档只是匹配时没有任何更改。由于此处唯一的修改是在该块内,这只会影响发生“更新插入”的文档。
还请注意,在接下来的两个语句中,您不要再次尝试“插入”。这认为第一个语句可能在它必须成功的地方成功,否则无关紧要。
没有“upsert”的另一个原因是因为测试数组中元素是否存在所需的条件在不满足时会导致新文档的“upsert”。这是不希望的,因此没有“upsert”。
他们实际上所做的就是分别检查数组元素是否存在,要么更新现有元素,要么创建一个新元素。因此,总的来说,所有操作都意味着在发生 upsert 的情况下您要么修改“一次”,要么最多修改“两次”。可能的“两次”产生的开销很小,没有真正的问题。
同样在第三条语句中,$not 运算符反转$elemMatch 的逻辑以确定不存在具有查询条件的数组元素。
用.findOneAndUpdate() 翻译这个问题就有点多了。现在重要的不仅是“成功”,它还决定了最终内容的返回方式。
因此,最好的办法是按“系列”运行事件,然后对结果进行一点魔术,以返回最终的“更新”表单。
我们将在此处使用async.waterfall 和lodash 库的帮助:
var _ = require('lodash'); // letting you know where _ is coming from
async.waterfall(
[
function(callback) {
Client.findOneAndUpdate(
{ "clientName": clientName },
{
"$setOnInsert": {
// other valid client info in here
"contacts": [contact]
}
},
{ "upsert": true, "new": true },
callback
);
},
function(client,callback) {
Client.findOneAndUpdate(
{
"clientName": clientName,
"contacts": {
"$elemMatch": {
"firstName": contact.firstName,
"lastName": contact.lastName
}
}
},
{ "$set": { "contacts.$": contact } },
{ "new": true },
function(err,newClient) {
client = client || {};
newClient = newClient || {};
client = _.merge(client,newClient);
callback(err,client);
}
);
},
function(client,callback) {
Client.findOneAndUpdate(
{
"clientName": clientName,
"contacts": {
"$not": { "$elemMatch": {
"firstName": contact.firstName,
"lastName": contact.lastName
}}
}
},
{ "$push": { "contacts": contact } },
{ "new": true },
function(err,newClient) {
newClient = newClient || {};
client = _.merge(client,newClient);
callback(err,client);
}
);
}
],
function(err,client) {
if (err) throw err;
console.log(client);
}
);
这遵循与以前相同的逻辑,即只有两个或其中一个语句实际上会做任何事情,返回的“新”文档可能是null。这里的“瀑布”将每个阶段的结果传递到下一个阶段,包括任何错误都会立即分支到的结尾。
在这种情况下,null 将被替换为一个空对象{},_.merge() 方法将在以后的每个阶段将这两个对象合并为一个。这将为您提供最终结果,即修改后的对象,无论前面的哪些操作实际上做了什么。
当然,$pull 需要进行不同的操作,而且您的问题本身也将输入数据作为对象形式。但实际上这些本身就是答案。
这至少应该让您开始了解如何处理您的更新模式。