
Mongoose 5.x 升级 6.x 迁移指南破坏性变更全解析与源码级应对方案【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose本文以 Mongoose 官方迁移文档为主体系统梳理从 Mongoose 5.x 升级到 6.x 过程中所有破坏性变更breaking changes包括 MongoDB Driver 4.0 升级、连接与查询 API 的 Promise 化、strictQuery/sanitizeFilter等新行为以及 TypeScript 类型变化。文中每个变更点均结合当前仓库源码lib/mongoose.js、lib/query.js、lib/schema.js等给出实现依据与可复制的迁移代码帮助你在升级前完成影响面评估、升级中精准改码、升级后规避隐蔽行为差异。前置提示Mongoose 5 已于 2024 年 3 月 1 日停止支持官方版本支持策略详见 docs/version-support.md。如果你仍在使用 Mongoose 4.x请先阅读 docs/migrating_to_5.md 升级到 5.x再按本文升级到 6.x。一、升级前评估版本要求与整体路线1.1 运行环境要求Node.jsMongoose 6 要求Node.js 12.0.0。MongoDB Server仍支持 MongoDB Server 3.0.0 及以上版本。升级前请先确认生产环境的 Node 运行时满足要求避免升级依赖后出现语法或 API 兼容问题。1.2 底层驱动升级MongoDB Driver 4.0Mongoose 6 底层改用 MongoDB Node.js Driver v4.x这是本次升级中影响面最大的底层变更。值得关注的变化包括TypeScript 类型定义冲突Driver 4.x 由 TypeScript 编写并自带类型定义可能与types/mongodb冲突。若出现编译器报错请确保升级到最新的types/mongodb新版本为空 stub。连接池选项更名poolSize被拆分为minPoolSize与maxPoolSize。Mongoose 5.x 的poolSize等价于 6.x 的maxPoolSize且maxPoolSize默认值提高到了100。在 lib/mongoose.js 的connect()/createConnection()文档注释中可以看到maxPoolSize默认 100、minPoolSize默认 0 的约定MongoDB 同一时刻每个 socket 只允许一个操作若存在慢查询阻塞快查询的场景可适当调大maxPoolSize。updateOne()/updateMany()返回值变化const res await TestModel.updateMany({}, { someProperty: someValue }); res.matchedCount; // 匹配过滤条件的文档数替代 res.n res.modifiedCount; // 实际被修改的文档数替代 res.nModified res.upsertedCount; // upsert 的文档数替代 res.upserteddeleteOne()/deleteMany()返回值不再有n属性const res await TestModel.deleteMany({}); // Mongoose 6{ acknowledged: true, deletedCount: 2 } // Mongoose 5{ n: 2, ok: 1, deletedCount: 2 } res; res.deletedCount; // 被删除的文档数替代 res.n凡是依赖res.n、res.nModified、res.upserted的存量代码升级后必须改为访问matchedCount、modifiedCount、upsertedCount、deletedCount。二、连接层变更清理废弃选项与 Promise 化2.1 移除四个 Deprecation Warning 选项useNewUrlParser、useUnifiedTopology、useFindAndModify、useCreateIndex四个选项在 Mongoose 6 中不再受支持。Mongoose 6 的行为等价于useNewUrlParser、useUnifiedTopology、useCreateIndex恒为trueuseFindAndModify恒为false请直接删除代码中的这些选项// 不再需要 mongoose.set(useFindAndModify, false); await mongoose.connect(mongodb://127.0.0.1:27017/test, { useNewUrlParser: true, // -- 不再需要 useUnifiedTopology: true // -- 不再需要 });2.2 Connection 不再 thenable改用asPromise()Mongoose 6 中 connection 不再是 thenableawait mongoose.createConnection(uri)不再等待连接建立。从 lib/mongoose.js 的源码可以看到createConnection()同步创建 Connection 实例并立即返回连接通过conn.openUri(uri, { ...options, _fireAndForget: true })异步进行。若需要等待连接完成必须显式调用asPromise()// 以下写法在 Mongoose 6 中不再生效 await mongoose.createConnection(uri); // 请改为 await mongoose.createConnection(uri).asPromise();2.3mongoose.connect()始终返回 Promisemongoose.connect()现在总是返回 Promiseresolve 为 Mongoose 实例而不再是 Mongoose 实例本身。源码中Mongoose.prototype.connect是async function且传入回调函数会直接抛出MongooseError(Mongoose.prototype.connect() no longer accepts a callback)见 lib/mongoose.js。连接失败时await会抛出MongooseServerSelectionError。同样地mongoose.disconnect()也不再接受回调。三、查询行为变更3.1 禁止重复执行同一查询对象Mongoose 6 不再允许对同一个 query 对象执行两次否则抛出Query was already executed错误。该错误由 lib/query.js 中的throw new MongooseError(Query was already executed: str)产生。重复执行通常意味着回调与 Promise 混用如需再次执行请使用Query#clone()// 会报 Query was already executed 错误因为这次 find() 实际执行了两次 await Model.find({}, function(err, result) {}); const q Model.find(); await q; await q.clone(); // 用 clone() 克隆查询后可再次执行3.2Model.exists()返回 lean 文档而非布尔值Mongoose 6 中Model.exists()不再返回布尔值而是返回{ _id: ObjectId(...) }或null// Mongoose 5.x 中 existingUser 是布尔值 // 现在 existingUser 为 { _id: ObjectId(...) } 或 null const existingUser await User.exists({ name: John }); if (existingUser) { console.log(existingUser._id); }3.3strictQuery默认跟随strictMongoose 6.0.10 起恢复了strictQuery选项并将其默认值设为与strict一致默认true。这意味着默认情况下Mongoose 会过滤掉查询过滤条件中不在 schema 里的属性。注意Mongoose 7 起该默认值改回false。如果你希望保留 Mongoose 5 以及 7 的默认行为可以全局关闭mongoose.set(strictQuery, false);strictQuery支持true、false、throw三种取值。在测试套件中设为throw很有用——任何查询引用 schema 中不存在的字段都会抛异常有助于暴露测试或代码中的 bug当前仓库中该选项的默认值定义在 lib/mongoose.js 的set()文档注释里为false。strictQuery的实际效果示例const userSchema new Schema({ name: String }); const User mongoose.model(User, userSchema); // 默认等价于 User.find()因为 Mongoose 会过滤掉 notInSchema await User.find({ notInSchema: 1 }); // 设置 strictQuery: false 允许按 schema 之外的属性过滤 await User.find({ notInSchema: 1 }, null, { strictQuery: false }); // 等价写法 await User.find({ notInSchema: 1 }).setOptions({ strictQuery: false });3.4 查询过滤器安全加固sanitizeFilter与trusted()Mongoose 6 引入sanitizeFilter选项可作用于全局或单个查询用于防御query selector 注入攻击。开启后Mongoose 会把过滤条件中任何包含$开头的键的对象用$eq包裹。从 lib/helpers/query/sanitizeFilter.js 的实现可以看到其具体策略递归处理$and/$or/$nor内部的对象对含$键的对象且不是单独的$eq包装为{ $eq: 原对象 }对$jsonSchema、$where、$expr、$text直接抛出MongooseError明确禁止与sanitizeFilter同用打上trustedSymbol标记的对象会被跳过见 lib/helpers/query/trusted.js通过Symbol(mongoose#trustedSymbol)实现。// Mongoose 会把该过滤器转换为 { username: val, pwd: { $eq: { $ne: null } } } // 从而阻止 query selector 注入。 await Test.find({ username: val, pwd: { $ne: null } }).setOptions({ sanitizeFilter: true });如需显式放行某个 query selector使用mongoose.trusted()// mongoose.trusted() 允许 query selector 原样通过 await Test.find({ username: val, pwd: mongoose.trusted({ $ne: null }) }).setOptions({ sanitizeFilter: true });mongoose.sanitizeFilter()与mongoose.trusted()均暴露在 Mongoose 实例上见 lib/mongoose.js 与 lib/mongoose.js。3.5 移除omitUndefinedundefined键默认被剔除Mongoose 5.x 中在更新操作里把键设为undefined等价于设为null5.x 还提供了omitUndefined选项来剥离undefined键。Mongoose 6 中omitUndefined选项已被移除Mongoose 始终剥离undefined键不再置为null。// Mongoose 6 中等价于 findOneAndUpdate({}, {}, { new: true }) // 因为 Mongoose 会移除 name: undefined const res await Test.findOneAndUpdate({}, { $set: { name: undefined } }, { new: true });唯一的工作区是显式把属性设为nullconst res await Test.findOneAndUpdate({}, { $set: { name: null } }, { new: true });3.6Model.exists()之外create()空数组返回空数组await Model.create([])在 v6.0 中传入空数组时返回空数组v5.0 中返回undefined。如果代码中有对返回值是否为undefined的判断需要改为只要传入的是数组await Model.create(...)就始终返回数组。3.7 副本集场景的disconnected事件连接副本集时Mongoose 6 在失去与主节点的连接时就会触发disconnected事件而 Mongoose 5 只有在失去与副本集所有成员的连接时才触发。但需要注意Mongoose 6不会在连接处于 disconnected 状态时缓冲命令因此即使 Mongoose connection 处于 disconnected 状态仍可正常执行readPreference secondary之类的查询。四、Schema 与数据建模变更4.1 Discriminator Schema 默认克隆Mongoose 6 默认克隆 discriminator schema。如果使用递归嵌入式 discriminator需要显式传入{ clone: false }// 在 Mongoose 6 中以下两者等价 User.discriminator(author, authorSchema); User.discriminator(author, authorSchema.clone()); // 如果 clone() 引发问题可传 clone: false 退出该行为 User.discriminator(author, authorSchema, { clone: false });4.2isValidObjectId()简化与新的isObjectIdOrHexString()Mongoose 5 中mongoose.isValidObjectId()对数字等值返回false与 MongoDB 驱动的ObjectId.isValid()行为不一致。Mongoose 6 中isValidObjectId()只是mongoose.Types.ObjectId.isValid()的包装见 lib/mongoose.js而任何 JavaScript 数字技术上都能转换为 ObjectId因此返回值会出乎意料。Mongoose 6.2.5 新增mongoose.isObjectIdOrHexString()它更贴合isValidObjectId()的常见使用场景判断给定值是否为ObjectId实例或 24 位十六进制字符串。其实现见 lib/mongoose.jsisBsonType(v, ObjectId) || (typeof v string /^[0-9A-Fa-f]{24}$/.test(v))。// isValidObjectId() 会对一些意外的值返回 true // 因为这些值_技术上_是 ObjectId 的表现形式 mongoose.isValidObjectId(new mongoose.Types.ObjectId()); // true mongoose.isValidObjectId(0123456789ab); // true mongoose.isValidObjectId(6); // true mongoose.isValidObjectId(new User({ name: test })); // true // isObjectIdOrHexString() 只对 ObjectId 实例和 24 位十六进制字符串返回 true mongoose.isObjectIdOrHexString(new mongoose.Types.ObjectId()); // true mongoose.isObjectIdOrHexString(62261a65d66c6be0a63c051f); // true mongoose.isObjectIdOrHexString(0123456789ab); // false mongoose.isObjectIdOrHexString(6); // false4.3 Schema 定义的文档键顺序Mongoose 6 按schema 中定义键的顺序保存文档而不是按用户传入对象的键顺序。Object.keys(new User({ name: String, email: String }).toObject())返回[name, email]还是[email, name]取决于 schema 中name与email的定义顺序const schema new Schema({ profile: { name: { first: String, last: String } } }); const Test db.model(Test, schema); const doc new Test({ profile: { name: { last: Musashi, first: Miyamoto } } }); // 注意 first 排在 last 之前尽管 new Test() 的参数颠倒了键序。 // Mongoose 使用 schema 的键顺序而不是传入对象的键顺序。 assert.deepEqual(Object.keys(doc.toObject().profile.name), [first, last]);4.4default函数接收文档参数Mongoose 6 会把文档作为第一个参数传给default函数这对箭头函数形式的默认值非常有用const schema new Schema({ name: String, age: Number, canVote: { type: Boolean, // default 函数现在接收 doc 参数便于使用箭头函数 default: doc doc.age 18 } });注意如果传入的默认函数并不需要文档参数比如default: mongoose.Types.ObjectId请改为default: () myFunction()避免意外传入参数改变行为。4.5 数组是 Proxy不再需要markModified()Mongoose 6 的数组是 ES6 Proxy直接按索引赋值后无需再调用markModified()const post await BlogPost.findOne(); post.tags[0] javascript; await post.save(); // 直接生效无需 markModified()4.6typePojoToMixed移除对象字面量成为子文档type: { name: String }声明的 schema 路径在 Mongoose 6 中变成单嵌套子文档而不是 Mongoose 5 中的 Mixed因此不再需要typePojoToMixed选项// Mongoose 6 中下面使 foo 成为带 name 属性的子文档。 // Mongoose 5 中除非设置 typePojoToMixed: false否则 foo 是 Mixed 类型。 const schema new Schema({ foo: { type: { name: String } } });4.7 保留字路径警告使用save、isNew等 Mongoose 保留字作为 schema 路径名现在触发警告而非错误。仓库 lib/schema.js 中维护了保留字列表save、isNew、validate、remove、collection、init等当路径名命中保留字且未设置suppressReservedKeysWarning时会打印警告见 lib/schema.js。可通过 schema 选项抑制警告new Schema({ save: String }, { suppressReservedKeysWarning: true });但请留意这可能导致依赖这些保留字的插件失效。4.8 子文档路径重命名单嵌套子文档被重命名为 subdocument paths子文档路径SchemaSingleNestedOptions→SchemaSubdocumentOptionsmongoose.Schema.Types.Embedded→mongoose.Schema.Types.Subdocument对应地仓库中 schema 类型目录里也保留了subdocument.js如 lib/schema/subdocument.js与类型定义 types/subdocument.d.ts。4.9 移除嵌套路径合并doc.set({ child: { age: 21 } })现在无论child是嵌套路径还是子文档行为一致覆盖child的值。Mongoose 5 中若child是嵌套路径该操作会进行合并。4.10 ObjectId 新增valueOf()Mongoose 6 给 ObjectId 增加了valueOf()函数因此可以用把 ObjectId 与字符串直接比较const a ObjectId(6143b55ac9a762738b15d4f0); a 6143b55ac9a762738b15d4f0; // true4.11createdAt默认不可变设置timestamps: true后Mongoose 6 会把createdAt属性设为immutable防止更新操作误改创建时间。仓库中时间戳实现位于 lib/helpers/timestamps/setupTimestamps.js。若业务确实需要修改createdAt可通过timestamps.createdAt.immutable选项调整mongoose.set(timestamps.createdAt.immutable, false)对应说明见 lib/mongoose.js。4.11 移除isAsync验证器与safe选项isAsync不再是validate的选项请改用async function编写异步验证器。safe不再是 schema、query 或save()的选项请改用writeConcern。4.12 SchemaTypeset函数参数变化Mongoose 6 中 setter 函数的第二个参数从schemaTypeMongoose 5变为priorValue前一个值第三个参数才是schemaTypeconst userSchema new Schema({ name: { type: String, trimStart: true, set: trimStartSetter } }); // v5.x 参数为 (value, schemaType)v6.x 参数为 (value, priorValue, schemaType) function trimStartSetter(val, priorValue, schemaType) { if (schemaType.options.trimStart typeof val string) { return val.trimStart(); } return val; } const User mongoose.model(User, userSchema); const user new User({ name: Robert Martin }); console.log(user.name); // robert martin4.13toObject()/toJSON()使用嵌套 schema 的minimizetoObject()与toJSON()默认使用各层嵌套 schema 自身的minimize选项而不是顶层 schema 的。该变更随 5.10.5 发布但从 5.9.x 直接升 6.x 的用户容易踩坑const child new Schema({ thing: Schema.Types.Mixed }); const parent new Schema({ child }, { minimize: false }); const Parent model(Parent, parent); const p new Parent({ child: { thing: {} } }); // v5.10.4因 toObject() 使用 parent schema 的 minimize 选项会包含 child.thing // 5.10.5child schema 的 minimize: truechild.thing 被省略 console.log(p.toObject());两种工作区// 方案一显式传 minimize 给 toObject() 或 toJSON() console.log(p.toObject({ minimize: false }));// 方案二仅 Mongoose 6内联定义 child schema继承父级 minimize 选项 const parent new Schema({ // 隐式创建新 schema并继承顶层 schema 的 minimize 选项 child: { type: { thing: Schema.Types.Mixed } } }, { minimize: false });五、Populate 相关变更5.1strictPopulate()populate 未定义路径报错Mongoose 6 在populate()一个 schema 中未定义的路径时会抛出错误。该行为仅适用于能推断本地 schema 的场景如Query#populate()不适用于对 POJO 调用Model.populate()。5.2 子文档ref函数上下文populate 子文档时若ref或refPath是函数this/参数指向正在被 populate 的子文档而不是顶层文档const schema new Schema({ works: [{ modelId: String, data: { type: mongoose.ObjectId, ref: function(doc) { // Mongoose 6 中 doc 是数组元素因此可以访问 modelId。 // Mongoose 5 中 doc 是顶层文档。 return doc.modelId; } } }] });5.3 自定义验证器使用反 populate 路径Mongoose 6 始终以反 populate 后的路径即 id 而非文档本身调用验证器。Mongoose 5 在路径被 populate 时会把填充后的文档传给验证器。5.4 移除execPopulate()Document#populate()现在直接返回 Promise且不再可链式调用将await doc.populate(path1).populate(path2).execPopulate();替换为await doc.populate([path1, path2]);将await doc.populate(path1, select1).populate(path2, select2).execPopulate();替换为await doc.populate([{path: path1, select: select1}, {path: path2, select: select2}]);5.5Query.prototype.populate()不再有默认 modelMongoose 5 中对 Mixed 类型等没有ref的路径调用populate()会回退使用查询所属的 modelconst testSchema new mongoose.Schema({ data: String, parents: Array // Array of mixed }); const Test mongoose.model(Test, testSchema); // 下面的 populate()... await Test.findOne().populate(parents); // 在 Mongoose 5 中是如下 populate 的简写 await Test.findOne().populate({ path: parents, model: Test });Mongoose 6 中populate 一个没有ref、refPath或model的路径是no-op无操作// 下面的 populate() 什么也不做。 await Test.findOne().populate(parents);六、聚合与索引行为变更6.1 创建聚合游标Aggregate#cursor()现在直接返回AggregationCursor实例与Query#cursor()保持一致。不再需要Model.aggregate(pipeline).cursor().exec()直接Model.aggregate(pipeline).cursor()即可。6.2autoCreate默认trueautoCreate默认开启除非readPreference为secondary或secondaryPreferredMongoose 会在创建索引前尝试为每个 model 创建底层集合。从源码看全局默认选项autoIndex: true、autoCreate: true定义在 lib/mongoose.js。当readPreference为 secondary 或 secondaryPreferred 时autoCreate与autoIndex都会默认变为false因为连接从节点时createCollection()与createIndex()都会失败。6.3 移除context: query选项query 的context选项已被移除Mongoose 现在始终使用context query。七、其他重要行为变化7.1MongoError更名为MongoServerErrorMongoDB Node.js Driver v4.x 中MongoError已更名为MongoServerError。请修改所有依赖硬编码字符串MongoError的代码。7.2 Driver 新 URL 解析器与部分 npm 包的兼容性问题Mongoose 6 依赖的 MongoDB Driver 使用 whatwg-url。7.3 移除reconnectTries与reconnectInterval这两个选项已被移除且不再必要。MongoDB Node.js Driver 会在serverSelectionTimeoutMS默认 30000ms即 30 秒见 lib/mongoose.js内持续重试任何操作即使 MongoDB 长时间宕机也不会耗尽重试次数或主动断线重连。7.4 LodashisEmpty()对 ObjectId 返回trueLodash 的isEmpty()对原始类型及原始类型包装对象返回true。ObjectId()是被 Mongoose 视为原始类型的对象包装器但从 Mongoose 6 起由于 Lodash 的实现细节_.isEmpty()会对 ObjectId 返回true。Mongoose 中的 ObjectId 永远不会为空因此使用isEmpty()时应额外检查instanceof ObjectIdif (!(val instanceof Types.ObjectId) _.isEmpty(val)) { // 在此处理空对象 }7.5 移除mongoose.modelSchemasmongoose.modelSchemas属性已被移除它曾被用来删除某个 model schema请改用mongoose.deleteModel()// 之前 delete mongoose.modelSchemas.User; // Mongoose 6.x delete mongoose.deleteModel(User);deleteModel()的实现见 lib/mongoose.js同时支持字符串与 RegExp例如mongoose.deleteModel(/./)删除全部 model在测试清理场景非常实用。八、TypeScript 类型变化8.1Schema泛型参数从 4 个变为 3 个Schema类现在接收 3 个泛型参数原来是 4 个。第 3 个泛型参数SchemaDefinitionType现在与第 1 个泛型参数DocType相同。请把new SchemaUserDocument, UserModel, User(schemaDefinition)替换为new SchemaUserDocument, UserModel(schemaDefinition)。8.2Types.ObjectId是类必须使用newTypes.ObjectId现在是一个类创建新 ObjectId 时不能省略new。JavaScript 中目前仍可省略new但TypeScript 中必须写newconst id new mongoose.Types.ObjectId(); // TypeScript 必须带 new8.3 移除的遗留类型以下遗留类型已被移除ModelUpdateOptionsDocumentQueryHookSyncCallbackHookAsyncCallbackHookErrorCallbackHookNextFunctionHookDoneFunctionSchemaTypeOptsConnectionOptions8.4 virtual getter/setter 中this类型推断Mongoose 6 会在 virtual getter 和 setter 中推断文档类型。Mongoose 5.x 中下面代码里的this是anyschema.virtual(myVirtual).get(function() { this; // Mongoose 5.x 中为 any });Mongoose 6 中this会被推断为文档类型const schema new Schema({ name: String }); schema.virtual(myVirtual).get(function() { this.name; // string });九、迁移检查清单升级到 Mongoose 6 后建议逐项核对以下要点环境Node.js ≥ 12MongoDB Server ≥ 3.0。连接代码删除useNewUrlParser/useUnifiedTopology/useFindAndModify/useCreateIndexcreateConnection()后如需等待连接用.asPromise()connect()不再接受回调。返回值update*与delete*改用matchedCount/modifiedCount/upsertedCount/deletedCountModel.exists()按 lean 文档处理Model.create([])返回空数组。查询不要重复执行同一 query需要复用用clone()按需设置strictQuery测试环境可用throw有不可信输入时开启sanitizeFilter。Schemadefault函数会收到 doc 参数数组索引赋值免markModified()createdAt默认 immutable对象字面量类型路径变为子文档。Populate移除execPopulate()用数组形式的populate()无ref的 populate 是 no-op未定义路径的 populate 会抛错。TypeScriptSchema泛型减为 3 个new mongoose.Types.ObjectId()必须带new删除已废弃的类型引用。行为细节文档键顺序按 schema 定义排列toObject()/toJSON()的minimize逐层生效ObjectId 可用与字符串比较LodashisEmpty()对 ObjectId 返回true用deleteModel()替代modelSchemas删除。如需进一步查阅本仓库中的相关实现可关注lib/mongoose.js连接与全局选项、lib/query.js查询执行与Query was already executed错误、lib/schema.js保留字与 schema 行为、lib/helpers/query/sanitizeFilter.js 与 lib/helpers/query/trusted.js过滤器净化、lib/helpers/timestamps/setupTimestamps.js时间戳与 immutablecreatedAt。完整的变更历史可参考仓库根目录的 CHANGELOG.md若准备继续升级到更高版本可阅读 docs/migrating_to_7.md 与 docs/migrating_to_8.md、docs/migrating_to_9.md 等后续迁移文档。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考