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()现在返回 Query- MongoDB Node 驱动升级到 v6
- 移除
findOneAndRemove()与findByIdAndRemove() - 移除
count() - 移除 id Setter
null对非必填 string enum 有效save()更新已有文档时应用 minimize- base schema paths 先于 discriminator paths 应用
- 移除
findOneAndUpdate()的overwrite选项 findOneAndUpdate()与orFail()+ upsert 的行为变更create()等待所有 save 完成后再抛错Model.validate()返回对象副本- TypeScript:可选字段允许
null - TypeScript:Model 构造参数全部可选
- 从 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自身的 Promise; - Mongoose 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 驱动升级到 v6
Mongoose 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 选项:
| 已移除选项 | 替换选项 |
|---|---|
sslCA | tlsCAFile |
sslCRL | tlsCRLFile |
sslCert | tlsCertificateKeyFile |
sslKey | tlsCertificateKeyFile |
sslPass | tlsCertificateKeyFilePassword |
sslValidate | tlsAllowInvalidCertificates |
tlsCertificateFile | tlsCertificateKeyFile |
升级建议:在升级到 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 Setter
Mongoose 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 中为 undefined,Mongoose 7 中为 {}源码印证:在 lib/document.js 中可以看到 minimize 取值的优先级链:options._calledWithOptions.minimize→this.$__schemaTypeOptions?.minimize→defaultOptions?.minimize→this.$__schema.options.minimize(默认true)。实际剥离动作发生在 lib/document.js:if (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 中为 'string',Mongoose 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. TypeScript:Model 构造参数全部可选
变更要点:Mongoose 8 中,模型构造函数的参数默认没有任何必填属性。
import {Schema, model, Model} from 'mongoose'; interface IDocument { name: string; createdAt: Date; updatedAt: Date; } const documentSchema = new Schema<IDocument>( { name: { type: String, required: true } }, { timestamps: true } ); const TestModel = model<IDocument>('Document', documentSchema); // Mongoose 7 中会编译报错,Mongoose 8 中可以编译通过 const newDoc = new TestModel({ name: 'Foo' }); // 显式传入泛型参数给构造函数,以指定构造参数的期望类型。 // 下面这行会让 TS 在 Mongoose 8 中因为缺少 createdAt 和 updatedAt 而报错。 const newDoc2 = new TestModel<IDocument>({ name: 'Foo' });迁移注意点:
- 类型收紧手段:Mongoose 8 放宽了构造参数的默认约束,若需要在构造时强制要求传入某些字段,请显式把接口类型传给构造函数泛型参数:
new TestModel<IDocument>({...})。 - 该变更背后的理念是: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 Schema<User>({ 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 | 全部替换为includeResultMetadata |
findOneAndRemove(/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:ci(CI 精简模式),详见 package.json。 - 针对 TypeScript 相关的类型变更(第 14~16 节),仓库通过
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),仅供参考