【问题标题】:$addToSet Based on Object key exists$addToSet 基于 Object 键是否存在
【发布时间】:2016-03-03 02:41:41
【问题描述】:

我有数组'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': []}]}}

【问题讨论】:

    标签: node.js mongodb mongodb-query


    【解决方案1】:

    如果"pets" 数组的每个成员中总是存在“可变”内容(即 petName 作为键),那么$addToSet 不适合您。至少不在您希望应用它的数组级别。

    相反,您基本上需要对包含在数组中的文档的“键”进行$exists 测试,然后使用positional $ 运算符对匹配键的“包含”数组进行$addToSet 测试,或者如果“key”不匹配,则将$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 并类似地包装为一个数组。

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

    顺便说一句。请通过此示例中可怕的设置代码清理您的实际代码中的查询和更新对象:)


    作为一个独立的列表来展示:

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

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

    【讨论】:

    • 谢谢!我最初有`“pets”:[{“name”:“fido”,“data”:[“abc”]}]`并认为它不是那么好。我会改回来...感谢您确认这一点。
    • 如果可能的话,我想避免在pets 数组上使用额外的索引,以节省内存。由于pets数组是用户文档的嵌入文档,搜索索引_id: ObjectId()不够吗?如果没有额外的索引和相同的性能,我是否仍然可以使用"pets.fido": { "$exists": false } "pets.name": { "$ne": petName }
    • @dman 只要您在查询中包含_id,这将不是问题,因为该值已经选择了文档。当您想要跨此源上的文档进行任何类型的分析时,它会在结构上产生影响以及索引优势的所在。命名键在分析中确实不能很好地工作,这是避免使用它们的一个强有力的理由。至于你的记忆问题,这个问题一遍又一遍地提出来,但真正的区别是微不足道的,在有线老虎(内部散列键)下更是如此。
    • @dman 所以这里的主要建议是不要过度优化编程的便利性和可读性,以及未来的分析选项,只是为了你“认为”会为你节省几个字节.真正的情况是它真的不会节省太多或任何东西。无论如何,要修改的等式匹配外部数组和要添加的不等式匹配为您提供了一个“真正的”等价操作来控制“集合”。但是你确实需要两个语句来控制这两种情况。
    【解决方案2】:

    请用string template试试这个,这里是一个在mongo shell下运行的例子

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

    【讨论】:

    • 这不是有效的 JavaScript。键“字符串化”,因此它正在寻找 t 并尝试将 [name] 添加为键而不是变量插值。也远没有那么简单
    • @BlakesSeven,我的错,我对以前的答案犯了错误。我已经用一些测试代码更新了我的答案。现在好像好了?
    • 我还没有尝试过这个......因为我可能会更改数组结构。但这是一个绝妙的主意!我也喜欢它的干净程度。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-15
    • 2011-06-06
    • 1970-01-01
    相关资源
    最近更新 更多