乐闻世界logo
搜索文章和话题

面试题手册

Mongoose 如何处理文档关联和 Populate 功能?

Mongoose 提供了多种方式来处理文档之间的关联关系,包括引用(Reference)、嵌入(Embedding)和 Populate 功能。关联类型1. 嵌入式关联(Embedding)将相关数据直接嵌入到父文档中,适合一对一或一对多关系,且子文档较小的情况。const addressSchema = new Schema({ street: String, city: String, country: String});const userSchema = new Schema({ name: String, address: addressSchema // 嵌入式关联});const User = mongoose.model('User', userSchema);2. 引用式关联(Reference)通过 ObjectId 引用其他文档,适合一对多或多对多关系。const authorSchema = new Schema({ name: String, email: String});const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' // 引用式关联 }});const Author = mongoose.model('Author', authorSchema);const Book = mongoose.model('Book', bookSchema);Populate 功能Populate 是 Mongoose 提供的强大功能,可以自动替换引用的 ObjectId 为完整的文档。基本 Populate// 创建作者和书籍const author = await Author.create({ name: 'John Doe', email: 'john@example.com' });const book = await Book.create({ title: 'My Book', author: author._id });// 使用 populate 获取完整作者信息const populatedBook = await Book.findById(book._id).populate('author');console.log(populatedBook.author.name); // "John Doe"多字段 Populateconst bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' }, publisher: { type: Schema.Types.ObjectId, ref: 'Publisher' }});const book = await Book.findById(id) .populate('author') .populate('publisher');嵌套 Populateconst commentSchema = new Schema({ text: String, user: { type: Schema.Types.ObjectId, ref: 'User' }});const postSchema = new Schema({ title: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]});const post = await Post.findById(id) .populate({ path: 'comments', populate: { path: 'user' } });选择字段const book = await Book.findById(id) .populate({ path: 'author', select: 'name email' // 只选择特定字段 });条件 Populateconst books = await Book.find() .populate({ path: 'author', match: { status: 'active' } // 只填充符合条件的作者 });多对多关系使用数组引用const studentSchema = new Schema({ name: String, courses: [{ type: Schema.Types.ObjectId, ref: 'Course' }]});const courseSchema = new Schema({ title: String, students: [{ type: Schema.Types.ObjectId, ref: 'Student' }]});const Student = mongoose.model('Student', studentSchema);const Course = mongoose.model('Course', courseSchema);// 添加课程到学生const student = await Student.findById(studentId);student.courses.push(courseId);await student.save();// 查询学生的所有课程const studentWithCourses = await Student.findById(studentId).populate('courses');使用中间集合const enrollmentSchema = new Schema({ student: { type: Schema.Types.ObjectId, ref: 'Student' }, course: { type: Schema.Types.ObjectId, ref: 'Course' }, enrolledAt: { type: Date, default: Date.now }});const Enrollment = mongoose.model('Enrollment', enrollmentSchema);// 查询学生的所有课程const enrollments = await Enrollment.find({ student: studentId }) .populate('course');虚拟字段 Populate使用虚拟字段创建动态关联:const authorSchema = new Schema({ name: String, books: [{ type: Schema.Types.ObjectId, ref: 'Book' }]});const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' }});// 在 Book Schema 上添加虚拟字段bookSchema.virtual('authorBooks', { ref: 'Book', localField: 'author', foreignField: 'author'});const Book = mongoose.model('Book', bookSchema);// 启用虚拟字段const book = await Book.findById(id).populate('authorBooks');性能优化选择性 Populate:只填充需要的字段限制数量:使用 limit 限制填充的文档数量分页:使用 skip 和 limit 实现分页避免 N+1 查询:合理设计数据结构使用索引:为引用字段创建索引const books = await Book.find() .populate({ path: 'author', select: 'name', options: { limit: 10 } });最佳实践根据数据访问模式选择嵌入或引用避免过度嵌套和过深的 populate考虑使用虚拟字段处理复杂关联为引用字段创建索引以提高查询性能在多对多关系中考虑使用中间集合注意 populate 可能导致的性能问题
阅读 0·2月22日 20:12

Mongoose 性能优化有哪些最佳实践?

Mongoose 性能优化是开发高效应用的关键。通过合理的配置和最佳实践,可以显著提升查询速度和整体性能。连接优化连接池配置mongoose.connect('mongodb://localhost:27017/mydb', { maxPoolSize: 100, // 最大连接数 minPoolSize: 10, // 最小连接数 socketTimeoutMS: 45000, // 套接字超时 serverSelectionTimeoutMS: 5000, // 服务器选择超时 connectTimeoutMS: 10000 // 连接超时});连接重用// 在应用启动时建立连接mongoose.connect('mongodb://localhost:27017/mydb');// 不要频繁关闭和重新连接// 避免在每次请求时都创建新连接索引优化创建索引const userSchema = new Schema({ email: { type: String, index: true, // 单字段索引 unique: true }, name: { type: String, index: true }, age: Number, status: String});// 复合索引userSchema.index({ status: 1, age: -1 });// 文本索引userSchema.index({ name: 'text', bio: 'text' });// 地理空间索引userSchema.index({ location: '2dsphere' });索引策略为常用查询字段创建索引使用复合索引优化多字段查询避免过多索引影响写入性能定期分析查询性能,优化索引// 分析查询计划const query = User.find({ email: 'john@example.com' });const explanation = await query.explain('executionStats');console.log(explanation.executionStats);查询优化使用 lean()// 返回普通 JavaScript 对象,性能更好const users = await User.find().lean();// 只读查询使用 lean()const users = await User.find({ status: 'active' }).lean();选择性查询// 只查询需要的字段const users = await User.find() .select('name email age') .lean();// 排除大字段const users = await User.find() .select('-largeField -anotherLargeField');限制结果数量// 使用 limit 限制返回数量const users = await User.find() .limit(100);// 实现分页const page = 1;const pageSize = 20;const users = await User.find() .skip((page - 1) * pageSize) .limit(pageSize);使用投影// 投影减少数据传输const users = await User.find( { status: 'active' }, { name: 1, email: 1, _id: 0 });批量操作批量插入// 使用 insertMany 代替多次 insertOneconst users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }, // ... 更多用户]);批量更新// 使用 updateMany 代替多次 updateOneawait User.updateMany( { status: 'pending' }, { status: 'active' });批量删除// 使用 deleteMany 代替多次 deleteOneawait User.deleteMany({ status: 'deleted' });缓存策略查询缓存const userSchema = new Schema({ name: String, email: String}, { query: { cache: true }});// 启用缓存const users = await User.find().cache();// 设置缓存时间const users = await User.find().cache(60); // 60秒应用层缓存const NodeCache = require('node-cache');const cache = new NodeCache({ stdTTL: 600 }); // 10分钟缓存async function getUserById(userId) { const cacheKey = `user:${userId}`; let user = cache.get(cacheKey); if (!user) { user = await User.findById(userId).lean(); if (user) { cache.set(cacheKey, user); } } return user;}数据模型优化嵌入 vs 引用// 嵌入适合一对一或一对多,子文档较小const userSchema = new Schema({ name: String, profile: { bio: String, avatar: String }});// 引用适合一对多或多对多,子文档较大const postSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'User' }});避免过深嵌套// 避免过深的嵌套结构// 不推荐const badSchema = new Schema({ level1: { level2: { level3: { level4: { data: String } } } }});// 推荐:扁平化结构const goodSchema = new Schema({ level1: String, level2: String, level3: String, level4: String});监控和调优查询性能监控// 启用调试模式mongoose.set('debug', true);// 自定义调试函数mongoose.set('debug', (collectionName, method, query, doc) => { console.log(`${collectionName}.${method}`, JSON.stringify(query));});慢查询日志// 记录慢查询mongoose.connection.on('connected', () => { mongoose.connection.db.admin().command({ profile: 1, slowms: 100 // 超过100ms的查询 });});最佳实践总结连接管理:使用连接池,避免频繁连接断开索引优化:为常用查询创建合适的索引查询优化:使用 lean()、选择性查询、限制结果批量操作:使用批量操作代替多次单条操作缓存策略:合理使用查询缓存和应用层缓存数据模型:根据访问模式选择嵌入或引用监控调优:持续监控查询性能,及时优化避免 N+1 查询:合理设计数据结构,避免循环查询
阅读 0·2月22日 20:12

Mongoose 子文档如何使用,有哪些应用场景?

Mongoose 子文档(Subdocuments)是嵌套在父文档中的文档,它们可以是单个文档或文档数组。子文档提供了一种组织相关数据的方式,同时保持数据的完整性。子文档类型1. 嵌套 Schema(单个子文档)const addressSchema = new Schema({ street: String, city: String, state: String, zipCode: String});const userSchema = new Schema({ name: String, email: String, address: addressSchema // 单个子文档});const User = mongoose.model('User', userSchema);// 创建包含子文档的用户const user = await User.create({ name: 'John Doe', email: 'john@example.com', address: { street: '123 Main St', city: 'New York', state: 'NY', zipCode: '10001' }});2. 子文档数组const commentSchema = new Schema({ text: String, author: String, createdAt: { type: Date, default: Date.now }});const postSchema = new Schema({ title: String, content: String, comments: [commentSchema] // 子文档数组});const Post = mongoose.model('Post', postSchema);// 创建包含子文档数组的文章const post = await Post.create({ title: 'My First Post', content: 'This is my first post', comments: [ { text: 'Great post!', author: 'Alice' }, { text: 'Thanks for sharing', author: 'Bob' } ]});子文档操作访问子文档// 访问单个子文档const user = await User.findById(userId);console.log(user.address.city); // "New York"// 访问子文档数组const post = await Post.findById(postId);console.log(post.comments[0].text); // "Great post!"修改子文档// 修改单个子文档const user = await User.findById(userId);user.address.city = 'Los Angeles';await user.save();// 修改子文档数组元素const post = await Post.findById(postId);post.comments[0].text = 'Updated comment';await post.save();添加子文档到数组// 添加新评论const post = await Post.findById(postId);post.comments.push({ text: 'New comment', author: 'Charlie'});await post.save();// 使用 unshift 添加到开头post.comments.unshift({ text: 'First comment', author: 'Dave'});await post.save();删除子文档// 删除数组中的子文档const post = await Post.findById(postId);post.comments.splice(1, 1); // 删除第二个评论await post.save();// 使用 pull 删除符合条件的子文档post.comments.pull({ author: 'Alice' });await post.save();子文档中间件子文档级别的中间件commentSchema.pre('save', function(next) { console.log('Saving comment:', this.text); next();});commentSchema.post('save', function(doc) { console.log('Comment saved:', doc.text);});父文档中间件postSchema.pre('save', function(next) { console.log('Saving post with', this.comments.length, 'comments'); next();});子文档验证子文档级别的验证const addressSchema = new Schema({ street: { type: String, required: true }, city: { type: String, required: true }, state: { type: String, required: true, minlength: 2 }, zipCode: { type: String, required: true, match: /^\d{5}$/ }});// 验证会在保存父文档时自动触发try { const user = await User.create({ name: 'John', email: 'john@example.com', address: { street: '123 Main St', city: 'New York', state: 'NY', zipCode: '10001' } });} catch (error) { console.error('Validation error:', error.message);}子文档方法为子文档添加方法commentSchema.methods.getFormattedDate = function() { return this.createdAt.toLocaleDateString();};const post = await Post.findById(postId);console.log(post.comments[0].getFormattedDate());为子文档添加静态方法commentSchema.statics.findByAuthor = function(author) { return this.find({ author });};// 注意:子文档的静态方法通常不直接使用// 更常见的是在父文档上定义方法来操作子文档子文档引用使用 ObjectId 引用const postSchema = new Schema({ title: String, content: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]});const commentSchema = new Schema({ text: String, author: String});// 使用 populate 获取完整评论const post = await Post.findById(postId).populate('comments');子文档 vs 引用选择指南使用子文档当:数据总是与父文档一起访问子文档数量有限且相对较小需要原子性更新数据不需要独立查询使用引用当:子文档可能独立访问子文档数量可能很大需要跨多个文档查询需要更好的性能// 子文档示例 - 适合少量评论const postSchema = new Schema({ title: String, comments: [commentSchema]});// 引用示例 - 适合大量评论const postSchema = new Schema({ title: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]});高级用法子文档数组操作// 使用 $push 添加元素await Post.findByIdAndUpdate(postId, { $push: { comments: { $each: [ { text: 'Comment 1', author: 'User1' }, { text: 'Comment 2', author: 'User2' } ], $position: 0 // 添加到开头 } }});// 使用 $pull 删除元素await Post.findByIdAndUpdate(postId, { $pull: { comments: { author: 'User1' } }});// 使用 $set 更新特定元素await Post.updateOne( { _id: postId, 'comments._id': commentId }, { $set: { 'comments.$.text': 'Updated text' } });子文档验证器const postSchema = new Schema({ title: String, comments: [commentSchema]});// 自定义验证器postSchema.path('comments').validate(function(comments) { return comments.length <= 100;}, 'Maximum 100 comments allowed');// 验证子文档属性postSchema.path('comments').validate(function(comments) { return comments.every(comment => comment.text.length > 0);}, 'All comments must have text');最佳实践合理选择结构:根据访问模式选择子文档或引用限制数组大小:避免子文档数组过大使用验证:为子文档添加适当的验证规则考虑性能:大型子文档数组可能影响性能使用中间件:利用中间件处理子文档逻辑文档清晰:为子文档 Schema 添加清晰的注释测试覆盖:为子文档操作编写测试
阅读 0·2月22日 20:12

Mongoose 和原生 MongoDB 驱动有什么区别?

Mongoose 和原生 MongoDB 驱动都是 Node.js 中与 MongoDB 交互的工具,但它们在设计理念、使用方式和适用场景上有显著差异。主要区别1. 抽象层次Mongoose(ODM - 对象数据模型)const userSchema = new Schema({ name: { type: String, required: true }, email: { type: String, unique: true }, age: { type: Number, min: 0 }});const User = mongoose.model('User', userSchema);const user = await User.create({ name: 'John', email: 'john@example.com', age: 25 });原生 MongoDB 驱动const { MongoClient } = require('mongodb');const client = await MongoClient.connect('mongodb://localhost:27017');const db = client.db('mydb');const user = await db.collection('users').insertOne({ name: 'John', email: 'john@example.com', age: 25});2. 数据验证Mongooseconst userSchema = new Schema({ email: { type: String, required: true, unique: true, match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }, age: { type: Number, min: 0, max: 120 }});try { await User.create({ email: 'invalid-email', age: 150 });} catch (error) { console.log(error.message); // 验证错误}原生 MongoDB 驱动// 没有内置验证,需要手动实现function validateUser(user) { if (!user.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(user.email)) { throw new Error('Invalid email'); } if (user.age < 0 || user.age > 120) { throw new Error('Invalid age'); }}validateUser({ email: 'invalid-email', age: 150 });await db.collection('users').insertOne(user);3. 类型安全Mongooseconst user = await User.findById(userId);user.age = 'twenty-five'; // 自动转换为数字或报错await user.save();原生 MongoDB 驱动const user = await db.collection('users').findOne({ _id: userId });user.age = 'twenty-five'; // 不会有类型检查await db.collection('users').updateOne( { _id: userId }, { $set: user });4. 中间件和钩子MongooseuserSchema.pre('save', function(next) { this.email = this.email.toLowerCase(); next();});userSchema.post('save', function(doc) { console.log('User saved:', doc.email);});原生 MongoDB 驱动// 需要手动实现类似功能async function saveUser(user) { user.email = user.email.toLowerCase(); const result = await db.collection('users').insertOne(user); console.log('User saved:', user.email); return result;}5. 查询构建器Mongooseconst users = await User.find({ age: { $gte: 18 } }) .select('name email') .sort({ name: 1 }) .limit(10) .lean();原生 MongoDB 驱动const users = await db.collection('users') .find({ age: { $gte: 18 } }) .project({ name: 1, email: 1 }) .sort({ name: 1 }) .limit(10) .toArray();性能对比查询性能Mongoose// 有额外的抽象层开销const users = await User.find({ age: { $gte: 18 } });原生 MongoDB 驱动// 直接操作,性能更好const users = await db.collection('users').find({ age: { $gte: 18 } }).toArray();批量操作Mongoose// 使用 insertManyconst users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }]);原生 MongoDB 驱动// 使用 bulkWriteawait db.collection('users').bulkWrite([ { insertOne: { document: { name: 'John', email: 'john@example.com' } } }, { insertOne: { document: { name: 'Jane', email: 'jane@example.com' } } }]);适用场景使用 Mongoose 当:需要数据验证:需要强制数据结构和类型团队协作:多人开发,需要统一的接口快速开发:需要快速构建原型复杂业务逻辑:需要中间件和钩子类型安全:使用 TypeScript 时需要类型定义// 适合使用 Mongoose 的场景const userSchema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, createdAt: { type: Date, default: Date.now }});userSchema.pre('save', async function(next) { this.password = await bcrypt.hash(this.password, 10); next();});使用原生 MongoDB 驱动当:性能关键:需要最佳性能灵活的数据结构:数据结构经常变化简单操作:只需要基本的 CRUD 操作学习 MongoDB:想深入了解 MongoDB微服务:需要轻量级依赖// 适合使用原生驱动的场景const users = await db.collection('users') .find({ age: { $gte: 18 } }) .project({ name: 1, email: 1 }) .toArray();迁移指南从 Mongoose 到原生驱动// Mongooseconst user = await User.findById(userId);// 原生驱动const user = await db.collection('users').findOne({ _id: new ObjectId(userId) });从原生驱动到 Mongoose// 原生驱动const users = await db.collection('users').find({}).toArray();// Mongooseconst users = await User.find().lean();混合使用可以在同一项目中同时使用两者:// 使用 Mongoose 处理需要验证的数据const User = mongoose.model('User', userSchema);const user = await User.create(userData);// 使用原生驱动处理高性能查询const stats = await db.collection('users').aggregate([ { $group: { _id: '$city', count: { $sum: 1 } } }]).toArray();总结| 特性 | Mongoose | 原生驱动 ||------|----------|----------|| 抽象层次 | 高(ODM) | 低(直接驱动) || 数据验证 | 内置 | 需手动实现 || 类型安全 | 强 | 弱 || 中间件 | 支持 | 不支持 || 学习曲线 | 较陡 | 较平 || 性能 | 较低 | 较高 || 灵活性 | 较低 | 较高 || 开发效率 | 高 | 中等 |最佳实践根据项目需求选择:考虑团队规模、性能要求、开发速度可以混合使用:在不同场景使用最适合的工具性能测试:对性能关键路径进行测试团队共识:确保团队对选择有共识文档完善:为选择提供充分的文档和理由
阅读 0·2月22日 20:12

Mongoose 中间件和钩子如何工作,有哪些应用场景?

Mongoose 中间件(Middleware)和钩子(Hooks)是强大的功能,允许在执行某些操作之前或之后执行自定义逻辑。中间件分为两类:文档中间件和查询中间件。中间件类型1. 文档中间件(Document Middleware)在文档实例上执行的操作,如 save()、validate()、remove() 等。userSchema.pre('save', function(next) { console.log('About to save user:', this.name); next();});userSchema.post('save', function(doc) { console.log('User saved:', doc.name);});2. 查询中间件(Query Middleware)在 Model 查询上执行的操作,如 find()、findOne()、updateOne() 等。userSchema.pre('find', function() { this.where({ deleted: false });});userSchema.post('find', function(docs) { console.log('Found', docs.length, 'users');});常用钩子文档操作钩子validate - 验证文档save - 保存文档remove - 删除文档init - 初始化文档(从数据库加载)查询操作钩子count - 计数查询find - 查找文档findOne - 查找单个文档findOneAndDelete - 查找并删除findOneAndUpdate - 查找并更新updateOne - 更新单个文档updateMany - 更新多个文档deleteOne - 删除单个文档deleteMany - 删除多个文档Pre 和 Post 钩子的区别Pre 钩子在操作执行前运行可以修改数据或中止操作必须调用 next() 或返回 Promise可以访问 this(文档实例或查询对象)userSchema.pre('save', function(next) { if (this.age < 0) { const err = new Error('Age cannot be negative'); return next(err); } this.email = this.email.toLowerCase(); next();});Post 钩子在操作执行后运行不能修改数据或中止操作接收操作结果作为参数可以访问 this(文档实例或查询对象)userSchema.post('save', function(doc) { console.log('User saved with ID:', doc._id); // 发送通知、记录日志等});异步中间件Mongoose 中间件支持异步操作:// 使用 async/awaituserSchema.pre('save', async function(next) { const existing = await this.constructor.findOne({ email: this.email }); if (existing && existing._id.toString() !== this._id.toString()) { const err = new Error('Email already exists'); return next(err); } next();});// 返回 PromiseuserSchema.pre('save', function() { return checkEmailAvailability(this.email).then(isAvailable => { if (!isAvailable) { throw new Error('Email already exists'); } });});实际应用场景密码哈希:在保存用户前对密码进行加密时间戳:自动设置 createdAt 和 updatedAt软删除:在删除前标记为已删除数据验证:执行复杂的验证逻辑日志记录:记录操作历史缓存失效:更新相关缓存关联数据:自动更新关联文档通知发送:在操作后发送通知注意事项中间件按定义顺序执行pre 钩子中的错误会中止操作查询中间件不会触发文档中间件使用 findOneAndUpdate 等方法时,需要设置 { runValidators: true } 来触发验证中间件中避免无限循环
阅读 0·2月22日 20:12

Mongoose Schema 是什么,如何定义和使用?

Mongoose Schema(模式)是 Mongoose 的核心概念,用于定义 MongoDB 文档的结构、数据类型、验证规则和默认值。Schema 本身不是数据库中的集合,而是一个蓝图,用于创建 Model。Schema 的基本定义const mongoose = require('mongoose');const { Schema } = mongoose;const userSchema = new Schema({ name: { type: String, required: true, trim: true }, email: { type: String, required: true, unique: true, lowercase: true, trim: true }, age: { type: Number, min: 0, max: 120 }, createdAt: { type: Date, default: Date.now }});Schema 的主要属性字段类型:String、Number、Date、Buffer、Boolean、Mixed、ObjectId、Array验证器:required、min、max、enum、match、validate修饰符:lowercase、uppercase、trim、default索引:unique、sparse、index虚拟字段:不存储在数据库中的计算字段实例方法:添加到文档实例的方法静态方法:添加到模型类的方法中间件:pre 和 post 钩子Schema 与 Model 的关系Schema 是定义,Model 是构造函数通过 mongoose.model('User', userSchema) 创建 ModelModel 的实例是 Document,代表数据库中的实际文档一个 Schema 可以创建多个 Model(不推荐)Schema 的优势数据一致性:强制文档结构一致数据验证:在应用层验证数据类型安全:提供类型检查和转换中间件支持:可以在操作前后执行逻辑可扩展性:可以添加方法和虚拟字段
阅读 0·2月22日 20:12

Mongoose 数据验证有哪些类型,如何实现自定义验证?

Mongoose 提供了强大的数据验证功能,可以在保存数据到数据库之前验证数据的完整性和正确性。验证可以在 Schema 层面定义,也可以自定义验证器。内置验证器1. 必填验证(required)const userSchema = new Schema({ name: { type: String, required: [true, 'Name is required'] }, email: { type: String, required: true }});2. 类型验证(type)const userSchema = new Schema({ age: Number, isActive: Boolean, birthDate: Date});3. 枚举验证(enum)const userSchema = new Schema({ status: { type: String, enum: ['active', 'inactive', 'pending'], enum: { values: ['active', 'inactive', 'pending'], message: '{VALUE} is not a valid status' } }});4. 范围验证(min, max)const userSchema = new Schema({ age: { type: Number, min: [0, 'Age must be at least 0'], max: [120, 'Age cannot exceed 120'] }, score: { type: Number, min: 0, max: 100 }});5. 长度验证(minlength, maxlength)const userSchema = new Schema({ username: { type: String, minlength: [3, 'Username must be at least 3 characters'], maxlength: [20, 'Username cannot exceed 20 characters'] }});6. 正则表达式验证(match)const userSchema = new Schema({ email: { type: String, match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'] }, phone: { type: String, match: /^[0-9]{10}$/, message: 'Phone number must be 10 digits' }});7. 唯一验证(unique)const userSchema = new Schema({ email: { type: String, unique: true, index: true }});8. 默认值(default)const userSchema = new Schema({ status: { type: String, default: 'active' }, createdAt: { type: Date, default: Date.now }});自定义验证器单字段验证器const userSchema = new Schema({ password: { type: String, validate: { validator: function(v) { return v.length >= 8; }, message: 'Password must be at least 8 characters long' } }});异步验证器const userSchema = new Schema({ email: { type: String, validate: { validator: async function(v) { const user = await this.constructor.findOne({ email: v }); return !user || user._id.toString() === this._id.toString(); }, message: 'Email already exists' } }});多字段验证器const userSchema = new Schema({ password: String, confirmPassword: String});userSchema.path('confirmPassword').validate(function(v) { return v === this.password;}, 'Passwords do not match');验证时机验证在以下时机自动触发:save() - 保存文档时validate() - 显式调用验证时validateSync() - 同步验证时const user = new User({ name: '', age: -5 });try { await user.save();} catch (err) { console.log(err.errors.name.message); // "Name is required" console.log(err.errors.age.message); // "Age must be at least 0"}跳过验证在某些情况下,可以跳过验证:// 跳过验证保存await user.save({ validateBeforeSave: false });// 跳过验证更新await User.findByIdAndUpdate(id, { age: 25 }, { runValidators: false });验证错误处理userSchema.pre('validate', function(next) { if (this.password !== this.confirmPassword) { this.invalidate('confirmPassword', 'Passwords do not match'); } next();});// 捕获验证错误try { await user.save();} catch (err) { if (err.name === 'ValidationError') { Object.keys(err.errors).forEach(field => { console.log(`${field}: ${err.errors[field].message}`); }); }}最佳实践在 Schema 层面定义验证规则提供清晰的错误消息使用异步验证器检查唯一性在前端和后端都进行验证考虑性能影响,避免过于复杂的验证使用自定义验证器处理业务逻辑记录验证失败的情况
阅读 0·2月22日 20:12

Mongoose 查询构建器有哪些常用方法和优化技巧?

Mongoose 提供了强大的查询构建器,支持链式调用和丰富的查询操作,使 MongoDB 查询更加直观和易用。基本查询查找所有文档const users = await User.find();条件查询// 等于const users = await User.find({ age: 25 });// 不等于const users = await User.find({ age: { $ne: 25 } });// 大于const users = await User.find({ age: { $gt: 18 } });// 大于等于const users = await User.find({ age: { $gte: 18 } });// 小于const users = await User.find({ age: { $lt: 30 } });// 小于等于const users = await User.find({ age: { $lte: 30 } });// 在数组中const users = await User.find({ status: { $in: ['active', 'pending'] } });// 不在数组中const users = await User.find({ status: { $nin: ['deleted'] } });逻辑操作符// ANDconst users = await User.find({ age: { $gte: 18 }, status: 'active'});// ORconst users = await User.find({ $or: [ { status: 'active' }, { status: 'pending' } ]});// NOTconst users = await User.find({ status: { $not: { $eq: 'deleted' } }});// NORconst users = await User.find({ $nor: [ { status: 'deleted' }, { status: 'inactive' } ]});链式查询选择字段// 只选择特定字段const users = await User.find() .select('name email age');// 排除特定字段const users = await User.find() .select('-password -__v');// 使用对象语法const users = await User.find() .select({ name: 1, email: 1, age: 1 });排序// 升序const users = await User.find() .sort({ name: 1 });// 降序const users = await User.find() .sort({ age: -1 });// 多字段排序const users = await User.find() .sort({ status: 1, age: -1 });限制和跳过// 限制结果数量const users = await User.find() .limit(10);// 跳过指定数量const users = await User.find() .skip(5);// 分页const page = 2;const pageSize = 10;const users = await User.find() .skip((page - 1) * pageSize) .limit(pageSize);高级查询正则表达式// 包含const users = await User.find({ name: /john/i });// 以开头const users = await User.find({ name: /^john/i });// 以结尾const users = await User.find({ name: /doe$/i });数组查询// 数组包含特定值const users = await User.find({ tags: 'javascript' });// 数组包含多个值(AND)const users = await User.find({ tags: { $all: ['javascript', 'nodejs'] } });// 数组大小const users = await User.find({ tags: { $size: 3 } });嵌套文档查询// 嵌套字段查询const users = await User.find({ 'address.city': 'New York' });// 嵌套数组查询const users = await User.find({ 'orders.status': 'completed' });元素查询// 字段存在const users = await User.find({ email: { $exists: true } });// 字段类型const users = await User.find({ age: { $type: 'number' } });聚合查询// 分组统计const result = await User.aggregate([ { $match: { age: { $gte: 18 } } }, { $group: { _id: '$status', count: { $sum: 1 } } }]);// 查找并修改const user = await User.findOneAndUpdate( { email: 'john@example.com' }, { age: 25 }, { new: true });查询优化使用索引const userSchema = new Schema({ email: { type: String, index: true }, age: { type: Number, index: true }});// 复合索引userSchema.index({ email: 1, age: -1 });投影优化// 只查询需要的字段const users = await User.find() .select('name email');使用 lean()// 返回普通 JavaScript 对象,性能更好const users = await User.find().lean();批量操作// 批量插入const users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }]);// 批量更新const result = await User.updateMany( { status: 'pending' }, { status: 'active' });查询缓存Mongoose 支持查询缓存以提高性能:const userSchema = new Schema({ name: String, email: String}, { query: { cache: true }});// 启用缓存const users = await User.find().cache();// 设置缓存时间const users = await User.find().cache(60); // 60秒最佳实践为常用查询字段创建索引使用 select() 只查询需要的字段对于只读查询使用 lean() 提高性能合理使用分页避免查询大量数据避免在循环中执行查询使用批量操作代替多次单条操作监控查询性能,优化慢查询使用 explain() 分析查询计划
阅读 0·2月22日 20:12

Mongoose 插件如何创建和使用?

Mongoose 插件(Plugins)是一种可重用的机制,用于扩展 Mongoose Schema 的功能。插件允许你将通用功能封装起来,并在多个 Schema 中复用。基本插件创建简单插件// timestamp.jsfunction timestampPlugin(schema) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(next) { this.updatedAt = new Date(); next(); });}module.exports = timestampPlugin;使用插件const timestampPlugin = require('./plugins/timestamp');const userSchema = new Schema({ name: String, email: String});userSchema.plugin(timestampPlugin);const User = mongoose.model('User', userSchema);插件选项带选项的插件// softDelete.jsfunction softDeletePlugin(schema, options = {}) { const deletedAtField = options.deletedAtField || 'deletedAt'; const isDeletedField = options.isDeletedField || 'isDeleted'; schema.add({ [deletedAtField]: Date, [isDeletedField]: { type: Boolean, default: false } }); schema.pre('find', function() { this.where({ [isDeletedField]: { $ne: true } }); }); schema.pre('findOne', function() { this.where({ [isDeletedField]: { $ne: true } }); }); schema.methods.softDelete = function() { this[isDeletedField] = true; this[deletedAtField] = new Date(); return this.save(); };}module.exports = softDeletePlugin;// 使用带选项的插件userSchema.plugin(softDeletePlugin, { deletedAtField: 'deletedAt', isDeletedField: 'isDeleted'});常用插件类型1. 时间戳插件function timestampPlugin(schema) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(next) { this.updatedAt = new Date(); next(); });}2. 软删除插件function softDeletePlugin(schema) { schema.add({ deletedAt: Date, isDeleted: { type: Boolean, default: false } }); schema.pre('find', function() { this.where({ isDeleted: { $ne: true } }); }); schema.pre('findOne', function() { this.where({ isDeleted: { $ne: true } }); }); schema.methods.softDelete = function() { this.isDeleted = true; this.deletedAt = new Date(); return this.save(); }; schema.statics.findDeleted = function() { return this.find({ isDeleted: true }); };}3. 分页插件function paginatePlugin(schema) { schema.statics.paginate = function(query = {}, options = {}) { const { page = 1, limit = 10, sort = {}, populate = [] } = options; const skip = (page - 1) * limit; return Promise.all([ this.countDocuments(query), this.find(query) .sort(sort) .skip(skip) .limit(limit) .populate(populate) ]).then(([total, docs]) => ({ docs, total, page, pages: Math.ceil(total / limit) })); };}// 使用分页插件const result = await User.paginate( { status: 'active' }, { page: 1, limit: 10, sort: { name: 1 } });4. 自动填充插件function autoPopulatePlugin(schema, options) { const fields = options.fields || []; schema.pre('find', function() { fields.forEach(field => { this.populate(field); }); }); schema.pre('findOne', function() { fields.forEach(field => { this.populate(field); }); });}// 使用自动填充插件userSchema.plugin(autoPopulatePlugin, { fields: ['profile', 'settings']});5. 加密插件const bcrypt = require('bcrypt');function encryptionPlugin(schema, options) { const fields = options.fields || ['password']; schema.pre('save', async function(next) { for (const field of fields) { if (this.isModified(field)) { this[field] = await bcrypt.hash(this[field], 10); } } next(); }); schema.methods.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password); };}// 使用加密插件userSchema.plugin(encryptionPlugin, { fields: ['password']});插件组合多个插件const timestampPlugin = require('./plugins/timestamp');const softDeletePlugin = require('./plugins/softDelete');const paginatePlugin = require('./plugins/paginate');userSchema.plugin(timestampPlugin);userSchema.plugin(softDeletePlugin);userSchema.plugin(paginatePlugin);插件依赖function advancedPlugin(schema, options) { // 依赖其他插件的功能 if (!schema.path('createdAt')) { throw new Error('timestampPlugin must be applied before advancedPlugin'); } // 使用其他插件添加的字段 schema.virtual('age').get(function() { return Date.now() - this.createdAt.getTime(); });}全局插件应用到所有 Schema// 全局应用插件mongoose.plugin(timestampPlugin);// 之后创建的所有 Schema 都会自动应用该插件const userSchema = new Schema({ name: String });const postSchema = new Schema({ title: String });// 两个 Schema 都会有 createdAt 和 updatedAt 字段条件全局插件// 只对特定模型应用插件mongoose.plugin(function(schema) { if (schema.options.enableTimestamps) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); }});// 在 Schema 选项中启用const userSchema = new Schema({ name: String }, { enableTimestamps: true });插件最佳实践1. 插件命名// 使用清晰的命名const timestampPlugin = require('./plugins/timestamp');const softDeletePlugin = require('./plugins/softDelete');2. 插件文档/** * 软删除插件 * * 功能: * - 添加 deletedAt 和 isDeleted 字段 * - 自动过滤已删除的文档 * - 提供 softDelete() 方法 * * 选项: * - deletedAtField: 删除时间字段名(默认:deletedAt) * - isDeletedField: 删除标记字段名(默认:isDeleted) */function softDeletePlugin(schema, options = {}) { // 插件实现}3. 插件测试// plugins/softDelete.test.jsconst mongoose = require('mongoose');const softDeletePlugin = require('./softDelete');describe('softDeletePlugin', () => { let User; beforeAll(async () => { await mongoose.connect('mongodb://localhost/test'); const userSchema = new Schema({ name: String }); userSchema.plugin(softDeletePlugin); User = mongoose.model('User', userSchema); }); it('should add soft delete fields', async () => { const user = await User.create({ name: 'John' }); expect(user.isDeleted).toBe(false); expect(user.deletedAt).toBeUndefined(); }); it('should filter deleted documents', async () => { const user = await User.create({ name: 'Jane' }); await user.softDelete(); const users = await User.find(); expect(users.length).toBe(0); });});发布插件NPM 包结构mongoose-plugin-softdelete/├── package.json├── README.md├── index.js└── test/ └── softDelete.test.jspackage.json{ "name": "mongoose-plugin-softdelete", "version": "1.0.0", "description": "Mongoose plugin for soft delete functionality", "main": "index.js", "keywords": ["mongoose", "plugin", "soft-delete"], "peerDependencies": { "mongoose": ">=6.0.0" }}最佳实践保持插件简单:每个插件只做一件事提供选项:允许用户自定义插件行为文档完善:提供清晰的文档和示例测试覆盖:为插件编写完整的测试版本管理:使用语义化版本控制错误处理:妥善处理错误情况性能考虑:避免插件影响性能向后兼容:保持 API 的向后兼容性
阅读 0·2月22日 20:12

Mongoose 如何处理事务和并发控制?

Mongoose 支持 MongoDB 的事务功能,允许在多个文档或集合之间执行原子性操作。事务是确保数据一致性的重要机制。事务基础启用事务支持MongoDB 4.0+ 支持副本集上的事务,MongoDB 4.2+ 支持分片集群上的事务。const mongoose = require('mongoose');// 连接到副本集mongoose.connect('mongodb://localhost:27017/mydb?replicaSet=myReplicaSet');创建会话const session = await mongoose.startSession();基本事务操作简单事务const session = await mongoose.startSession();session.startTransaction();try { // 执行操作 const user = await User.create([{ name: 'John' }], { session }); const account = await Account.create([{ userId: user[0]._id, balance: 100 }], { session }); // 提交事务 await session.commitTransaction(); console.log('Transaction committed');} catch (error) { // 回滚事务 await session.abortTransaction(); console.log('Transaction aborted:', error);} finally { // 结束会话 session.endSession();}更新操作事务const session = await mongoose.startSession();session.startTransaction();try { // 转账操作 const fromAccount = await Account.findById(fromAccountId).session(session); const toAccount = await Account.findById(toAccountId).session(session); if (fromAccount.balance < amount) { throw new Error('Insufficient balance'); } fromAccount.balance -= amount; toAccount.balance += amount; await fromAccount.save({ session }); await toAccount.save({ session }); await session.commitTransaction();} catch (error) { await session.abortTransaction(); throw error;} finally { session.endSession();}事务选项读写关注const session = await mongoose.startSession({ defaultTransactionOptions: { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary' }});session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }});事务超时session.startTransaction({ maxTimeMS: 5000 // 5秒超时});并发控制乐观锁使用版本号实现乐观锁:const productSchema = new Schema({ name: String, stock: Number, version: { type: Number, default: 0 }});productSchema.pre('save', function(next) { this.increment(); next();});// 更新时检查版本号const product = await Product.findById(productId);product.stock -= quantity;try { await product.save();} catch (error) { if (error.name === 'VersionError') { console.log('Document was modified by another process'); }}悲观锁使用 findOneAndUpdate 实现悲观锁:const session = await mongoose.startSession();session.startTransaction();try { // 查找并锁定文档 const product = await Product.findOneAndUpdate( { _id: productId, locked: false }, { locked: true }, { session, new: true } ); if (!product) { throw new Error('Product is locked or not found'); } // 执行操作 product.stock -= quantity; await product.save({ session }); // 释放锁 product.locked = false; await product.save({ session }); await session.commitTransaction();} catch (error) { await session.abortTransaction(); throw error;} finally { session.endSession();}原子操作原子更新// 原子递增await User.findByIdAndUpdate(userId, { $inc: { balance: 100 } });// 原子条件更新await User.findOneAndUpdate( { _id: userId, balance: { $gte: 100 } }, { $inc: { balance: -100 } });// 原子数组操作await User.findByIdAndUpdate(userId, { $push: { tags: 'new-tag' }, $addToSet: { uniqueTags: 'unique-tag' }});批量原子操作// 批量更新await User.updateMany( { status: 'active' }, { $set: { lastActive: new Date() } });// 批量删除await User.deleteMany({ status: 'deleted' });事务最佳实践保持事务简短:事务时间越长,冲突概率越高避免长时间持有锁:尽快释放资源使用适当的隔离级别:根据业务需求选择处理重试逻辑:实现自动重试机制监控事务性能:跟踪事务执行时间和失败率合理设计数据模型:减少跨文档事务的需求// 自动重试事务async function runWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (error.name === 'TransientTransactionError' && i < maxRetries - 1) { console.log(`Retrying transaction (attempt ${i + 1})`); await new Promise(resolve => setTimeout(resolve, 100 * (i + 1))); } else { throw error; } } }}注意事项事务只能在副本集或分片集群上使用事务操作必须在同一个会话中执行事务会带来性能开销,谨慎使用避免在事务中执行耗时操作确保正确处理事务错误和回滚考虑使用原子操作替代简单事务
阅读 0·2月22日 20:08