DEBUG.WIKI
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
雨云;给项目一台云服务器
晚安云;香港 CN2 ¥15/月

Mongoose replaceOne 没报 CastError,Number 字段反而丢失

Mongoose9.11.3的replaceOne默认路径可能忽略替换文档内的转换错误。局部启用runValidators,并区分非法字段丢失与整文档替换本身的省略语义。

Mongoose 的 schema 把 price 定义为 Number,调用 replaceOne 时传入 price: 'abc',操作却没有抛出转换错误。重新读取文档,price 反而消失了。这时不能把“写入返回成功”理解为字段已经按 schema 校验。

Mongoose 9.11.3 存在这条具体路径,当前正式 9.11.4 仍保留相同处理:replaceOne 先构造替换文档,Number 转换失败的信息留在文档验证错误中;默认不运行后续验证,错误没有在写入前被抛出,无法转换的字段也没有进入替换结果。

replacement 是整份新文档,不是局部更新

假设原记录包含 name、price、description,而替换数据只有 name 和无法转换的 price。price 没有形成有效字段,description 又没有出现在替换数据里,最终两者都可能从记录中消失。

这两种删除原因需要分开看。忽略转换错误是这里要处理的问题;replaceOne 省略普通字段时替换掉旧值,则是整文档替换的语义。加上验证选项不会自动把遗漏的可选字段补回去。

若接口本意只是修改价格,应按既有业务约定使用局部更新,而不是拿不完整表单执行 replaceOne。已有接口确实允许整文档替换时,则应先确认允许替换的字段、必填约束和授权过滤条件。

在这次替换上明确启用验证

对已连接的 Product 模型,可以在局部操作中设置 runValidators。下面接收应用已经构造的授权过滤器和完整替换数据,不建立连接,也不清空集合:

export async function replaceProduct(Product, authorizedFilter, replacement) {
  return Product.replaceOne(authorizedFilter, replacement, {
    runValidators: true,
    upsert: false,
  }).exec();
}

authorizedFilter 应保留租户、权限及应用已有的并发控制条件,不能直接采用客户端提交的任意查询对象。replacement 也应来自接口允许的字段集合,不应把请求对象原样透传。

在这条实现中,runValidators 为 true 时会验证构造后的替换文档,Number 的 CastError 会成为 ValidationError 的字段错误,阻止后面的数据库 replaceOne。调用方应沿原有输入错误流程处理,不能吞掉错误后再用默认选项重试。

也可以在写入前单独构造模型实例并执行 validate,用于更早反馈字段问题;这不能代替实际写入入口的校验和权限检查。字段类型转换还受 setter、自定义类型和默认值影响,不要用一个通用 Number() 遍历所有输入来替代 schema。

验证没有替你解决所有选项差异

replaceOne 与 findOneAndReplace、bulkWrite 的替换路径并不完全相同。不能因为另一个方法对同一输入抛错,就断定当前 replaceOne 一定也会抛错;也不能据此无条件改方法而忽略返回值、中间件和选项差异。

尤其不能把 runValidators 当成 strict 选项问题的通用修复。当前路径构造替换模型时如何处理未知字段,需要按实际 schema 和调用选项另行确认。旧文中的bulkWrite 数组过滤值未转换发生在 arrayFilters 条件上,升级那条修复不会自动解决本文的整文档验证入口。

在隔离记录中保留原始文档,先用不合法的 price 确认调用失败且数据库内容未变,再用合法的完整替换数据检查实际字段。还要覆盖省略可选字段、缺少必填字段、过滤器未匹配和并发条件不符,确认接口对这些结果各有明确处理。matchedCount 为零不是验证失败;modifiedCount 也不能证明每个业务字段都符合预期。

修正代码不会恢复已经被替换掉的字段。若线上出现数据丢失,应依据保留的业务记录和备份核对具体文档,走单独的数据恢复流程,不能把旧请求整批重放。