Mongoose Models 完全指南:从 Schema 编译、CRUD 操作到 Change Streams 与视图
Mongoose Models 完全指南从 Schema 编译、CRUD 操作到 Change Streams 与视图【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseModel 是 Mongoose 中最核心的抽象它是由Schema定义编译出来的构造器fancy constructor负责在底层 MongoDB 数据库中创建文档、读取文档、更新与删除数据。本文以 docs/models.md 为主线结合仓库源码lib/mongoose.js、lib/model.js、lib/utils.js等深入讲解如何编译第一个模型、如何构造/查询/删除/更新文档、如何利用 Change Streams 监听数据库变化以及如何为 MongoDB 视图View定义模型。读完本文你将掌握 Mongoose Model 的完整使用链路并能理解其底层的编译、复数化命名、连接绑定与写操作策略。编译你的第一个模型构造文档Document查询文档删除文档更新文档Change Streams监听数据库变更MongoDB 视图View与模型更多模型 API 与下一步编译你的第一个模型当你对某个Schema调用mongoose.model()时Mongoose 就会为你编译出一个模型const schema new mongoose.Schema({ name: String, size: String }); const Tank mongoose.model(Tank, schema);第一个参数是你的模型对应的集合collection的单数形式名称。Mongoose 会自动查找模型名称的复数、小写形式。因此上面的例子中模型Tank对应的是数据库中的tanks集合。从源码看这个自动复数化发生在lib/mongoose.js的Mongoose.prototype.model()中当没有显式传入 collection 名称时会调用utils.toCollectionName(name, _mongoose.pluralize())来推导集合名见 lib/mongoose.js。toCollectionName定义于 lib/utils.js它会先对system.profile、system.indexes这类系统集合名做特殊处理原样返回否则调用 Mongoose 内置的复数化函数pluralize生成集合名若名称不是字符串或为空字符串则会抛出TypeError。关于集合名的三种显式控制方式如果你不想依赖自动复数化Mongoose 提供了三种途径在调用model()时显式传入第三个参数作为 collection 名称const M mongoose.model(Actor, schema, actor);在 Schema 上通过选项或set()指定const schema new Schema({ name: String }, { collection: actor }); // 等价于 schema.set(collection, actor);调整全局mongoose.pluralize()行为或关闭集合名推导。重要注意事项.model()函数会对schema做一份拷贝。请确保在调用.model()之前把想加到schema上的所有内容包括 hooks/中间件都添加完毕。这一行为在源码中有两处体现在lib/mongoose.js的Mongoose.prototype.model()中如果全局设置了cloneSchemas选项会先对 schema 执行schema.clone()再应用全局插件_applyPlugins见 lib/mongoose.js最终由_model()调用Model.compile()后者会在 schema 上补齐版本键__v字段并生成模型类见 lib/model.js。另外模型名是全局缓存键若对同一个名字用不同的 schema再次调用model()会抛出OverwriteModelError用同名同 schema 或仅更换 collection 名称的调用则会复用缓存或派生子类见 lib/mongoose.js。构造文档一个模型的实例被称为文档Document。创建并保存到数据库非常简单const Tank mongoose.model(Tank, yourSchema); const small new Tank({ size: small }); await small.save(); // 或者 await Tank.create({ size: small }); // 或者用于批量插入大量文档 await Tank.insertMany([{ size: small }]);三种方式的适用场景与底层行为有所区别new Model(doc)await doc.save()先构造文档实例再手动保存会触发save中间件、validate校验与版本键逻辑适合需要精细控制保存时机的场景。Model.create(doc)一步完成构造与保存。从源码看Model.create()对单个文档会先new Model(toSave)再调用toSave.$save(options)支持一次传入多个文档按参数展开或数组多个文档时默认并行保存也可以传{ session }参与事务但同一 session 下批量创建多个文档必须设置ordered: true否则会抛出错误。Model.insertMany(docs)批量插入的快车道。源码注释明确指出它比.create()更快因为只向服务器发送一次写操作而不是每个文档一次它不会触发save中间件但会触发insertMany中间件且 Mongoose 会在发送前对每个文档做校验除非设置ordered: false让部分失败不中断整体。注意在你模型所使用的连接打开之前不会真正创建/删除任何 tank 文档。每个模型都关联一个连接使用mongoose.model()时模型使用默认的 mongoose 连接await mongoose.connect(mongodb://127.0.0.1/gettingstarted);如果你创建了自定义连接请改用该连接的model()函数使模型绑定到对应连接const connection mongoose.createConnection(mongodb://127.0.0.1:27017/test); const Tank connection.model(Tank, yourSchema);这一每模型一连接的设计在源码Connection.prototype.model()lib/connection.js与Mongoose.prototype.model()中保持一致两者都最终调用Model.compile()并注册到对应连接/全局的models缓存中。查询文档Mongoose 查询非常容易它完整支持 MongoDB 丰富的查询语法。文档可通过模型上的静态方法获取包括find、findById、findOne、whereawait Tank.find({ size: small }).where(createdDate).gt(oneYearAgo).exec();Mongoose 的查询是**可链式chainable**的find()等静态方法返回的是一个Query实例你可以继续调用.where()、.gt()、.lt()、.select()、.sort()、.limit()等构建查询条件最后通过.exec()或await真正执行。关于QueryAPI 的完整用法请参考 查询章节 与 API 文档。查询相关的测试覆盖非常充分例如 test/model.querying.test.js 验证了各类查询条件与投影行为test/model.query.casting.test.js 则验证了查询条件中的类型转换。删除文档模型提供静态方法deleteOne()和deleteMany()用于删除所有匹配给定filter的文档await Tank.deleteOne({ size: large });deleteOne()删除最多一条匹配的文档deleteMany()删除所有匹配的文档。两个方法都定义于 lib/model.js内部会构造deleteOne/deleteMany类型的 Query经过 cast 后下发到 MongoDB。返回结果中可通过deletedCount查看实际删除的条数。相关测试见 test/model.deleteOne.test.js如有与 test/model.test.js。更新文档每个模型都有自己的更新方法用于在不把文档返回给应用的情况下直接修改数据库中的文档// 最多更新一个文档res.nModified 表示 MongoDB 实际修改的文档数 await Tank.updateOne({ size: large }, { name: T-90 });对应的方法包括updateOne(filter, update)更新匹配的第一条文档updateMany(filter, update)更新所有匹配的文档replaceOne(filter, doc)整篇替换匹配的第一条文档。如果你想更新数据库中的单个文档并把它返回给应用请改用findOneAndUpdate或findByIdAndUpdate。相关测试见 test/model.findOneAndUpdate.test.js、test/model.updateOne.test.js。在底层更新操作会经过 lib/helpers/query/castUpdate.js 对更新操作符$set、$inc、$push等做类型转换并可能触发timestamps、版本键__v自动维护等逻辑。Change Streams监听数据库变更Change Streams 提供了一种监听所有插入与更新流经你 MongoDB 数据库的方式。注意除非你连接的是 MongoDB 副本集replica set否则 Change Streams 无法工作。async function run() { // 创建一个新的 mongoose 模型 const personSchema new mongoose.Schema({ name: String }); const Person mongoose.model(Person, personSchema); // 创建 change stream。当数据库发生变更时change 事件被触发 Person.watch(). on(change, data console.log(new Date(), data)); // 插入一个文档将触发上面的 change stream 处理器 console.log(new Date(), Inserting doc); await Person.create({ name: Axl Rose }); }上面异步函数的输出如下2018-05-11T15:05:35.467Z Inserting doc 2018-05-11T15:05:35.487Z Inserted doc 2018-05-11T15:05:35.491Z { _id: { _data: ... }, operationType: insert, fullDocument: { _id: 5af5b13fe526027666c6bf83, name: Axl Rose, __v: 0 }, ns: { db: test, coll: Person }, documentKey: { _id: 5af5b13fe526027666c6bf83 } }从源码看Model.watch()的实现要点包括支持传入聚合pipeline对变更事件做过滤/转换并会通过prepareDiscriminatorPipeline为判别器discriminator模型自动补充fullDocument相关的管道阶段内部先等待连接就绪db._waitForConnect()再调用底层集合的watch()最后包装为ChangeStream实例见 lib/cursor/changeStream.js返回返回的ChangeStream是 EventEmitter可监听change事件也支持pipeline传入的聚合阶段。相关测试见 test/model.watch.test.js其中覆盖了插入、更新、删除等操作类型事件的触发。MongoDB 视图与模型MongoDB 视图View本质上是只读的集合其数据由其他集合通过聚合管道计算而来。在 Mongoose 中你应该为每个视图单独定义一个模型。你也可以用createCollection()来创建视图。下面这个例子展示了如何在User模型之上创建一个RedactedUser视图以隐藏 name、email 等敏感信息// 对视图请务必关闭 autoCreate 和 autoIndex // 因为你要手动创建这个集合。 const userSchema new Schema({ name: String, email: String, roles: [String] }, { autoCreate: false, autoIndex: false }); const User mongoose.model(User, userSchema); const RedactedUser mongoose.model(RedactedUser, userSchema); // 首先创建 User 模型底层的集合... await User.createCollection(); // 然后把 RedactedUser 模型底层的集合创建为视图。 await RedactedUser.createCollection({ viewOn: users, // viewOn 要填集合名**不是**模型名。 pipeline: [ { $set: { name: { $concat: [{ $substr: [$name, 0, 3] }, ...] }, email: { $concat: [{ $substr: [$email, 0, 3] }, ...] } } } ] }); await User.create([ { name: John Smith, email: john.smithgmail.com, roles: [user] }, { name: Bill James, email: billacme.co, roles: [user, admin] } ]); // [{ _id: ..., name: Bil..., email: bil..., roles: [user, admin] }] console.log(await RedactedUser.find({ roles: admin }));要点说明关闭autoCreate与autoIndex视图需要手动创建因此两个选项必须设为false否则 Mongoose 的自动行为会干扰视图的建立viewOn填集合名而非模型名这里User模型对应users集合所以viewOn: userspipeline定义视图的计算逻辑示例用$set$concat$substr将 name/email 脱敏为前 3 个字符加省略号先User.createCollection()创建源集合再通过RedactedUser.createCollection({ viewOn, pipeline })创建视图最后插入数据并查询视图可看到输出结果为脱敏后的文档。Model.createCollection()的实现见 lib/model.js相关测试见 test/model.create.test.js 中关于createCollection的用例。注意Mongoose 目前并不会强制视图只读。如果你尝试对视图中的文档执行save()会收到来自 MongoDB 服务器的错误因为视图本身不支持写入。更多模型 API 与下一步除本文介绍的方法外API 文档 还覆盖了大量模型方法例如countDocuments(filter)统计匹配文档数量实现见 lib/model.jsaggregate(pipeline)执行聚合管道实现见 lib/model.jsfindByIdAndUpdate/findOneAndUpdate/findOneAndDelete/findOneAndReplace查找并修改/删除后返回文档startSession()启动 MongoDB 会话以支持事务与因果一致性实现见 lib/model.jspopulate()跨集合引用填充。现在我们已经完整覆盖了Models接下来可以继续学习文档层面的细节见 文档Documents章节。如果你关注模型的中间件与钩子机制可进一步阅读 中间件Middleware章节若关心模型在 TypeScript 下的类型推导可参考 TypeScript 指南。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考