Mongodb中匹配子文档的批量更新数组 [英] Bulk update array of matching sub document in Mongodb

查看:47
本文介绍了Mongodb中匹配子文档的批量更新数组的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在Mongodb 3.6上运行.以下是我的文档结构,其中存储了产品列表的月费率信息:

{
  "_id": 12345,
  "_class": "com.example.ProductRates",
  "rates": [
    {
      "productId": NumberInt(1234),
      "rate": 100.0,
      "rateCardId": NumberInt(1),
      "month": NumberInt(201801)
    },
    {
      "productId": NumberInt(1234),
      "rate": 200.0,
      "rateCardId": NumberInt(1),
      "month": NumberInt(201802)
    },
    {
      "productId": NumberInt(1234),
      "rate": 400.0,
      "rateCardId": NumberInt(2),
      "month": NumberInt(201803)
    },
    {
      "productId": NumberInt(1235),
      "rate": 500.0,
      "rateCardId": NumberInt(1),
      "month": NumberInt(201801)
    },
    {
      "productId": NumberInt(1235),
      "rate": 234,
      "rateCardId": NumberInt(2),
      "month": NumberInt(201803)
    }
  ]
}

对价目表的任何更改都会将更新传播到'rates'数组中的多个子文档.

以下是需要在上述文档中应用的更改

{
    "productId" : NumberInt(1234), 
    "rate" : 400.0, 
    "rateCardId": NumberInt(1),
    "month" : NumberInt(201801)
}, 
{
    "productId" : NumberInt(1234), 
    "rate" : 500.0, 
    "rateCardId": NumberInt(1),
    "month" : NumberInt(201802)
}, 
{
    "productId" : NumberInt(1235), 
    "rate" : 700.0, 
    "rateCardId": NumberInt(1),
    "month" : NumberInt(201802)
}

是否有一种方法可以递增地更新数组"rates"下的子文档,而无需将整个文档加载到内存中以合并更改?可以说我的子文档标识符是rates.[].productIdrates.[].monthrates.[].rateCardId的组合.

我能够使用3.6中的$[<identifier>]一次更新多个文档,但是具有相同的值.

db.avail.rates_copy.update(
  { "_id" : 12345 },
  { $set: { "rates.$[item].rate": 0  } },
  { multi: true, 
   arrayFilters: [ { "item.rateCardId": {$in: [ 1, 2]} } ]
  }
)

在我看来,基于上述标识符组合(来自不同系统),文档之间的值将发生变化.

有没有办法这样说,用新值更新变更集中与(productId,month和rateCardId)匹配的所有子文档.

解决方案

最简短的答案是是"和否".

确实存在一种匹配单个数组元素并在单个语句中用单独的值更新它们的方法,因为实际上您可以提供多个" arrayFilters条件,并在您的update语句中使用这些标识符.

这里特定样本的问题在于,更改集"中的一项(最后一项)实际上与当前存在的任何数组成员都不匹配.这里的假定"操作将是 $push 找不到数组的新的不匹配成员.但是,该特定操作 不能通过单个操作" 完成,但是您可以使用

匹配不同的数组条件

以点数解释,请考虑变更集"中的前两项.您可以使用带有多个arrayFilters单个" 更新语句,如下所示:

db.avail_rates_copy.updateOne(
  { "_id": 12345 },
  { 
    "$set": {
      "rates.$[one]": {
        "productId" : NumberInt(1234), 
        "rate" : 400.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201801)
      },
      "rates.$[two]": {
        "productId" : NumberInt(1234), 
        "rate" : 500.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201802)
      } 
    }
  },
  { 
    "arrayFilters": [
      {
        "one.productId": NumberInt(1234),
        "one.rateCardId": NumberInt(1),
        "one.month": NumberInt(201801)
      },
      {
        "two.productId": NumberInt(1234),
        "two.rateCardId": NumberInt(1),
        "two.month": NumberInt(201802)
      }
    ]
  }
)

如果您运行该命令,将会看到修改后的文档:

