2016-03-03 54 views
3

我有一个嵌入文档的数组'pets': [{'fido': ['abc']}。当我给宠物添加宠物时,如何检查宠物是否已经存在?例如,如果我再次添加fido ...我如何检查是否只有fido存在,而不是添加它?我希望我可以使用$addToSet,但我只想检查一部分(宠物名称)。加入fido两次

User.prototype.updatePetArray = function(userId, petName) { 
    userId = { _id: ObjectId(userId) }; 
    return this.collection.findOneAndUpdate(userId, 
    { $addToSet: { pets: { [petName]: [] } } }, 
    { returnOriginal: false, 
     maxTimeMS: QUERY_TIME }); 

结果:

{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': []}]}} 

回答

1

如果有总是将成为"pets"阵列(即petName作为密钥)的每个成员中“可变的”内容然后$addToSet是不适合你。至少不是在您希望应用它的阵列级别。

相反,你基本上需要对文档的“钥匙”的$exists测试被包含在阵列中,那么无论$addToSet为“载”与positional $运营商匹配键的阵列,其中“键“直接与$push不匹配的”宠物“阵列,新的内部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()解决,然后完成后通过.findOne()提取文档。虽然修改后通过.findOne()修改后不会真的是“原子的”,并且可能会在“之后”返回文档进行另一个类似的修改,而不仅仅是该特定更改的状态。


NB还假设除了子文档的键在"pets"为“设置”您的其他意图用于阵列包含被添加到“设置”,以及经由提供给函数的附加content 。如果您只是想覆盖一个值,那么只需应用$set而不是$addToSet,并将其类似地换成数组。

但是,前者就是你所问的,这听起来很合理。

顺便说一句。请清理被可怕的设置代码在这个例子中的查询和更新对象实际代码:)


作为一个自包含上市证明:

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"]}]} 

随意更改为不同的值,以了解如何应用不同的“集合”。

+0

谢谢!我最初有''宠物':[{“名字”:“fido”,“data”:[“abc”]}]'并且认为它不是很好。我会改变这一点......谢谢你确认这一点。 – dman

+0

如果可能的话,我想避免'pets'数组上的额外索引,以节省内存。由于'pets'数组是User doc的嵌入式文档,因此不会搜索索引的'_id:ObjectId()'就足够了吗?我仍然可以使用'“pets.fido”:{“$ exists”:false}'和'“pets.name”:{“$ ne”:petName}'没有额外的索引和相同的性能? – dman

+1

@dman只要你在查询中包含'_id',它就不会成为问题,因为文档已经被该值所选中。如果它在结构上有所作为,并且指数收益来自何处,那么您希望对此来源的文档进行任何类型的分析。命名键在分析中确实不能很好地工作,这是避免它们的强有力的理由。至于你的内存问题,这个get一次又一次提出,但真正的差别是微不足道的,甚至在有线老虎(内部哈希键)下更是如此。 –

1

请尝试这一个与string template,这里是一个例子蒙戈下运行外壳

> var name = 'fido'; 
> var t = `pets.${name}`; \\ string temple, could parse name variable 
> db.pets.find() 
    { "_id" : ObjectId("56d7b5019ed174b9eae2b9c5"), "pets" : [ { "fido" : [ "abc" ]} ] } 

用下面update命令,它会如果存在相同的宠物名称,则不更新它。

> db.pets.update({[t]: {$exists: false}}, {$addToSet: {pets: {[name]: []}}}) 
    WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 }) 

如果pets文档

> db.pets.find() 
{ "_id" : ObjectId("56d7b7149ed174b9eae2b9c6"), "pets" : [ { "fi" : [ "abc" ] } ] } 

更新后与

> db.pets.update({[t]: {$exists: false}}, {$addToSet: {pets: {[name]: []}}}) 
    WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) 

结果表明添加的宠物名字,如果它不存在。

> db.pets.find() 
    { "_id" : ObjectId("56d7b7149ed174b9eae2b9c6"), "pets" : [ { "fi" : [ "abc" ] }, { "fido" : [ ] } ] } 
+0

这不是有效的JavaScript。键“stringify”,因此它正在寻找't'并尝试添加'[name]'作为键而不是变量插值。也没有那么简单 –

+0

@BlakesSeven,我的坏,我犯了以前的答案错误。我用一些测试代码更新了我的答案。现在看起来好吗? – zangw

+0

我还没有尝试过,因为我可能会改变阵列结构。但这是一个好主意!我喜欢它也很干净。 – dman

相关问题