DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS

Mongoose bulkWrite 修改数为 0,arrayFilters 没按 schema 转换

Mongoose 9.11.1 批量更新遗漏 arrayFilters 值转换,字符串 ObjectId、数值或日期可能匹配不到元素。采用含修正的正式版,或按确认的 schema 显式构造条件并核对实际数组变化。

同一个 Mongoose 模型,updateOne() 可以改到数组元素,改成 bulkWrite() 后却返回 modifiedCount: 0,没有异常。若数组过滤器里的 _id 来自接口字符串,比较两次实际发出的 BSON 类型,别先把过滤器放宽成更新整个数组。

Mongoose 9.11.1 有这条具体路径:普通查询会按 schema 转换 arrayFilters 的值,批量更新只转换外层 filter 和 update,没有同样转换数组过滤值。模型内的 ObjectId、Number、Date 分别收到字符串时,都可能出现“外层文档匹配,数组元素不匹配”。8.24.5 也有报告,不能据此断言所有 8.x 都已得到同样回补。

名称一致,类型仍可能不一致

假定 schema 中 items 是子文档数组,包含自动生成的 ObjectId _id。更新形态是:

await Order.bulkWrite([
  {
    updateOne: {
      filter: { _id: orderId },
      update: { $set: { 'items.$[item].name': 'pear' } },
      arrayFilters: [{ 'item._id': itemIdText }],
    },
  },
]);

这里 item 是数组元素标识符,items 才是文档字段。若两处标识符不一致,通常会出现 No array filter found 一类错误;本例标识符正确,问题是 itemIdText 仍为字符串,而目标子文档保存的是 BSON ObjectId。

外层 filter 的 _id 能被转换,不代表 arrayFilters 里的值也走同一条转换。也不要把所有 24 位十六进制字符串都强制改成 ObjectId:实际 schema 把标识设计成字符串时,应保留字符串。

正式修复与旧版过渡

Mongoose 9.11.2 的正式变更已包含 bulkWrite 数组过滤值转换,当前 9.11.4 保留该处理。它在批量 updateOne、updateMany 路径中复制 arrayFilters,再按更新路径与 schema 转换条件。可以在项目兼容范围内更新到包含这项修正的正式版本,核对应用实际加载的依赖与锁文件。

短期无法更新时,在已确认 schema 类型的应用边界构造明确的 BSON 值。下面的函数接收已连接的模型、由服务端构造并包含授权范围的文档过滤器,以及待处理的子文档 ID:

import mongoose from 'mongoose';

export async function renameItem(Order, documentFilter, itemIdText) {
  if (typeof itemIdText !== 'string' || !/^[0-9a-fA-F]{24}$/.test(itemIdText)) {
    throw new TypeError('itemId must be a 24-character hexadecimal string');
  }
  const itemId = new mongoose.Types.ObjectId(itemIdText);

  return Order.bulkWrite([
    {
      updateOne: {
        filter: documentFilter,
        update: { $set: { 'items.$[item].name': 'pear' } },
        arrayFilters: [{ 'item._id': itemId }],
        upsert: false,
      },
    },
  ]);
}

documentFilter 必须来自应用已有的身份与租户检查,不能直接接收客户端提供的任意对象。数值和日期条件也应按各自字段约定校验后构造,不能用一个递归函数盲转所有字符串;无效日期、非有限数值或错误 ID 都应在写入前明确拒绝。

结果要回到目标元素上核对

在隔离记录中,分别比较字符串条件与明确 BSON 类型条件,并对照普通 updateOne 的结果。先确认目标元素存在、旧值与新值不同,避免字段原本已经是 pear,让一次正确匹配也显示修改数为零。

再重新读取目标数组,确认只有预期元素变化。modifiedCount 统计修改的文档数,不是数组元素数量;同一文档中多个元素满足条件时,计数不会逐元素增加。没有匹配的元素应保持不变,而不是通过去掉 arrayFilters、改成 $[] 或开启 upsert 强行制造成功。

已有 bulkWrite 可能包含多项操作。修正前先检查哪些项已经成功,尤其是带 $inc、$push 的写入,不能因为某一项没有修改就重放整批。代码回退同样不会撤销已经发生的数据库写入。