Mongoose Models 完全指南:从 Schema 编译、CRUD 操作到 Change Streams 与视图
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
Model 是 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、where:
await 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.smith@gmail.com', roles: ['user'] }, { name: 'Bill James', email: 'bill@acme.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: 'users';pipeline定义视图的计算逻辑:示例用$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.js);aggregate(pipeline):执行聚合管道(实现见 lib/model.js);findByIdAndUpdate/findOneAndUpdate/findOneAndDelete/findOneAndReplace:查找并修改/删除后返回文档;startSession():启动 MongoDB 会话以支持事务与因果一致性(实现见 lib/model.js);populate():跨集合引用填充。
现在我们已经完整覆盖了Models,接下来可以继续学习文档层面的细节:见 文档(Documents)章节。如果你关注模型的中间件与钩子机制,可进一步阅读 中间件(Middleware)章节;若关心模型在 TypeScript 下的类型推导,可参考 TypeScript 指南。
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考