Upsert Document和/或添加子文档

时间:2015-08-07 04:59:27

标签: mongodb mongoose mongodb-query

我一直在努力解决MongoDB,Mongoose和JavaScript的异步特性,以及如何最好地对集合进行多次更新。

我有一张Excel客户端和联系人数据。有些客户端具有多个联系人,每行一个,客户端数据相同(因此客户端名称可以用作唯一键 - 实际上在使用unique: true定义的模式中)。

我想要实现的逻辑是:

  1. 在客户端集合中搜索以clientName为键
  2. 的客户端
  3. 如果找不到匹配的clientName,则为该客户端创建一个新文档(不是upsert,如果客户端文档已经在数据库中,我不想更改任何内容)
  4. 使用firstNamelastName作为密钥,检查客户端文档中的联系人数组中是否已存在该联系人
  5. 如果未找到联系人,则联系到阵列的$push
  6. 当然,我们可能很容易出现客户端不存在的情况(因此创建),然后立即,工作表的下一行,是同一客户端的另一个联系人,所以我想要找到现有的(刚创建的)客户端和$push第二个新的联系人进入数组。

    我试过这个,但它不起作用:

    Client.findOneAndUpdate(
      {clientName: obj.client.clientname},
      {$set: obj.client, $push: {contacts: obj.contact}},
      {upsert: true, new: true},
      function(err, client){
        console.log(client)
      }
    )
    

    我已经仔细研究过其他问题,例如:

    但是无法得到解决方案......我得出的结论是,我可能必须使用一些应用程序逻辑来执行查找,然后在我的代码中进行决策,然后写入,而不是使用单个Mongoose /蒙古的声明,但随后异步的问题使他们的头脑变得丑陋。

    有什么建议吗?

2 个答案:

答案 0 :(得分:2)

处理这个问题的方法并不简单,因为将“upserts”与向“数组”添加项目混合在一起很容易导致不希望的结果。它还取决于您是否希望逻辑设置其他字段,例如“计数器”,表示数组中有多少联系人,您只想在添加或删除项目时增加/减少。

在最简单的情况下,如果“联系人”仅包含一个奇异值,例如ObjectId链接到另一个集合,那么$addToSet修饰符效果很好,只要没有“计数器“涉及:

Client.findOneAndUpdate(
    { "clientName": clientName },
    { "$addToSet": { "contacts":  contact } },
    { "upsert": true, "new": true },
    function(err,client) {
        // handle here
    }
);

这一切都很好,因为你只是在测试是否在“clientName”上匹配doucment,如果不是它的话。无论是否匹配,$addToSet运算符都会处理独特的“奇异”值,即任何真正独特的“对象”。

遇到困难的地方有:

{ "firstName": "John", "lastName": "Smith", "age": 37 }

已经在contacts数组中,然后你想做这样的事情:

{ "firstName": "John", "lastName": "Smith", "age": 38 }

你的实际意图是,这是“相同的”约翰史密斯,只是“年龄”没有区别。理想情况下,您只想“更新”数组条目end neiter创建新数组或新文档。

使用.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”。这是不可取的,因此没有“upsert”。

他们实际上分别检查数组元素是否存在,并更新现有元素或创建新元素。因此,总的来说,所有操作都意味着在发生upsert的情况下修改“一次”或最多“两次”。可能的“两次”产生很少的开销,没有真正的问题。

同样在第三个语句中,$not运算符反转$elemMatch的逻辑,以确定不存在具有查询条件的数组元素。

使用.findOneAndUpdate()进行翻译会成为一个问题。它不仅是现在重要的“成功”,它还决定了最终内容的返回方式。

所以这里最好的想法是在“系列”中运行事件,然后对结果进行一些魔术,以便返回结束的“更新”表单。

我们将在此处使用的帮助包括async.waterfalllodash库:

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需要进行不同的操作,而且您的问题本身也将输入数据作为对象形式。但这些本身就是答案。

至少应该让您开始了解如何处理更新模式。

答案 1 :(得分:0)

我没有使用猫鼬,所以我发布了一个mongo shell更新;对不起我认为以下情况可以做到:

 db.clients.update({$and:[{'clientName':'apple'},{'contacts.firstName': {$ne: 'nick'}},{'contacts.lastName': {$ne: 'white'}}]}, 
                  {$set:{'clientName':'apple'}, $push: {contacts: {'firstName': 'nick', 'lastName':'white'}}},
                  {upsert: true });

所以:

如果客户" apple"它不存在,它是通过给定名字和姓氏的联系人创建的。如果它存在,并且没有给定的联系,它就会被推送。如果它存在,并且已经有给定的联系,则没有任何反应。