Mongoose 文档(Documents)完全指南:从挂载文档到变更追踪、校验与中间件

发布时间:2026/9/11 4:51:58
Mongoose 文档(Documents)完全指南:从挂载文档到变更追踪、校验与中间件 Mongoose 文档Documents完全指南从挂载文档到变更追踪、校验与中间件【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseMongoose 的文档Document是构建在 MongoDB 之上的核心数据单元每个文档都是 MongooseDocument类的实例内置变更追踪change tracking、类型转换casting、校验validation、中间件middleware与持久化能力。本文围绕 docs/documents.md 的技术脉络结合本仓库lib/document.js、lib/model.js与lib/query.js的源码实现系统讲解文档的创建、加载、更新、嵌套路径操作、类型转换与校验规则以及文档中间件的生命周期读完你既能熟练使用save()、validate()、lean()等 API也能理解它们背后的实现机制。什么是 DocumentDocument 与 Model 的关系Mongoose 文档是 MongooseDocument类的实例由 MongoDB 数据支撑。在 Mongoose 中Model与Document是两个独立的类Model继承自Document原型链为Model.prototype→Document.prototype见 lib/model.js 中的Object.setPrototypeOf(Model.prototype, Document.prototype)。// User 是一个 Model 类 const User mongoose.model(User, new Schema({ name: String })); // doc 是一个文档 const doc new User({ name: John Smith }); // 类的继承层级User 继承自 Model 继承自 Document doc instanceof User; // true doc instanceof mongoose.Model; // true doc instanceof mongoose.Document; // trueMongoose 文档对应 MongoDB 集合collection中的单个文档。例如由上面User模型创建的文档默认存放在users集合中。你可以通过两种方式获得文档新建new User()在内存中创建文档或await User.create()直接创建并保存加载通过findOne()等查询从 MongoDB 读取已存在的文档。// 在内存中创建新文档 const doc new User({ name: John Smith }); // 持久化到 MongoDB在调用 save() 之前文档不会被写入数据库 await doc.save(); // 加载已存在的文档 const existingDoc await User.findOne({ name: John Smith }); existingDoc instanceof User; // true existingDoc instanceof mongoose.Model; // true existingDoc instanceof mongoose.Document; // true需要注意new User()只是构造一个内存对象真正的落库发生在调用save()之后而User.create()是构造 立即保存的快捷方式。挂载文档Hydrated Document与瘦文档Lean Document当你新建文档或用查询加载文档时Mongoose 返回的是挂载文档hydrated document——它是 MongooseDocument类的实例而不是普通 JavaScript 对象POJO。挂载文档内部维护着一整套状态包括变更追踪change tracking校验validation中间件middleware文档方法document methodssave()持久化getters、setters 与 virtuals正因如此部分 JavaScript 对象操作在文档上表现不同尤其是依赖自有可枚举属性own enumerable properties或赋值语义的操作使用delete操作符不会删除文档属性。例如delete doc.name对 MongoDB 中的文档毫无影响。要移除属性应使用doc.name undefined然后save()Mongoose 不会把值为undefined的属性写入 MongoDB。对文档使用**展开运算符spread**不会得到底层数据的浅拷贝得到的对象只有_doc属性。Object.keys()、Object.values()、Object.entries()枚举的是文档实例本身而不是底层文档数据。想枚举文档属性请先调用doc.toObject()例如Object.keys(doc.toObject())。在文档上使用in操作符对 Schema 中声明的属性永远返回true。例如userDoc是带name属性的 User 模型实例时name in userDoc恒为true。请改用userDoc.name ! undefined判断属性是否存在。空值合并赋值??在设置嵌套路径时可能出人意料(doc.nested ?? {}).name John Smith是无效操作因为(doc.nested ?? {})求值得到的是临时对象后续的.name赋值作用在该临时对象上而非文档路径本身。应改用doc.set(nested.name, John Smith)。用toObject()得到 POJO需要文档的普通对象表示时使用toObject()方法const doc await User.findOne(); // 不推荐 const copy { ...doc }; copy.name; // undefined copy; // { _doc: { name: John Smith }, ... } // 获取文档的普通对象克隆使用 toObject() const obj doc.toObject(); obj.name; // John Smith用lean()跳过挂载lean()让查询返回 POJO 而非挂载文档实现位于 lib/query.js 的Query.prototype.lean。瘦查询通常更快、占用内存更少非常适合只读、不需要文档功能的场景const doc await User.findOne().lean(); doc instanceof User; // false doc.name; // John Smith但请记住lean()会绕过大量 Mongoose 文档特性包括变更追踪校验save()getters默认值defaultsvirtuals含填充的 virtuals如果确实需要这些特性Mongoose 提供了Model.applyDefaults()与Model.applyVirtuals()等辅助方法见 lib/model.js 与 lib/model.js社区也有对 lean 查询结果应用 getters、virtuals 和 defaults 的插件。使用lean()时你有责任显式启用应用所需的任何文档特性。仓库测试 test/docs/lean.test.js 覆盖了lean()与populate()组合等典型场景在 test/document.test.js 等用例中也能看到大量findOne().lean()的用法。使用save()更新文档Mongoose 文档有save()方法将文档当前状态持久化到 MongoDB。Model.prototype.save的实现位于 lib/model.js其内部通过this.$__save(options)完成落库并会先触发校验与中间件流程详见下文中间件一节。对于新文档save()执行插入insertconst doc new User({ name: John Smith }); // 插入一条新文档 await doc.save();对于已存在文档save()发送一条updateOne()只更新发生修改的路径const doc await User.findOne(); doc.name Something else; // 向 MongoDB 发送 { $set: { name: Something else } } 的 updateOne await doc.save();Mongoose 文档具备变更追踪能力当你给文档属性赋值时Mongoose 会将该路径标记为已修改modified。可以通过isModified()判断某路径是否被修改用$getChanges()查看调用save()时将发送给 MongoDB 的变更集doc.name Something else; doc.isModified(name); // true doc.$getChanges(); // { $set: { name: Something else } }$getChanges()的实现位于 lib/document.js其核心是调用私有方法$__delta()见 lib/document.js$__delta()通过this.$__dirty()收集所有脏路径再依据optimisticConcurrency等 Schema 选项组装出最终的 update 语句。换句话说save()只更新修改过的路径不会整体覆盖整个文档。从源码看save()还有几点值得注意同一文档实例并发保存时会抛出ParallelSaveError见 lib/model.js 与 lib/error/parallelSave.js。若文档不是新建!this.$isNew会先生成版本错误$versionError配合optimisticConcurrency选项用于乐观并发控制。save()已不再接受回调函数见 lib/model.js统一使用 Promise/async-await 风格。设置嵌套属性Nested PropertiesMongoose 文档提供set()方法可以安全地设置深层嵌套属性const schema new Schema({ subdoc: new Schema({ subdocLevel2: new Schema({ name: String }, { _id: false }) }, { _id: false }) }); const TestModel mongoose.model(Test, schema); const doc new TestModel(); doc.set(subdoc.subdocLevel2.name, John Smith); doc.subdoc.subdocLevel2.name; // John Smithset()的实现为Document.prototype.$set的别名见 lib/document.js赋值时会依据 Schema 对路径进行转换并按需标记为已修改内部通过$__shouldModify决定是否标记见 lib/document.js当文档非新建、值与旧值深比较相等时不会标记为 modified。文档同样提供get()方法让你安全地读取深层嵌套属性。get()免去了显式判空类似于 JavaScript 的可选链操作符?.const doc2 new TestModel(); doc2.get(subdoc.subdocLevel2.name); // undefined doc2.subdoc?.subdocLevel2?.name; // undefined doc2.set(subdoc.subdocLevel2.name, Will Smith); doc2.get(subdoc.subdocLevel2.name); // Will Smithget()的实现位于 lib/document.js并提供了$get别名lib/document.js。在处理用户输入、不确定路径是否存在的场景下set()/get()比链式访问更健壮。类型转换Casting与校验Validation保存文档之前Mongoose 会依次执行两步将值转换为匹配 Schema 的类型casting对转换后的值进行校验validation。类型转换与校验是两个相关但不同的概念发生在不同阶段。Casting自动类型转换Casting指把值转换为 Schema 配置的类型。Mongoose 会自动处理某些类型转换例如把字符串42转为 number 路径的数字把数字0转为 boolean 路径的falseconst schema new Schema({ age: Number, isEnabled: Boolean }); const Person mongoose.model(Person, schema); const doc new Person(); doc.age 42; doc.isEnabled 0; doc.age; // 42数字类型 doc.isEnabled; // false doc.isEnabled 1; doc.isEnabled; // true各类型的具体转换逻辑分散在 lib/cast 目录下例如 lib/cast/number.js、lib/cast/boolean.js、lib/cast/string.js 等仓库测试 test/cast.number.test.js、test/cast.test.js 对转换边界做了大量验证。如果 Mongoose 无法把值转换为期望类型会产生一个 cast error 并暂存在文档上。最重要的是赋值非法值不会立即抛错而是在你调用validate()或调用内部执行validate()的save()时才报告错误// 不会立即抛错 doc.age not a number; // 抛出 ValidationError其中包含路径 age 的 CastError await doc.validate();这种延迟报告的设计让 Mongoose 能一次性收集多个校验/转换错误。而且如果你多次赋值最后一次赋值生效——覆盖掉非法值后校验便会通过doc.age not a number; doc.age 42; // 校验通过 await doc.validate();Validation校验 Schema 规则Validation是独立的步骤在你调用文档的validate()方法时执行save()内部会调用validate()因此save()也会触发校验Document.prototype.validate的实现见 lib/document.js且不再接受回调参数。校验检查文档是否满足 Schema 规则包括检查 cast errors以及required必填min/max数值上下限enum枚举取值自定义验证器custom validators如果校验失败Mongoose不会保存文档const schema new Schema({ age: { type: Number, min: 0 } }); const Person mongoose.model(Person, schema); const doc new Person({ age: -1 }); // 抛出错误 Path age (-1) is less than minimum allowed value (0) await doc.validate();min/max的默认错误消息由 lib/error/messages.js 定义可全局覆盖。必填属性Required PropertiesMongoose 中最常用的验证器是required。校验或保存时若必填路径缺失Mongoose 抛出验证错误const schema new Schema({ name: { type: String, required: true } }); const User mongoose.model(User, schema); const doc new User(); // 抛出错误 Path name is required await doc.validate();对大多数 Schema 类型而言任何非null、非undefined的值都能通过必填验证。例外是字符串string和缓冲区bufferrequired为真时空字符串和空 buffer 会触发ValidationErrorconst doc new User({ name: }); // 抛出错误 Path name is required await doc.validate();注意空数组不会因required触发ValidationError。中间件Middleware文档中间件让你在文档生命周期的关键节点执行代码最常见的文档中间件钩子hook是validate()和save()。从高层看保存一个文档的流程如下以下示例展示了用pre(validate)钩子设置normalizedName属性const userSchema new Schema({ name: String, normalizedName: String }); userSchema.pre(validate, function() { if (this.name ! null) { this.normalizedName this.name.trim().toLowerCase(); } }); userSchema.pre(save, function() { console.log(Saving user:, this.name); }); const User mongoose.model(User, userSchema); const user new User({ name: JOHN SMITH }); await user.save(); user.normalizedName; // john smith文档中间件适合承载与文档自身强相关的逻辑例如在校验前规范化值normalizing values由一个路径推导另一个路径deriving one path from another强制文档级不变量document-level invariants记录或审计文档保存logging / auditing更新时间戳或元数据timestamps / metadata不过请谨慎使用中间件。中间件通常不适合承载复杂应用逻辑尤其是以下代码应尽量避免放进中间件调用无关服务进行网络请求高度依赖请求上下文执行本应显式的昂贵操作另一个关键点是文档中间件只对文档操作生效。updateOne()、findOneAndUpdate()这类查询更新不会运行save()中间件const doc await User.findOne(); doc.name Jane Doe; // 会运行 validate 与 save 中间件 await doc.save(); // 不会运行 save 中间件 await doc.updateOne({ name: John Doe });从源码看Model.prototype.savelib/model.js经$__save()进入文档保存管线先后经历pre(validate)→ 校验validate()→pre(save)→ 写入 MongoDB →post(save)的流程中间件的注册与调度由 lib/helpers/model/applyHooks.js 完成。库内插件 lib/plugins/saveSubdocs.js 也借助pre(save)钩子实现嵌套子文档的级联保存是理解中间件用法的参考实现。下一步本文基于 docs/documents.md 完整讲解了 Mongoose 文档的核心机制。掌握文档之后下一步建议阅读 docs/subdocs.md子文档 Subdocuments其中详细介绍了嵌套在父文档中的文档数组与单层子文档的声明、修改与校验方式。若想深入文档相关 API 的全部选项可查阅 types/document.d.ts 与 types/models.d.ts 中的 TypeScript 类型定义。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考