【问题标题】:Upsert Document and/or add a Sub-DocumentUpsert 文档和/或添加子文档
【发布时间】:2015-10-30 10:34:20
【问题描述】:

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

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

我要实现的逻辑是:

  1. 在 Client 集合中搜索以 clientName 为键的客户端
  2. 如果未找到匹配的客户端名称,则为该客户端创建一个新文档(不是 upsert,如果客户端文档已在数据库中,我不想更改任何内容)
  3. 使用firstNamelastName 作为键检查联系人是否已存在于客户端文档的联系人数组中
  4. 如果未找到联系人,则 $push 将该联系人添加到数组中

当然,我们很容易遇到客户不存在(因此被创建)的情况,然后立即,工作表的下一行,是同一客户的另一个联系人,所以我想要找到现有的(刚刚创建的)客户端和$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/ Mongo 声明,但随后异步问题出现在他们丑陋的头脑中。

有什么建议吗?

【问题讨论】:

  • 我正在查看 async.js,特别是 github.com/caolan/async#seriestasks-callback,我认为这可能有助于对一个客户文档的特定查询,但同一客户的联系人问题下一行仍然存在...这可能意味着一些复杂、丑陋的代码在执行数据库事务之前尝试将所有联系人解析到一个数组中...说不是这样!

标签: mongodb mongoose mongodb-query


【解决方案1】:

我不使用 mongoose,所以我会发布一个 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”不存在,则创建它,并带有给定名字和姓氏的联系人。如果它存在,并且没有给定的联系人,则将其推送。如果它存在,并且已经拥有给定的联系人,则不会发生任何事情。

【讨论】:

    【解决方案2】:

    处理这个问题的方法并不简单,因为将“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.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 需要进行不同的操作,而且您的问题本身也将输入数据作为对象形式。但实际上这些本身就是答案。

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

    【讨论】:

    • 它和我担心的一样复杂 :) 但我想这是异步代码工作原理的内在原因。我认为 async.waterfall 方法,它基本上使事务相互依赖(有点同步)是我要走的路 - Mongoose 中对批量操作的支持似乎有限(尽管有 Model.collection.insert())而且我没有出于一致性的原因,不想下拉到本机驱动程序。无论如何,感谢您提供的非常完整和详细的信息,回复,不胜感激。哦,还有一个名字的提示 - Orac、Liberator 和所有这些:)
    • 您能确认我的查询不起作用吗?我认为当已经与给定的名字和姓氏有联系时,线索排除了客户,但尚未尝试过
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-21
    • 1970-01-01
    • 2013-11-24
    相关资源
    最近更新 更多