Mongoose 8 升级迁移指南:从 7.x 到 8.x 的全面破坏性变更解析
Mongoose 8 升级迁移指南从 7.x 到 8.x 的全面破坏性变更解析【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose从 Mongoose 7.x 升级到 8.x 引入了一系列破坏性变更backwards-breaking changes涉及查询 API 行为、MongoDB Node 驱动版本、文档删除语义、findOneAndUpdate()系列选项、enum/minimize校验规则、discriminator getter 执行顺序、以及 TypeScript 类型推断等多个维度。本文基于 docs/migrating_to_8.md 官方迁移文档逐项解析这些变更的含义、动机与升级路径并结合当前仓库源码Mongoose 9.x 代码基线印证各项变更在代码层面的落地形态帮助你快速完成迁移评估与升级改造。阅读前置条件如果你仍停留在 Mongoose 6.x 或更早版本请先阅读 Mongoose 6.x 到 7.x 迁移指南先升级到 7.x再按照本文迁移到 8.x。同时建议在升级前查看 MongoDB Node.js 驱动 v6.0.0 的发布说明因为 Mongoose 8 依赖的是驱动 v6 系列。目录findOneAndUpdate()的rawResult改为includeResultMetadataDocument.prototype.deleteOne()现在返回 QueryMongoDB Node 驱动升级到 v6移除findOneAndRemove()与findByIdAndRemove()移除count()移除 id Setternull对非必填 string enum 有效save()更新已有文档时应用 minimizebase schema paths 先于 discriminator paths 应用移除findOneAndUpdate()的overwrite选项findOneAndUpdate()与orFail() upsert 的行为变更create()等待所有 save 完成后再抛错Model.validate()返回对象副本TypeScript可选字段允许nullTypeScriptModel 构造参数全部可选从 schema 推断distinct()返回类型升级检查清单与验证方法1.findOneAndUpdate()的rawResult改为includeResultMetadata变更要点rawResult选项在findOneAndUpdate()、findOneAndReplace()和findOneAndDelete()中已被includeResultMetadata选项取代。迁移方式直接把rawResult: true替换为includeResultMetadata: true行为完全一致const filter { name: Will Riker }; const update { age: 29 }; const res await Character.findOneAndUpdate(filter, update, { new: true, upsert: true, // 将 rawResult: true 替换为 includeResultMetadata: true includeResultMetadata: true });源码印证从当前仓库源码看includeResultMetadata是findOneAndUpdate()、findOneAndReplace()、findOneAndDelete()系列方法的正式选项见 lib/query.js 与 lib/model.js 中对该选项的文档注释当其为true时返回 MongoDB 驱动的完整ModifyResult而非仅返回文档本身。查询内部实现中是否返回完整结果的关键分支位于 lib/query.js 等处const doc !options.includeResultMetadata ? res : res.value;——即默认false时直接返回res中的value被修改/删除的文档而true时返回完整的ModifyResult对象包含value、ok、lastErrorObject等元数据。测试验证仓库测试用例 test/model.findOneAndDelete.test.js 直接验证了两种取值的行为差异// includeResultMetadata: false 时返回被删除的文档本身 const doc await Test.findOneAndDelete({ name: Test }, { includeResultMetadata: false }); assert.equal(doc.ok, undefined); assert.equal(doc.name, Test); // includeResultMetadata: true 时返回完整的 ModifyResult const data await Test.findOneAndDelete({ name: Test }, { includeResultMetadata: true }); assert(data.ok); assert.equal(data.value.name, Test);影响范围任何依赖rawResult返回{ value, ok, lastErrorObject }结构例如 upsert 场景下需要读取lastErrorObject.upserted来获取新插入文档的_id的代码都必须迁移到includeResultMetadata。同时注意当includeResultMetadata: false默认且配合new: false、upsert 命中已存在文档时返回值为null的语义保持不变。2.Document.prototype.deleteOne()现在返回 Query变更要点Mongoose 7 中doc.deleteOne()返回一个解析为doc自身的 PromiseMongoose 8 中doc.deleteOne()返回一个Query 实例便于链式调用并与doc.updateOne()保持一致性。行为对比const numberOne await Character.findOne({ name: Will Riker }); // Mongoose 7 中q 是解析为 numberOne 的 Promise // Mongoose 8 中q 是一个 Query const q numberOne.deleteOne(); // Mongoose 7 中res numberOne // Mongoose 8 中res 是一个 DeleteResult const res await q;源码印证当前仓库中Model.prototype.deleteOne的实现位于 lib/model.js。其返回类型明确标注为return {Query} Query。实现细节包括通过this.$__where()基于文档的_id构造删除条件若文档无_id会抛出MongooseError(No _id found on document!)内部调用self.constructor.deleteOne()构造 Query并在 query 的pre/post钩子中串联文档级deleteOne中间件、子文档subdoc的pre/post钩子通过query.transform()在结果中把deletedCount 0回写到self.$isDeleted(true)文档注释还特别提醒由于deleteOne()返回 Query除非使用await、.then()、.catch()或.exec()否则查询不会执行——product.deleteOne();什么都不做product.deleteOne().exec();才会真正删除并返回 Promise。迁移注意点返回值语义变化await doc.deleteOne()的返回值从doc变成DeleteResult{ acknowledged, deletedCount }。如果旧代码依赖返回值等于原文档需要改为先保存对doc的引用。执行时机变化由于返回的是 Query若没有await/.exec()删除不会发生。旧的doc.deleteOne()未 await 也能触发删除新版本不会。若需要链式调用.session()、.orFail()等 Query 方法现在可以直接在返回的 Query 上链式调用。3. MongoDB Node 驱动升级到 v6Mongoose 8 使用 MongoDB Node 驱动的 v6.x 版本。驱动 v6 有两个直接影响 Mongoose 的变更3.1ObjectId构造函数不再接受 12 字符字符串在 Mongoose 7 中new mongoose.Types.ObjectId(12charstring)是合法写法在 Mongoose 8 中这一调用会抛出错误。// Mongoose 7合法 // Mongoose 8抛出错误 new mongoose.Types.ObjectId(12charstring);这符合 MongoDB ObjectId 的规范合法的 ObjectId 是 24 位十六进制字符串。需要传入 12 字符字符串的场景应改用Buffer或先转换为合法的 24 位十六进制形式。3.2 已废弃的 SSL 选项被移除以下 SSL 选项在驱动 v6 中全部移除需按映射关系替换为 TLS 选项已移除选项替换选项sslCAtlsCAFilesslCRLtlsCRLFilesslCerttlsCertificateKeyFilesslKeytlsCertificateKeyFilesslPasstlsCertificateKeyFilePasswordsslValidatetlsAllowInvalidCertificatestlsCertificateFiletlsCertificateKeyFile升级建议在升级到 Mongoose 8 之前先在连接字符串或连接选项中全局搜索sslCA、sslValidate等键全部替换为对应的tls*选项。另外注意驱动 v6 对 Node.js 运行时的最低版本要求也同步提高请同时确认运行环境的 Node 版本满足驱动 v6 的要求。4. 移除findOneAndRemove()与findByIdAndRemove()在 Mongoose 7 中findOneAndRemove()是findOneAndDelete()的向后兼容别名Mongoose 8 不再支持该别名请改用findOneAndDelete()。同理findByIdAndRemove()findByIdAndDelete()的别名也被移除请改用findByIdAndDelete()。源码印证当前仓库 lib/query.js 中明确存在Query.prototype.findOneAndRemove undefined;即该方法被显式置为undefined以移除。全局搜索findOneAndRemove也找不到任何可用的实现仅保留这条禁用标记。迁移方式全局搜索并替换findOneAndRemove(→findOneAndDelete(findByIdAndRemove(→findByIdAndDelete(两者的语义完全一致都是查找并删除第一个匹配文档返回被删除的文档或null替换后行为不变。5. 移除count()Model.count()和Query.prototype.count()在 Mongoose 8 中被移除请改用Model.countDocuments()Query.prototype.countDocuments()源码印证当前仓库 lib/query.js 中countDocuments()的文档注释明确提到它“behaves likecount()”并指出count()已不再是可用 API。该注释还说明了countDocuments()与旧count()的关键差异旧count()会把文档交给 MongoDB 服务端直接统计而countDocuments()是基于聚合管道$match$group实现的两者在支持的查询操作符上存在差异——count()支持但countDocuments()不支持的操作符包括$where、$near等无法进入$match阶段的地理/脚本类操作符。迁移方式// 旧写法Mongoose 7 await Character.count({ name: Will Riker }); // 新写法Mongoose 8 await Character.countDocuments({ name: Will Riker });注意事项由于countDocuments()使用聚合管道实现对于包含$near、$where等地理空间或脚本操作符的查询迁移后行为可能与旧count()不同需要先改写过滤条件。6. 移除 id SetterMongoose 7.4 引入了一个idsetter使得doc.id 0.repeat(24)等价于doc._id 0.repeat(24)。在 Mongoose 8 中这个 setter 被移除。影响旧代码中形如doc.id someObjectIdString的赋值不再会同步写入_id。请改为直接赋值doc._id someObjectIdString或在构造文档时通过构造参数传入_id。关联理解id作为虚拟字段的 getter返回_id的字符串形式在 Mongoose 中仍然保留只是 setter 行为被移除。这一点与文档中 虚拟字段virtuals 的语义一致id只读返回_id.toHexString()的虚拟属性不再支持反向写入。7.null对非必填 string enum 有效变更要点在 Mongoose 8 之前即使某个 string 路径不是required只要设置了enum给它赋null也会触发校验错误。Mongoose 8 放宽了这一限制只要没有设置required即使配置了enum也可以把 string 路径设置为null。const schema new Schema({ status: { type: String, enum: [on, off] } }); const Test mongoose.model(Test, schema); // Mongoose 8 中正常工作 // Mongoose 7 中抛出 ValidationError await Test.create({ status: null });迁移注意点该变更仅适用于非必填字段。如果字段设置了required: true赋null依旧会触发校验错误因为required校验会拒绝null/undefined。如果业务上明确要求“enum 字段不允许为 null”需要在 schema 中显式加上required: true或增加自定义 validator 来保持旧行为。这一变更与 TypeScript 类型层面的变更见第 14 节可选字段允许null在语义上是一致的——两者共同表明 Mongoose 8 对“可选字段”的界定更宽松null被视为合法的“未设置”值之一。8.save()更新已有文档时应用 minimize变更要点Mongoose 7 只在保存新文档时应用 minimize删除空对象保存已有文档时不会。Mongoose 8 在两种场景下都会应用 minimize。背景知识minimize是 schema 的默认选项默认为true含义是当保存文档时删除值为空对象{}的属性路径。当前仓库 lib/document.js 中对minimize选项的文档注释为“if true, omit any empty objects from the output”为true时从输出中省略所有空对象。const schema new Schema({ nested: { field1: Number } }); const Test mongoose.model(Test, schema); // Mongoose 7 和 Mongoose 8 在保存新文档时都会默认剥离空对象 const { _id } await Test.create({ nested: {} }); let rawDoc await Test.findById(_id).lean(); rawDoc.nested; // undefined // Mongoose 8 在保存已有文档时也会剥离空对象 const doc await Test.findById(_id); doc.nested {}; doc.markModified(nested); await doc.save(); let rawDoc await Test.findById(_id).lean(); rawDoc.nested; // Mongoose 8 中为 undefinedMongoose 7 中为 {}源码印证在 lib/document.js 中可以看到 minimize 取值的优先级链options._calledWithOptions.minimize→this.$__schemaTypeOptions?.minimize→defaultOptions?.minimize→this.$__schema.options.minimize默认true。实际剥离动作发生在 lib/document.jsif (options.minimize) { ret minimize(ret) || {}; }。迁移注意点数据丢失风险旧代码中“保存已有文档时显式把嵌套对象置空”的习惯性写法doc.nested {}markModified在 8.x 中会把nested字段从 MongoDB 中彻底移除保存后读取为undefined。如果业务上需要保留空对象必须在 schema 选项中显式设置minimize: false。该行为对所有已有文档的save()生效建议在升级后的回归测试中重点覆盖“嵌套对象被置空”的业务场景。9. base schema paths 先于 discriminator paths 应用变更要点在 Mongoose 8 中discriminator 路径上的 getter/setter在 base 路径的 getter/setter 之后执行。Mongoose 7 中的顺序恰好相反discriminator 先执行。const schema new Schema({ name: { type: String, get(v) { console.log(Base schema getter); return v; } } }); const Test mongoose.model(Test, schema); const D Test.discriminator(D, new Schema({ otherProp: { type: String, get(v) { console.log(Discriminator schema getter); return v; } } })); const doc new D({ name: test, otherProp: test }); // Mongoose 8先打印 Base schema getter再打印 Discriminator schema getter // Mongoose 7先打印 Discriminator schema getter再打印 Base schema getter console.log(doc.toObject({ getters: true }));迁移注意点如果 base schema 与 discriminator schema 的 getter/setter 之间存在依赖关系例如 discriminator 的 getter 依赖 base getter 处理后的值Mongoose 8 的新顺序base 先执行通常更符合直觉反之如果旧代码依赖“discriminator 先执行”的顺序需要检查并调整 getter/setter 的实现。相关资源关于 discriminator 的更多用法可参考 discriminator 文档关于 getter/setter 的详细说明可参考 getters-setters 教程。10. 移除findOneAndUpdate()的overwrite选项变更历史Mongoose 7 及更早版本支持findOneAndUpdate()、updateOne()、update()的overwrite选项在 Mongoose 7 之前overwrite会跳过对update参数做$set包装从而使findOneAndUpdate()和update()直接覆盖匹配到的文档Mongoose 7 为了向后兼容把overwrite: true时的findOneAndUpdate()转换为findOneAndReplace()、updateOne()转换为replaceOne()Mongoose 8 完全移除了overwrite选项。迁移方式如果需要整体覆盖整个文档请直接使用findOneAndReplace()或replaceOne()// 旧写法Mongoose 7 及更早 await Character.findOneAndUpdate(filter, replacement, { overwrite: true }); // 新写法Mongoose 8 await Character.findOneAndReplace(filter, replacement);注意事项replaceOne()/findOneAndReplace()与update()的语义不同——替换操作会用整个新文档替换旧文档因此替换文档中必须包含所有需要的字段包括_id的处理策略未包含的字段会被删除而update()默认只做$set局部更新未涉及的字段保持不变。11.findOneAndUpdate()与orFail() upsert 的行为变更变更要点Mongoose 7 中findOneAndUpdate(filter, update, { upsert: true }).orFail()在upsert 插入新文档时也会抛出DocumentNotFoundError——即只要“没找到文档”就抛错即使最终 upsert 创建了新文档Mongoose 8 中findOneAndUpdate(filter, update, { upsert: true }).orFail()总是成功——orFail()改为在“没有文档返回”时抛错而不是“没有找到文档”时抛错。由于 upsert 总会产生一个文档并返回因此不再抛错。语义对比场景Mongoose 7 的orFail()Mongoose 8 的orFail()找到文档并更新成功成功未找到文档且 upsert 插入新文档抛错DocumentNotFoundError成功有文档返回未找到文档且未 upsert抛错抛错无文档返回源码印证orFail()的实现位于 lib/query.js。从代码结构看其抛错逻辑基于“结果是否为空”判断includeResultMetadata模式下检查res.value null普通模式下检查res null见 lib/query.js。由于 upsert 插入新文档后必然有返回文档所以orFail()不会触发。迁移注意点如果旧代码依赖“upsert 时orFail()抛错”这一行为例如用它来判断“本次是插入而非更新”迁移后该判断将失效需要改为检查返回结果中的upserted元数据配合includeResultMetadata: true。该变更同时影响findOneAndUpdate()、findOneAndReplace()、findOneAndDelete()上的orFail()语义——统一为“有文档返回即成功”。12.create()等待所有 save 完成后再抛错变更要点Mongoose 7 中create()在任何一个save()抛错时立即抛出该错误默认行为。Mongoose 8 会等待所有save()调用结束再抛出第一个发生的错误。因此抛出的错误对象在 7 和 8 中相同只是 8 可能耗时更久。const schema new Schema({ name: { type: String, enum: [Badger, Mushroom] } }); schema.pre(save, async function() { await new Promise(resolve setTimeout(resolve, 1000)); }); const Test mongoose.model(Test, schema); const err await Test.create([ { name: Badger }, { name: Mushroom }, { name: Cow } ]).then(() null, err err); err; // ValidationError // Mongoose 7数据库中有 0 条文档因为 Test.create() 在 // Badger 和 Mushroom 插入完成前就抛错 // Mongoose 8数据库中有 2 条文档。Test.create() 会等待 // Badger 和 Mushroom 插入完成后再抛错 await Test.countDocuments();迁移注意点部分写入是 8.x 的正常行为批量创建时合法的文档会被写入数据库不合法的文档会导致整体抛错——这类似于 MongoDB 的 bulkWrite 行为。如果业务要求“全有或全无”需要自己在事务session中执行create()。如果旧代码依赖“第一个文档校验失败时后续文档不被插入”迁移后行为会变化请用事务保证原子性参考 事务文档。13.Model.validate()返回对象副本变更要点Mongoose 7 中Model.validate()可能直接修改传入的对象Mongoose 8 会先复制传入的对象再校验原对象保持不变。const schema new Schema({ answer: Number }); const Test mongoose.model(Test, schema); const obj { answer: 42 }; const res Test.validate(obj); typeof obj.answer; // Mongoose 8 中为 stringMongoose 7 中为 number typeof res.answer; // 两个版本中都是 number源码印证Model.validate的实现位于 lib/model.js其签名支持validate(obj, pathsOrOptions, context)三种调用形态并可结合 discriminator 键自动切换到对应的 discriminator schema 进行校验见 lib/model.js。迁移注意点如果旧代码依赖Model.validate(obj)的“副作用”——即校验完成后obj中的字段已被 cast 为正确类型——迁移后该副作用消失必须改用返回值res。该变更让Model.validate()更安全传入的对象可以安全复用不会被隐式修改。14. TypeScript可选字段允许null变更要点Mongoose 8 中自动推断的 schema 类型允许可选字段为null。Mongoose 7 中可选字段只允许undefined不允许null。const schema new Schema({ name: String }); const TestModel model(Test, schema); const doc new TestModel(); // Mongoose 8 中该类型为 string | null | undefined // Mongoose 7 中该类型为 string | undefined doc.name;迁移注意点类型收窄从string | undefined变为string | null | undefined会让类型检查更严格——代码中对doc.name的判空逻辑需要同时处理null和undefined。旧代码如果只检查! undefined迁移后 TypeScript 编译器会在使用doc.name的地方报错。这与第 7 节“运行时允许 string enum 字段为 null”的变更保持一致两者是同一语义可选即允许null在运行时与类型层的统一体现。15. TypeScriptModel 构造参数全部可选变更要点Mongoose 8 中模型构造函数的参数默认没有任何必填属性。import {Schema, model, Model} from mongoose; interface IDocument { name: string; createdAt: Date; updatedAt: Date; } const documentSchema new SchemaIDocument( { name: { type: String, required: true } }, { timestamps: true } ); const TestModel modelIDocument(Document, documentSchema); // Mongoose 7 中会编译报错Mongoose 8 中可以编译通过 const newDoc new TestModel({ name: Foo }); // 显式传入泛型参数给构造函数以指定构造参数的期望类型。 // 下面这行会让 TS 在 Mongoose 8 中因为缺少 createdAt 和 updatedAt 而报错。 const newDoc2 new TestModelIDocument({ name: Foo });迁移注意点类型收紧手段Mongoose 8 放宽了构造参数的默认约束若需要在构造时强制要求传入某些字段请显式把接口类型传给构造函数泛型参数new TestModelIDocument({...})。该变更背后的理念是schema 的required约束与 TypeScript 接口的必填属性不完全等价required是在运行时校验而 TS 接口约束是编译期约束Mongoose 8 将二者解耦让开发者自己决定构造参数的严格程度。16. 从 schema 推断distinct()返回类型变更要点Mongoose 8 中distinct()的返回类型可以从 schema 定义中正确推断。interface User { name: string; email: string; avatar?: string; } const schema new SchemaUser({ name: { type: String, required: true }, email: { type: String, required: true }, avatar: String }); // Mongoose 8 中可以工作Mongoose 7 中编译报错 const names: string[] await MyModel.distinct(name);迁移注意点distinct(name)现在会基于 schema 中name字段的类型推断出string[]而不是宽松的any[]或错误类型。如果项目中存在将distinct()结果赋给错误类型变量的代码升级后 TypeScript 会提示类型不匹配需要同步修正。17. 升级检查清单与验证方法17.1 升级前代码扫描清单在升级到 Mongoose 8 前对代码库执行以下全局搜索搜索内容处理方式rawResult全部替换为includeResultMetadatafindOneAndRemove(/findByIdAndRemove(替换为findOneAndDelete(/findByIdAndDelete(\.count(Model.count/Query.count替换为.countDocuments(overwrite: true改用findOneAndReplace()/replaceOne()sslCA/sslCRL/sslCert/sslKey/sslPass/sslValidate/tlsCertificateFile按第 3.2 节映射表替换doc.id 赋值改为doc._id Model.validate(的返回值使用改用返回值不要依赖副作用create([...])的批量失败回滚逻辑如需原子性改用事务orFail()与upsert的组合确认新语义是否符合业务预期17.2 行为回归重点已有文档保存时的空对象剥离第 8 节检查所有“将嵌套对象置空后save()”的代码路径确认是否符合预期。discriminator 的 getter/setter 顺序第 9 节重点回归有 base discriminator 双层 getter/setter 的模型。doc.deleteOne()的返回值与执行时机第 2 节检查是否有遗漏await或.exec()的调用。string enum 字段赋null第 7 节确认新增的“允许 null”行为不会放过本应被拦截的脏数据。17.3 验证方法运行现有测试套件仓库使用 Mocha 作为测试运行器可执行npm test全量测试或npm run test:ciCI 精简模式详见 package.json。针对 TypeScript 相关的类型变更第 1416 节仓库通过tstyche执行类型级测试npm run test:types见 package.json可参考 test/types 目录下的类型测试用例编写自己的类型断言。升级后可先在小流量/灰度环境运行重点观察文档写入内容与查询返回值是否符合预期。结语Mongoose 8 的这次大版本升级本质上是围绕API 语义收敛与类型安全增强两个主线展开移除历史遗留别名findOneAndRemove、count、overwrite、统一选项命名rawResult→includeResultMetadata、让行为更加可预期deleteOne返回 Query、create()等待全部完成、validate()不再有副作用并在 TypeScript 类型层面对可选字段、构造参数与推断结果做了系统性收紧。迁移成本主要集中在返回值语义变化与运行时行为变化两处建议结合本文第 17 节的检查清单逐项扫描再用仓库自带的测试框架Mocha tstyche做完整回归即可平稳完成升级。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考