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

时间:2018-04-16 23:13:12

标签: 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)
    }
  ]
}

对相关的价目表进行的任何更改都会将更新传播到'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)匹配的所有子文档。

1 个答案:

答案 0 :(得分:0)

在最短的答案中,它是&#34;是&#34;和&#34;不&#34;。

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

此处您的特定示例的问题是您的&#34;更改集中的一个条目&#34; (最后一个)实际上并不匹配当前存在的任何数组成员。 &#34;推测&#34;这里的动作是将$push新的未匹配成员放入未找到它的数组中。但是,&#34;单个操作&#34; 中的不能,但您可以使用bulkWrite()来发出&#34;多个&# 34;陈述以涵盖该案件。

匹配不同的数组条件

以点数解释,考虑你的&#34;变更集中的前两项&#34;。您可以使用多个arrayFilters应用&#34;单个&#34; 更新语句,如下所示:

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
                }
        ]
}

请注意,您指定了每个&#34; identfier&#34;在arrayFilters列表中,有多个条件匹配元素,如下所示:

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

所以每个&#34;条件&#34;有效映射为:

  <identifier>.<property>

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

 "rates.$[one]"

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

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

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

添加未匹配的对象

所以第一部分理解起来相对简单,但正如所说的那样,为#34;元素做了一个$push,而不是那个&#34;是一个不同的问题,因为我们基本上需要一个查询条件&#34;文件&#34; level以确定数组元素是&#34;缺少&#34;。

这实质上意味着您需要发布更新$push查找每个数组元素以查看它是否存在。如果不存在,则文档匹配并执行$push

这是bulkWrite()发挥作用的地方,您可以通过在上面的第一个操作中为&#34;更改集&#34;中的每个元素添加一个额外的更新来使用它:

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,因为这是要求通过&#34;多个条件匹配数组元素&#34;。我们在arrayFilters条目上不需要它,因为它们查看它们已经应用的每个数组项,但作为&#34;查询&#34;条件要求$elemMatch为简单的&#34;点符号&#34;会返回错误的匹配。

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

发给服务器的单个声明基本上尝试将四个更新操作作为尝试更新匹配数组元素的操作,而另一个用于三个&#34中的每一个;变更集&#34;尝试$push发现文档与&#34;更改集&#34;中的数组元素的条件不匹配。

因此结果符合预期:

{
        "_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匹配并修改,因为&#34;第一个&#34;更新操作匹配现有的数组条目,&#34; last&#34;更改更新匹配文档不包含数组条目并执行$push修改。

结论

所以你有合并的方法,其中:

  • &#34;更新&#34;的第一部分您的问题非常简单,可以在单一陈述中完成,如第一部分所示。

  • 当前文档数组中存在&#34;目前不存在&#34; 的数组元素的第二部分实际上需要您使用bulkWrite()为了发出&#34;多个&#34;单个请求中的操作。

因此更新,是&#34;是&#34;一次操作。但添加差异意味着多个操作。但是你可以将这两种方法结合起来,就像在这里演示的那样。

有很多&#34;花式&#34;您可以根据&#34;更改集来构建这些语句的方法。带代码的数组内容,因此您不需要&#34;硬编码&#34;每个成员。

作为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 });

这将动态构建一个&#34; Bulk&#34;更新操作看起来像:

[
  {
    "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
          }
        }
      }
    }
  }
]

就像&#34;长形式&#34;一般答案,但当然只是使用输入&#34;数组&#34;内容以构建所有这些陈述。

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

  

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