{
        "_id" : 12345,
        "_class" : "com.example.ProductRates",
        "rates" : [
                {                             // Matched and changed this by one
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {                            // And this as two
                        "productId" : 1234,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201802
                },
                {
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 2,
                        "month" : 201803
                },
                {
                        "productId" : 1235,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {
                        "productId" : 1235,
                        "rate" : 234,
                        "rateCardId" : 2,
                        "month" : 201803
                }
        ]
}

请注意,您要在arrayFilters列表中指定具有多个条件的每个标识符",以匹配元素,如下所示:

  {
    "one.productId": NumberInt(1234),
    "one.rateCardId": NumberInt(1),
    "one.month": NumberInt(201801)
  },

因此每个条件"有效地映射为:

  <identifier>.<property>

因此,它知道要通过

并查看"rates"的每个元素以匹配条件.因此,"one"标识符将匹配以"one"前缀的条件,并且对于其他以"two"前缀的条件集也是如此,因此,实际的更新语句仅适用于与分配给该标识符的条件相匹配的条件.

如果您只想使用"rates"属性而不是整个对象,那么您只需将其标记为:

{ "$set": { "rates.$[one].rate": 400, "rates.$[two].rate": 500 } }

添加不匹配的对象

因此,第一部分相对容易理解,但是如所述 $push 对于不存在的元素"是另一回事,因为我们基本上需要在文档"级别上的查询条件才能确定数组元素缺失". >

这实际上意味着您需要使用发布更新$push 寻找每个数组元素以查看其是否存在.如果不存在,则该文档为匹配项,并且 $push 被执行.

这是 bulkWrite() 进入的地方播放,您可以通过对上述更改集"中的每个元素的上面的第一个操作添加额外的更新来使用它:

db.avail_rates_copy.bulkWrite(
  [
    { "updateOne": {
      "filter": { "_id": 12345 },
      "update": {
        "$set": {
          "rates.$[one]": {
            "productId" : NumberInt(1234), 
            "rate" : 400.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201801)
          },
          "rates.$[two]": {
            "productId" : NumberInt(1234), 
            "rate" : 500.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          },
          "rates.$[three]": {
            "productId" : NumberInt(1235), 
            "rate" : 700.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          }
        }
      },
      "arrayFilters": [
        {
          "one.productId": NumberInt(1234),
          "one.rateCardId": NumberInt(1),
          "one.month": NumberInt(201801)
        },
        {
          "two.productId": NumberInt(1234),
          "two.rateCardId": NumberInt(1),
          "two.month": NumberInt(201802)
        },
        {
          "three.productId": NumberInt(1235),
          "three.rateCardId": NumberInt(1),
          "three.month": NumberInt(201802)
        }
      ]    
    }},
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId" : NumberInt(1234), 
              "rateCardId": NumberInt(1),
              "month" : NumberInt(201801)
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId" : NumberInt(1234), 
            "rate" : 400.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201801)
          }
        }
      }
    }},
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId" : NumberInt(1234), 
              "rateCardId": NumberInt(1),
              "month" : NumberInt(201802)
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId" : NumberInt(1234), 
            "rate" : 500.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          }
        }
      }
    }},
    { "updateOne": {
      "filter": {
        "_id": 12345,
        "rates": {
          "$not": {
            "$elemMatch": {
              "productId" : NumberInt(1235),
              "rateCardId": NumberInt(1),
              "month" : NumberInt(201802)
            }
          }
        }
      },
      "update": {
        "$push": {
          "rates": {
            "productId" : NumberInt(1235),
            "rate" : 700.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          }
        }
      }
    }}
  ],
  { "ordered": true }
)

请注意 $elemMatch 以及查询过滤器,因为这是通过多个条件"匹配数组元素的要求.我们不需要arrayFilters条目,因为它们会查看已应用到的每个数组项,但是作为查询",条件需要

另请参阅此处使用 $not 运算符否定" $elemMatch ,这是我们的真实条件只能将与数组元素不匹配" 的文档与提供的条件进行匹配,这就是选择附加新元素的理由.

发出给服务器的那条语句实际上尝试进行 4 个更新操作,作为一种尝试更新匹配的数组元素的操作,而对 3 个变更集的每个操作执行另一种操作尝试$push找到文档与变更集"中数组元素的条件不匹配的地方.

因此,结果符合预期:

