我有一个嵌入文档的数组 'pets': [{'fido': ['abc']}
。当我将宠物添加到数组时,如何检查该宠物是否已经存在?例如,如果我再次添加 fido ......我如何检查是否只有 fido
存在而不添加它?我希望我可以使用 $addToSet
但我只想检查集合的一部分(宠物名称)。
User.prototype.updatePetArray = function(userId, petName) {
userId = { _id: ObjectId(userId) };
return this.collection.findOneAndUpdate(userId,
{ $addToSet: { pets: { [petName]: [] } } },
{ returnOriginal: false,
maxTimeMS: QUERY_TIME });
两次添加
fido
的结果:{u'lastErrorObject': {u'updatedExisting': True, u'n': 1}, u'ok': 1, u'value': {u'username': u'bob123', u'_id': u'56d5fc8381c9c28b3056f794', u'location': u'AT', u'pets': [{u'fido': []}]}}
{u'lastErrorObject': {u'updatedExisting': True, u'n': 1}, u'ok': 1, u'value': {u'username': u'bob123', u'_id': u'56d5fc8381c9c28b3056f794', u'location': u'AT', u'pets': [{u'fido': [u'abc']}, {u'fido': []}]}}
最佳答案
如果 "pets"
数组的每个成员(即 petName 作为键)中总是会有“可变”内容,那么 $addToSet
不适合您。至少不是在您希望应用它的数组级别。
相反,您基本上需要对包含在数组中的文档的“键”进行 $exists
测试,然后使用 positional $addToSet
运算符将 $
到匹配键的“包含”数组, 或 ,其中“键”不匹配然后 $push
直接到“pets”数组,新的内部 content
直接作为唯一的数组成员。
因此,如果您可以忍受不返回修改后的文档,那么“批量”操作适合您。在带有 bulkWrite()
的现代驱动程序中:
User.prototype.updatePetArray = function(userId, petName, content) {
var filter1 = { "_id": ObjectId(userId) },
filter2 = { "_id": ObjectId(userId) },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
// Return the promise that yields the BulkWriteResult of both calls
return this.collection.bulkWrite([
{ "updateOne": {
"filter": filter1,
"update": update1
}},
{ "updateOne": {
"filter": filter2,
"update": update2
}}
]);
};
如果您必须返回修改后的文档,那么您将需要解析每个调用并返回实际匹配的调用:
User.prototype.updatePetArray = function(userId, petName, content) {
var filter1 = { "_id": ObjectId(userId) },
filter2 = { "_id": ObjectId(userId) },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
// Return the promise that returns the result that matched and modified
return new Promise(function(resolve,reject) {
var operations = [
this.collection.findOneAndUpdate(filter1,update1,{ "returnOriginal": false}),
this.collection.findOneAndUpdate(filter2,update2,{ "returnOriginal": false})
];
// Promise.all runs both, and discard the null document
Promise.all(operations).then(function(result) {
resolve(result.filter(function(el) { return el.value != null } )[0].value);
},reject);
});
};
在任何一种情况下,这都需要“两次”更新尝试,其中只有“一个”实际上会成功并修改文档,因为只有一个
$exists
测试将是真的。因此,作为第一种情况的示例,“查询”和“更新”在插值后解析为:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets.fido": { "$exists": true }
},
{ "$addToSet": { "pets.$.fido": "ccc" } }
第二次更新为:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets.fido": { "$exists": false }
},
{ "$push": { "pets": { "fido": ["ccc"] } } }
给定变量:
userId = "56d7b759e955e2812c6c8c1b",
petName = "fido",
content = "ccc";
我个人不会像这样命名键,而是将结构更改为:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets": [{ "name": "fido", "data": ["abc"] }]
}
这使得更新语句更容易,并且不需要将变量插入到键名中。例如:
{
"_id": ObjectId(userId),
"pets.name": petName
},
{ "$addToSet": { "pets.$.data": content } }
和:
{
"_id": ObjectId(userId),
"pets.name": { "$ne": petName }
},
{ "$push": { "pets": { "name": petName, "data": [content] } } }
这感觉更干净,实际上可以使用“索引”进行匹配,当然
$exists
根本不能。如果使用
.findOneAndUpdate()
,当然会有更多的开销,因为这毕竟是对服务器的“两个”实际调用,您需要等待响应,而不是 Bulk 方法只是“一个”。但是,如果您需要返回的文档(无论如何,选项是驱动程序中的默认值),那么要么这样做,要么类似地等待
.bulkWrite()
的 Promise 解析,然后在完成后通过 .findOne()
获取文档。尽管在修改后通过 .findOne()
执行它并不是真正的“原子”,并且可能在进行另一个类似修改后返回文档,而不仅仅是在该特定更改的状态下。N.B 还假设除了
"pets"
中子文档的键作为“集合”之外,您对包含的数组的其他意图也是通过提供给函数的附加 content
添加到该“集合”中。如果您只想覆盖一个值,那么只需应用 $set
而不是 $addToSet
并类似地包装为数组。但是,前者就是您要问的,这听起来很合理。
顺便提一句。请通过本示例中可怕的设置代码清理实际代码中的查询和更新对象:)
作为一个独立的 list 来演示:
var async = require('async'),
mongodb = require('mongodb'),
MongoClient = mongodb.MongoClient;
MongoClient.connect('mongodb://localhost/test',function(err,db) {
var coll = db.collection('pettest');
var petName = "fido",
content = "bbb";
var filter1 = { "_id": 1 },
filter2 = { "_id": 1 },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
console.log(JSON.stringify(update1,undefined,2));
console.log(JSON.stringify(update2,undefined,2));
function CleanInsert(callback) {
async.series(
[
// Clean data
function(callback) {
coll.deleteMany({},callback);
},
// Insert sample
function(callback) {
coll.insert({ "_id": 1, "pets": [{ "fido": ["abc"] }] },callback);
}
],
callback
);
}
async.series(
[
CleanInsert,
// Modify Bulk
function(callback) {
coll.bulkWrite([
{ "updateOne": {
"filter": filter1,
"update": update1
}},
{ "updateOne": {
"filter": filter2,
"update": update2
}}
]).then(function(res) {
console.log(JSON.stringify(res,undefined,2));
coll.findOne({ "_id": 1 }).then(function(res) {
console.log(JSON.stringify(res,undefined,2));
callback();
});
},callback);
},
CleanInsert,
// Modify Promise all
function(callback) {
var operations = [
coll.findOneAndUpdate(filter1,update1,{ "returnOriginal": false }),
coll.findOneAndUpdate(filter2,update2,{ "returnOriginal": false })
];
Promise.all(operations).then(function(res) {
//console.log(JSON.stringify(res,undefined,2));
console.log(
JSON.stringify(
res.filter(function(el) { return el.value != null })[0].value
)
);
callback();
},callback);
}
],
function(err) {
if (err) throw err;
db.close();
}
);
});
和输出:
{
"$addToSet": {
"pets.$.fido": "bbb"
}
}
{
"$push": {
"pets": {
"fido": [
"bbb"
]
}
}
}
{
"ok": 1,
"writeErrors": [],
"writeConcernErrors": [],
"insertedIds": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 1,
"nModified": 1,
"nRemoved": 0,
"upserted": []
}
{
"_id": 1,
"pets": [
{
"fido": [
"abc",
"bbb"
]
}
]
}
{"_id":1,"pets":[{"fido":["abc","bbb"]}]}
随意更改为不同的值以查看如何应用不同的“集合”。
关于node.js - $addToSet 基于对象键存在,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/35762192/