MongoDB更新管道:更新嵌套数组中元素的字段。

4

我有一个集合,其中包含这样的对象。

{
  materials: {
    "m1": {
      inventory: [
        {
          price: 100,
          amount: 65
        }
      ]
    }
  }
}

正如您所看到的,库存是层次结构中深嵌的一个数组。我想要更新它的amount字段。

从客户端收到物料id("m1")和库存索引(0)。

我必须使用更新管道,因为我在此文档中设置和取消设置一些其他字段。

这是我尝试过的:

await products.findOneAndUpdate(filters, [
  {
    $set: {
      "materials.m1.inventory.0.amount": 100,
    },
  },
]);

但是它会在第一个元素内创建一个名为0的新字段,并设置该对象中的amount。因此,生成的文档如下所示。
{
  materials: {
    "m1": {
      inventory: [
        {
          0: {
            amount: 100
          }
          price: 100,
          amount: 65
        }
      ]
    }
  }
}

我需要的是:

{
  materials: {
    "m1": {
      inventory: [
        {
          price: 100,
          amount: 100
        }
      ]
    }
  }
}

我能够识别需要更新的数组元素只有它的索引。

我正在使用nodejs mongodb驱动程序。如何编写更新管道的$set阶段?

2个回答

5

我认为没有其他直接的方法可以使用聚合管道更新特定索引位置的元素:

  • $range 用于生成从0到materials.m1.inventory数组长度的数组
  • $map 迭代上述范围数组的循环
  • $cond 检查当前元素是否为0,如果是则转到更新部分,否则返回输入索引的materials.m1.inventory中的元素
  • $arrayElemAtmaterials.m1.inventory获取特定元素
  • $mergeObjects 将当前对象与已更新amount属性的对象合并
await products.findOneAndUpdate(filters, [
  {
    $set: {
      "materials.m1.inventory": {
        $map: {
          input: {
            $range: [0, { $size: "$materials.m1.inventory" }]
          },
          in: {
            $cond: [
              { $eq: ["$$this", 0] }, // 0 position
              {
                $mergeObjects: [
                  { $arrayElemAt: ["$materials.m1.inventory", "$$this"] },
                  { amount: 100 } // input amount
                ]
              },
              { $arrayElemAt: ["$materials.m1.inventory", "$$this"] }
            ]
          }
        }
      }
    }
  }
]);

游乐场


2
非常好的解决方案。$mergeObjects 在这里派上了用场。 - Marco Luzzara
我认为没有必要迭代,你可以使用$arrayElemAt和$mergeObjects来准备更新的元素,然后使用$concatArrays和$slice来连接数组,如其他答案中所述 - 不确定哪种方法更有效。 - s7vr
1
@s7vr 你说得对,我们不确定,但如果 OP 想要更新的是第 0 个位置以外的其他位置,那么他可以使用我的解决方案。另一个答案只适用于更新第 0 个位置,是一个非常好的解决方案。 - turivishal
1
我确实想要更新其他索引。有时还需要更新同一数组的多个索引。这对我来说效果最佳。非常感谢! - Aruna Herath

2

findOneAndUpdate 使用聚合管道的问题在于你只能使用 $set$unset$replaceWith(以及它们的别名)。似乎 $set 或者 $addFields 无法覆盖数组内部的对象,因此你只能重新创建带有所需更改的整个数组。你基本上被迫按照以下方式构建查询:

db.collection.update(filters,
[
  {
    "$addFields": {
      "materials.m1.inventory": {
      ...
      }
    }
  }
])

这是我的实现方法,但可能还有更简单的方法(请参见替换单个对象数组的修改):

db.collection.update(filters,
[
  {
    "$addFields": {
      "materials.m1.inventory": {
        $concatArrays: [
          [
            {
              price: {
                $getField: {
                  field: "price",
                  input: {
                    $first: "$materials.m1.inventory"
                  }
                }
              },
              amount: 100
            }
          ],
          {
            $slice: [
              "$materials.m1.inventory",
              1,
              {
                $size: "$materials.m1.inventory"
              }
            ]
          }
        ]
      }
    }
  }
])

我正在使用$concatArrays进行连接操作:
  1. 一个仅包含一个对象的数组,该对象位于索引0处,但将amount字段设置为100。
  2. inventory 数组从索引1开始的切片。
显然,如果索引不同于0,则必须获取第一个切片inventory [0:index],单个对象数组(请参见第1点)以及切片 inventory [index + 1:] 在此处 查看演示。
编辑: @turivishal提出的$mergeObjects部分绝对比我的单个对象数组要好:无论您有2个还是20个字段,代码都是相同的。

谢谢。这帮助我更好地理解了 MongoDB 管道。我猜更新数组索引不起作用是因为我期望的语法在更新第0个元素或添加一个名为0的新字段方面存在歧义。是的,另一个答案更容易使用,因为我需要更新其他索引,有时还需要更新同一数组的多个索引。 - Aruna Herath
就像我之前说的,你可以始终进行2个切片并取中间索引处的对象。我不知道哪种解决方案在性能方面更好,但被接受的解决方案肯定很容易理解 :) - Marco Luzzara

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接