{
        "_id" : 12345,
        "_class" : "com.example.ProductRates",
        "rates" : [
                {                               // matched and updated
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {                               // matched and updated
                        "productId" : 1234,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201802
                },
                {
                        "productId" : 1234,
                        "rate" : 400,
                        "rateCardId" : 2,
                        "month" : 201803
                },
                {
                        "productId" : 1235,
                        "rate" : 500,
                        "rateCardId" : 1,
                        "month" : 201801
                },
                {
                        "productId" : 1235,
                        "rate" : 234,
                        "rateCardId" : 2,
                        "month" : 201803
                },
                {                              // This was appended
                        "productId" : 1235,
                        "rate" : 700,
                        "rateCardId" : 1,
                        "month" : 201802
                }
        ]
}

根据bulkWrite()响应中实际不匹配的元素数,将报告其中有多少个语句实际上匹配并影响了文档.在这种情况下,它是2匹配和修改的,因为第一个"更新操作与现有数组条目匹配,而最后一个"更新更新与文档不包含数组条目并执行单个语句中完成,如第一部分所示.

  • 第二部分是当前文档数组中存在目前不存在" 的数组元素,这实际上需要您使用

  • 因此, update 对单个操作为是".但是添加差异表示多项操作.但是您可以将两种方法结合起来,如此处所示.


    您可以通过多种奇特"方式根据带有代码的变更集"数组内容来构造这些语句,因此您无需硬编码"每个成员.

    作为JavaScript的基本案例,并且与mongo shell的当前版本兼容(这有点令人讨厌地不支持对象散布运算符):

    db.getCollection('avail_rates_copy').drop();
    db.getCollection('avail_rates_copy').insert(
      {
        "_id" : 12345,
        "_class" : "com.example.ProductRates",
        "rates" : [
          {
            "productId" : 1234,
            "rate" : 100,
            "rateCardId" : 1,
            "month" : 201801
          },
          {
            "productId" : 1234,
            "rate" : 200,
            "rateCardId" : 1,
            "month" : 201802
          },
          {
            "productId" : 1234,
            "rate" : 400,
            "rateCardId" : 2,
            "month" : 201803
          },
          {
            "productId" : 1235,
            "rate" : 500,
            "rateCardId" : 1,
            "month" : 201801
          },
          {
            "productId" : 1235,
            "rate" : 234,
            "rateCardId" : 2,
            "month" : 201803
          }
        ]
      }
    );
    
    var changeSet = [
      {
          "productId" : 1234, 
          "rate" : 400.0, 
          "rateCardId": 1,
          "month" : 201801
      }, 
      {
          "productId" : 1234, 
          "rate" : 500.0, 
          "rateCardId": 1,
          "month" : 201802
      }, 
      {
    
          "productId" : 1235, 
          "rate" : 700.0, 
          "rateCardId": 1,
          "month" : 201802
      }
    ];
    
    var arrayFilters = changeSet.map((obj,i) => 
      Object.keys(obj).filter(k => k != 'rate' )
        .reduce((o,k) => Object.assign(o, { [`u${i}.${k}`]: obj[k] }) ,{})
    );
    
    var $set = changeSet.reduce((o,r,i) =>
      Object.assign(o, { [`rates.$[u${i}].rate`]: r.rate }), {});
    
    var updates = [
      { "updateOne": {
        "filter": { "_id": 12345 },
        "update": { $set },
        arrayFilters
      }},
      ...changeSet.map(obj => (
        { "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": Object.keys(obj).filter(k => k != 'rate')
                  .reduce((o,k) => Object.assign(o, { [k]: obj[k] }),{})
              }
            }
          },
          "update": {
            "$push": {
              "rates": obj
            }
          }
        }}
      ))
    ];
    
    db.getCollection('avail_rates_copy').bulkWrite(updates,{ ordered: true });
    

    这将动态构造一个看起来像批量"更新操作的列表:

    [
      {
        "updateOne": {
          "filter": {
            "_id": 12345
          },
          "update": {
            "$set": {
              "rates.$[u0].rate": 400,
              "rates.$[u1].rate": 500,
              "rates.$[u2].rate": 700
            }
          },
          "arrayFilters": [
            {
              "u0.productId": 1234,
              "u0.rateCardId": 1,
              "u0.month": 201801
            },
            {
              "u1.productId": 1234,
              "u1.rateCardId": 1,
              "u1.month": 201802
            },
            {
              "u2.productId": 1235,
              "u2.rateCardId": 1,
              "u2.month": 201802
            }
          ]
        }
      },
      {
        "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId": 1234,
                  "rateCardId": 1,
                  "month": 201801
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId": 1234,
                "rate": 400,
                "rateCardId": 1,
                "month": 201801
              }
            }
          }
        }
      },
      {
        "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId": 1234,
                  "rateCardId": 1,
                  "month": 201802
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId": 1234,
                "rate": 500,
                "rateCardId": 1,
                "month": 201802
              }
            }
          }
        }
      },
      {
        "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId": 1235,
                  "rateCardId": 1,
                  "month": 201802
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId": 1235,
                "rate": 700,
                "rateCardId": 1,
                "month": 201802
              }
            }
          }
        }
      }
    ]
    

    就像在一般答案的长格式"中所描述的一样,但当然只是使用输入的数组"内容来构造所有这些语句.

    您可以用任何语言进行这种动态对象构造,并且所有MongoDB驱动程序都接受您可以操纵"的某种类型的结构的输入,然后将其转换为BSON,然后再将其实际发送到服务器以供执行.

    注意:arrayFilters 必须<identifier>由字母数字字符组成,必须以字母字符开头.因此,在构造动态语句时,我们以"a"为前缀,然后是要处理的每个项目的当前数组索引.

    I am running on Mongodb 3.6. Below is the structure of my document, which stores monthly rate information for list of products:

    {
      "_id": 12345,
      "_class": "com.example.ProductRates",
      "rates": [
        {
          "productId": NumberInt(1234),
          "rate": 100.0,
          "rateCardId": NumberInt(1),
          "month": NumberInt(201801)
        },
        {
          "productId": NumberInt(1234),
          "rate": 200.0,
          "rateCardId": NumberInt(1),
          "month": NumberInt(201802)
        },
        {
          "productId": NumberInt(1234),
          "rate": 400.0,
          "rateCardId": NumberInt(2),
          "month": NumberInt(201803)
        },
        {
          "productId": NumberInt(1235),
          "rate": 500.0,
          "rateCardId": NumberInt(1),
          "month": NumberInt(201801)
        },
        {
          "productId": NumberInt(1235),
          "rate": 234,
          "rateCardId": NumberInt(2),
          "month": NumberInt(201803)
        }
      ]
    }
    

    Any changes to the ratecard associated, will propagate updates to multiple sub documents in the 'rates' array.

    Below are the changes that needs to be applied on the above document

    {
        "productId" : NumberInt(1234), 
        "rate" : 400.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201801)
    }, 
    {
        "productId" : NumberInt(1234), 
        "rate" : 500.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201802)
    }, 
    {
        "productId" : NumberInt(1235), 
        "rate" : 700.0, 
        "rateCardId": NumberInt(1),
        "month" : NumberInt(201802)
    }
    

    Is there a way to update the subdocuments under the array 'rates', incrementally without loading the entire document into the memory, inorder to merge the changes? Lets say my identifiers for the sub documents are combination of rates.[].productId, rates.[].month and rates.[].rateCardId.

    I am able to update multiple documents at once using $[<identifier>] in 3.6, but with same value.

    db.avail.rates_copy.update(
      { "_id" : 12345 },
      { $set: { "rates.$[item].rate": 0  } },
      { multi: true, 
       arrayFilters: [ { "item.rateCardId": {$in: [ 1, 2]} } ]
      }
    )
    

    Whereas in my case, values will change between the documents based on the above mentioned identifier combinations, which comes from a different system.

    Is there a way to say that, update all the sub-documents that matches with( productId, month and rateCardId) from the changeset, with new values.

    解决方案

    In the shortest answer, it's both "yes" and "no".

    There is indeed a way to match individual array elements and update them with separate values in a single statement, since you can in fact provide "multiple" arrayFilters conditions and use those identifiers in your update statement.

    The problem with your particular sample here is that one of the entries in your "change set" ( the last one ) does not actually match any array member that is currently present. The "presumed" action here would be to $push that new un-matched member into the array where it was not found. However that particular action cannot be done in a "single operation", but you can use bulkWrite() to issue "multiple" statements to cover that case.

    Matching Different Array Conditions

    Explaining that in points, consider the first two items in your "change set". You can apply a "single" update statement with multiple arrayFilters like this:

    db.avail_rates_copy.updateOne(
      { "_id": 12345 },
      { 
        "$set": {
          "rates.$[one]": {
            "productId" : NumberInt(1234), 
            "rate" : 400.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201801)
          },
          "rates.$[two]": {
            "productId" : NumberInt(1234), 
            "rate" : 500.0, 
            "rateCardId": NumberInt(1),
            "month" : NumberInt(201802)
          } 
        }
      },
      { 
        "arrayFilters": [
          {
            "one.productId": NumberInt(1234),
            "one.rateCardId": NumberInt(1),
            "one.month": NumberInt(201801)
          },
          {
            "two.productId": NumberInt(1234),
            "two.rateCardId": NumberInt(1),
            "two.month": NumberInt(201802)
          }
        ]
      }
    )
    

    If you ran that you would see the modified document becomes:

    {
            "_id" : 12345,
            "_class" : "com.example.ProductRates",
            "rates" : [
                    {                             // Matched and changed this by one
                            "productId" : 1234,
                            "rate" : 400,
                            "rateCardId" : 1,
                            "month" : 201801
                    },
                    {                            // And this as two
                            "productId" : 1234,
                            "rate" : 500,
                            "rateCardId" : 1,
                            "month" : 201802
                    },
                    {
                            "productId" : 1234,
                            "rate" : 400,
                            "rateCardId" : 2,
                            "month" : 201803
                    },
                    {
                            "productId" : 1235,
                            "rate" : 500,
                            "rateCardId" : 1,
                            "month" : 201801
                    },
                    {
                            "productId" : 1235,
                            "rate" : 234,
                            "rateCardId" : 2,
                            "month" : 201803
                    }
            ]
    }
    

    Note here that you specify each "identfier" within the list of arrayFilters with multiple conditions to match the element like so:

      {
        "one.productId": NumberInt(1234),
        "one.rateCardId": NumberInt(1),
        "one.month": NumberInt(201801)
      },
    

    So each "condition" effectively maps as:

      <identifier>.<property>
    

    So it knows to be looking at the "rates" array by the statement in the update block by the $[<indentifier>] :

     "rates.$[one]"
    

    And looks at each element of "rates" to match the conditions. So the "one" identifier would match the conditions prefixed with "one" and likewise for the other set of conditions prefixed with "two", therefore the actual update statement applies only to those which matches the conditions assigned to the identifier.

    If you just wanted the "rates" property as opposed to the whole object, then you just notate as:

    { "$set": { "rates.$[one].rate": 400, "rates.$[two].rate": 500 } }
    

    Adding Un-matched Objects

    So the first part is relatively simple to comprehend, but as stated doing a $push for the "element which is not there" is a different matter, since we basically need a query condition on the "document" level in order to determine that an array element is "missing".

    What this essentially means is that you need to issue an update with the $push looking for each array element to see if it exists or not. When it is not present, then the document is a match and the $push is performed.

    This is where bulkWrite() comes into play, and you use it by adding an additional update to our first operation above for every element in the "change set":

    db.avail_rates_copy.bulkWrite(
      [
        { "updateOne": {
          "filter": { "_id": 12345 },
          "update": {
            "$set": {
              "rates.$[one]": {
                "productId" : NumberInt(1234), 
                "rate" : 400.0, 
                "rateCardId": NumberInt(1),
                "month" : NumberInt(201801)
              },
              "rates.$[two]": {
                "productId" : NumberInt(1234), 
                "rate" : 500.0, 
                "rateCardId": NumberInt(1),
                "month" : NumberInt(201802)
              },
              "rates.$[three]": {
                "productId" : NumberInt(1235), 
                "rate" : 700.0, 
                "rateCardId": NumberInt(1),
                "month" : NumberInt(201802)
              }
            }
          },
          "arrayFilters": [
            {
              "one.productId": NumberInt(1234),
              "one.rateCardId": NumberInt(1),
              "one.month": NumberInt(201801)
            },
            {
              "two.productId": NumberInt(1234),
              "two.rateCardId": NumberInt(1),
              "two.month": NumberInt(201802)
            },
            {
              "three.productId": NumberInt(1235),
              "three.rateCardId": NumberInt(1),
              "three.month": NumberInt(201802)
            }
          ]    
        }},
        { "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId" : NumberInt(1234), 
                  "rateCardId": NumberInt(1),
                  "month" : NumberInt(201801)
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId" : NumberInt(1234), 
                "rate" : 400.0, 
                "rateCardId": NumberInt(1),
                "month" : NumberInt(201801)
              }
            }
          }
        }},
        { "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId" : NumberInt(1234), 
                  "rateCardId": NumberInt(1),
                  "month" : NumberInt(201802)
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId" : NumberInt(1234), 
                "rate" : 500.0, 
                "rateCardId": NumberInt(1),
                "month" : NumberInt(201802)
              }
            }
          }
        }},
        { "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId" : NumberInt(1235),
                  "rateCardId": NumberInt(1),
                  "month" : NumberInt(201802)
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId" : NumberInt(1235),
                "rate" : 700.0, 
                "rateCardId": NumberInt(1),
                "month" : NumberInt(201802)
              }
            }
          }
        }}
      ],
      { "ordered": true }
    )
    

    Note the $elemMatch withing the query filter, as this is a requirement to match an array element by "multiple conditions". We didn't need that on the arrayFilters entries because they only look at each array item they are applied to already, but as a "query" the conditions require $elemMatch as simple "dot notation" would return incorrect matches.

    Also see the $not operator is used here to "negate" the $elemMatch, as our true conditions are to only match a document which "has not matching array element" to the provided conditions, and that is what justifies selection for appending a new element.

    And that single statement issued to the server essentially attempts four update operations as one for attempting to update matched array elements, and another for each of the three "change sets" attempting to $push where the document was found to not match the conditions for the array element in the "change set".

    The result is therefore as expected:

    {
            "_id" : 12345,
            "_class" : "com.example.ProductRates",
            "rates" : [
                    {                               // matched and updated
                            "productId" : 1234,
                            "rate" : 400,
                            "rateCardId" : 1,
                            "month" : 201801
                    },
                    {                               // matched and updated
                            "productId" : 1234,
                            "rate" : 500,
                            "rateCardId" : 1,
                            "month" : 201802
                    },
                    {
                            "productId" : 1234,
                            "rate" : 400,
                            "rateCardId" : 2,
                            "month" : 201803
                    },
                    {
                            "productId" : 1235,
                            "rate" : 500,
                            "rateCardId" : 1,
                            "month" : 201801
                    },
                    {
                            "productId" : 1235,
                            "rate" : 234,
                            "rateCardId" : 2,
                            "month" : 201803
                    },
                    {                              // This was appended
                            "productId" : 1235,
                            "rate" : 700,
                            "rateCardId" : 1,
                            "month" : 201802
                    }
            ]
    }
    

    Depending on how many elements where actually un-matched the bulkWrite() response will report on how many of those statement actually matched and affected a document. In this case it's 2 matched and modified, since the "first" update operation matches existing array entries, and the "last" change update matches that the document does not contain the array entry and performs the $push to modify.

    Conclusion

    So there you have the combined approach, where:

    • The first part of "updating" in your question is very easy and can be done in a single statement, as is demonstrated in the first section.

    • The second part where there is an array element which "does not presently exist" within the current document array, this actually requires you use bulkWrite() in order to issue "multiple" operations in a single request.

    Therefore update, is "YES" to a single operation. But adding difference means multiple operations. But you can combine the two approaches just as is demonstrated here.


    There are many "fancy" ways in which you can construct these statements based on the "change set" array contents with code, so you don't need to "hardcode" each member.

    As a basic case for JavaScript and compatible with the current release of the mongo shell ( which somewhat annoyingly does not support object spread operators ):

    db.getCollection('avail_rates_copy').drop();
    db.getCollection('avail_rates_copy').insert(
      {
        "_id" : 12345,
        "_class" : "com.example.ProductRates",
        "rates" : [
          {
            "productId" : 1234,
            "rate" : 100,
            "rateCardId" : 1,
            "month" : 201801
          },
          {
            "productId" : 1234,
            "rate" : 200,
            "rateCardId" : 1,
            "month" : 201802
          },
          {
            "productId" : 1234,
            "rate" : 400,
            "rateCardId" : 2,
            "month" : 201803
          },
          {
            "productId" : 1235,
            "rate" : 500,
            "rateCardId" : 1,
            "month" : 201801
          },
          {
            "productId" : 1235,
            "rate" : 234,
            "rateCardId" : 2,
            "month" : 201803
          }
        ]
      }
    );
    
    var changeSet = [
      {
          "productId" : 1234, 
          "rate" : 400.0, 
          "rateCardId": 1,
          "month" : 201801
      }, 
      {
          "productId" : 1234, 
          "rate" : 500.0, 
          "rateCardId": 1,
          "month" : 201802
      }, 
      {
    
          "productId" : 1235, 
          "rate" : 700.0, 
          "rateCardId": 1,
          "month" : 201802
      }
    ];
    
    var arrayFilters = changeSet.map((obj,i) => 
      Object.keys(obj).filter(k => k != 'rate' )
        .reduce((o,k) => Object.assign(o, { [`u${i}.${k}`]: obj[k] }) ,{})
    );
    
    var $set = changeSet.reduce((o,r,i) =>
      Object.assign(o, { [`rates.$[u${i}].rate`]: r.rate }), {});
    
    var updates = [
      { "updateOne": {
        "filter": { "_id": 12345 },
        "update": { $set },
        arrayFilters
      }},
      ...changeSet.map(obj => (
        { "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": Object.keys(obj).filter(k => k != 'rate')
                  .reduce((o,k) => Object.assign(o, { [k]: obj[k] }),{})
              }
            }
          },
          "update": {
            "$push": {
              "rates": obj
            }
          }
        }}
      ))
    ];
    
    db.getCollection('avail_rates_copy').bulkWrite(updates,{ ordered: true });
    

    This will dynamically construct a list of "Bulk" update operations which would look like:

    [
      {
        "updateOne": {
          "filter": {
            "_id": 12345
          },
          "update": {
            "$set": {
              "rates.$[u0].rate": 400,
              "rates.$[u1].rate": 500,
              "rates.$[u2].rate": 700
            }
          },
          "arrayFilters": [
            {
              "u0.productId": 1234,
              "u0.rateCardId": 1,
              "u0.month": 201801
            },
            {
              "u1.productId": 1234,
              "u1.rateCardId": 1,
              "u1.month": 201802
            },
            {
              "u2.productId": 1235,
              "u2.rateCardId": 1,
              "u2.month": 201802
            }
          ]
        }
      },
      {
        "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId": 1234,
                  "rateCardId": 1,
                  "month": 201801
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId": 1234,
                "rate": 400,
                "rateCardId": 1,
                "month": 201801
              }
            }
          }
        }
      },
      {
        "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId": 1234,
                  "rateCardId": 1,
                  "month": 201802
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId": 1234,
                "rate": 500,
                "rateCardId": 1,
                "month": 201802
              }
            }
          }
        }
      },
      {
        "updateOne": {
          "filter": {
            "_id": 12345,
            "rates": {
              "$not": {
                "$elemMatch": {
                  "productId": 1235,
                  "rateCardId": 1,
                  "month": 201802
                }
              }
            }
          },
          "update": {
            "$push": {
              "rates": {
                "productId": 1235,
                "rate": 700,
                "rateCardId": 1,
                "month": 201802
              }
            }
          }
        }
      }
    ]
    

    Just like was described in the "long form" of the general answer, but of course simply uses the input "array" content in order to construct all of those statements.

    You can do such dynamic object construction in any language, and all MongoDB drivers accept input of some type of structure you are allowed to "manipulate" which is then transformed to BSON before it's actually sent to the server for execution.

    NOTE : The <identifier> for arrayFilters must consist of alpha-numeric characters and must begin with an alphabetical character. Hence whilst constructing the dynamic statement we prefix with "a" and then the current array index for each item being processed.

    这篇关于Mongodb中匹配子文档的批量更新数组的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