批量更新Mongodb中匹配子文档的数组

Kum*_*ran 8 mongodb mongodb-query

我在 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)
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

对关联费率卡的任何更改都会将更新传播到“费率”数组中的多个子文档。

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

{
    "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)
}
Run Code Online (Sandbox Code Playgroud)

有没有办法增量更新数组“rates”下的子文档,而不将整个文档加载到内存中,以便合并更改?假设我的子文档标识符是rates.[].productId,rates.[].month和的组合rates.[].rateCardId

我可以使用$[<identifier>]3.6 一次更新多个文档,但具有相同的值。

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

而在我的例子中,值会根据上述来自不同系统的标识符组合在文档之间发生变化。

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

Nei*_*unn 7

最简短的回答是“是”和“否”。

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

这里的特定示例的问题是“更改集”中的条目之一(最后一个条目)实际上与当前存在的任何数组成员都不匹配。这里的“假定”操作是将$push新的不匹配成员放入未找到的数组中。然而,该特定操作不能在“单个操作”中完成,但您可以使用bulkWrite()发出“多个”语句来覆盖这种情况。

匹配不同的数组条件

以点为单位进行解释,请考虑“变更集”中的前两项。您可以应用多个“单个”更新语句,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)
      }
    ]
  }
)
Run Code Online (Sandbox Code Playgroud)

如果你运行它,你会看到修改后的文档变成:

{
        "_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
                }
        ]
}
Run Code Online (Sandbox Code Playgroud)

请注意,您在列表中指定每个“identfier”arrayFilters并具有多个条件来匹配元素,如下所示:

  {
    "one.productId": NumberInt(1234),
    "one.rateCardId": NumberInt(1),
    "one.month": NumberInt(201801)
  },
Run Code Online (Sandbox Code Playgroud)

因此,每个“条件”有效地映射为:

  <identifier>.<property>
Run Code Online (Sandbox Code Playgroud)

"rates"所以它知道要通过更新块中的语句查看数组$[<indentifier>]

 "rates.$[one]"
Run Code Online (Sandbox Code Playgroud)

并查看 的每个元素"rates"以匹配条件。因此,"one"标识符将匹配以 为前缀的条件"one",同样对于以 为前缀的另一组条件"two",因此实际的更新语句仅适用于那些与分配给标识符的条件相匹配的语句。

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

{ "$set": { "rates.$[one].rate": 400, "rates.$[two].rate": 500 } }
Run Code Online (Sandbox Code Playgroud)

添加不匹配的对象

因此,第一部分相对容易理解,但如上所述,$push对“不存在的元素”执行 a 是另一回事,因为我们基本上需要“文档”级别的查询条件才能确定数组元素不见了”。

这本质上意味着您需要发出更新来查找$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 }
)
Run Code Online (Sandbox Code Playgroud)

请注意$elemMatch查询过滤器,因为这是通过“多个条件”匹配数组元素的要求。我们不需要在arrayFilters条目上这样做,因为它们查看它们已经应用到的每个数组项,但作为“查询”,条件需要$elemMatch简单的“点符号”会返回不正确的匹配。

另请参阅$not此处使用运算符来“否定” ,因为我们的真实条件是仅将“没有匹配数组元素”的$elemMatch文档与提供的条件相匹配,这就是选择附加新元素的理由。

向服务器发出的单个语句实际上尝试了四次更新操作,其中一个用于尝试更新匹配的数组元素,另一个用于尝试发现文档与数组元素的条件不匹配的三个$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
                }
        ]
}
Run Code Online (Sandbox Code Playgroud)

根据实际不匹配的元素数量,bulkWrite()响应将报告其中有多少语句实际匹配并影响文档。在这种情况下,它是2匹配和修改的,因为“第一个”更新操作与现有数组条目匹配,而“最后一个”更改更新与文档不包含数组条目匹配并执行修改$push

结论

所以你就有了组合方法,其中:

  • 您的问题中“更新”的第一部分非常简单,可以在单个语句中完成,如第一部分所示。

  • 第二部分是当前文档数组中“当前不存在”bulkWrite()的数组元素,这实际上需要您使用以便在单个请求中发出“多个”操作。

因此update,对于单个操作来说是“YES”。但增加差异就意味着多次操作。但是您可以将这两种方法结合起来,正如此处演示的那样。


有许多“奇特”的方法可以使用代码根据“更改集”数组内容构造这些语句,因此您不需要对每个成员进行“硬编码”。

作为 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 });
Run Code Online (Sandbox Code Playgroud)

这将动态构建“批量”更新操作的列表,如下所示:

[
  {
    "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
          }
        }
      }
    }
  }
]
Run Code Online (Sandbox Code Playgroud)

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

您可以用任何语言进行此类动态对象构造,并且所有 MongoDB 驱动程序都接受您可以“操作”的某种结构类型的输入,然后在实际发送到服务器执行之前将其转换为 BSON。

注意<identifier>forarrayFilters 必须由字母数字字符组成,并且必须以字母字符开头。因此,在构造动态语句时,我们使用前缀"a",然后是正在处理的每个项目的当前数组索引。