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

Mongoose

Mongoose 是一个面向 MongoDB 数据库的对象数据模型(ODM)库,用于在 Node.js 环境下建模和操作 MongoDB 文档结构。它提供了一些方便的特性,如数据验证、查询构建、业务逻辑钩子(hooks)和中间件,使得处理 MongoDB 文档更加直观和安全。
Mongoose
查看更多相关内容
Mongoose 鉴别器(Discriminators)如何使用?Mongoose Discriminators(鉴别器)是一种模式继承机制,允许你在同一个集合中存储不同类型的文档,同时保持各自独特的字段和验证规则。这对于处理具有共同基础但又有特定差异的数据模型非常有用。 ## 基本概念 ### 创建基础 Schema ```javascript const eventSchema = new Schema({ name: { type: String, required: true }, date: { type: Date, required: true }, location: String }, { discriminatorKey: 'kind' // 用于区分不同类型的字段 }); const Event = mongoose.model('Event', eventSchema); ``` ### 创建鉴别器 ```javascript // 创建会议类型的鉴别器 const conferenceSchema = new Schema({ speakers: [String], sponsors: [String] }); const Conference = Event.discriminator('Conference', conferenceSchema); // 创建聚会类型的鉴别器 const meetupSchema = new Schema({ attendees: Number, maxAttendees: Number }); const Meetup = Event.discriminator('Meetup', meetupSchema); ``` ## 使用鉴别器 ### 创建文档 ```javascript // 创建基础事件 const event = await Event.create({ name: 'General Event', date: new Date('2024-01-01'), location: 'New York' }); // 创建会议 const conference = await Conference.create({ name: 'Tech Conference', date: new Date('2024-02-01'), location: 'San Francisco', speakers: ['Alice', 'Bob'], sponsors: ['Company A', 'Company B'] }); // 创建聚会 const meetup = await Meetup.create({ name: 'Developer Meetup', date: new Date('2024-03-01'), location: 'Boston', attendees: 50, maxAttendees: 100 }); ``` ### 查询文档 ```javascript // 查询所有事件 const allEvents = await Event.find(); // 查询特定类型的事件 const conferences = await Conference.find(); const meetups = await Meetup.find(); // 使用 discriminatorKey 查询 const conferences2 = await Event.find({ kind: 'Conference' }); ``` ## 嵌套鉴别器 ### 在子文档中使用鉴别器 ```javascript const batchSchema = new Schema({ name: String, size: Number, product: { type: Schema.Types.ObjectId, ref: 'Product' } }, { discriminatorKey: 'kind' }); const orderSchema = new Schema({ customer: String, items: [batchSchema] }); // 创建产品类型的鉴别器 const productBatchSchema = new Schema({ quantity: Number, unit: String }); const productBatch = batchSchema.discriminator('ProductBatch', productBatchSchema); // 创建服务类型的鉴别器 const serviceBatchSchema = new Schema({ duration: Number, rate: Number }); const serviceBatch = batchSchema.discriminator('ServiceBatch', serviceBatchSchema); ``` ## 鉴别器中间件 ### 为鉴别器添加中间件 ```javascript // 为会议添加中间件 conferenceSchema.pre('save', function(next) { console.log('Saving conference:', this.name); next(); }); // 为聚会添加中间件 meetupSchema.pre('save', function(next) { if (this.attendees > this.maxAttendees) { return next(new Error('Attendees cannot exceed max')); } next(); }); ``` ### 基础 Schema 的中间件 ```javascript // 基础 Schema 的中间件会应用到所有鉴别器 eventSchema.pre('save', function(next) { console.log('Saving event:', this.name); next(); }); ``` ## 鉴别器方法 ### 为鉴别器添加方法 ```javascript // 为会议添加方法 conferenceSchema.methods.getSpeakerCount = function() { return this.speakers.length; }; // 为聚会添加方法 meetupSchema.methods.getAvailableSpots = function() { return this.maxAttendees - this.attendees; }; // 使用方法 const conference = await Conference.findById(conferenceId); console.log(conference.getSpeakerCount()); const meetup = await Meetup.findById(meetupId); console.log(meetup.getAvailableSpots()); ``` ## 鉴别器验证 ### 为鉴别器添加验证 ```javascript conferenceSchema.path('speakers').validate(function(speakers) { return speakers.length > 0; }, 'Conference must have at least one speaker'); meetupSchema.path('attendees').validate(function(attendees) { return attendees >= 0; }, 'Attendees cannot be negative'); ``` ## 实际应用场景 ### 1. 内容管理系统 ```javascript const contentSchema = new Schema({ title: { type: String, required: true }, author: { type: String, required: true }, publishedAt: Date }, { discriminatorKey: 'contentType' }); const Content = mongoose.model('Content', contentSchema); // 文章类型 const articleSchema = new Schema({ body: String, tags: [String] }); const Article = Content.discriminator('Article', articleSchema); // 视频类型 const videoSchema = new Schema({ url: String, duration: Number, thumbnail: String }); const Video = Content.discriminator('Video', videoSchema); ``` ### 2. 用户角色系统 ```javascript const userSchema = new Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true } }, { discriminatorKey: 'role' }); const User = mongoose.model('User', userSchema); // 管理员 const adminSchema = new Schema({ permissions: [String], department: String }); const Admin = User.discriminator('Admin', adminSchema); // 客户 const customerSchema = new Schema({ address: String, phone: String, loyaltyPoints: { type: Number, default: 0 } }); const Customer = User.discriminator('Customer', customerSchema); ``` ### 3. 订单系统 ```javascript const orderSchema = new Schema({ orderNumber: { type: String, required: true }, customer: { type: Schema.Types.ObjectId, ref: 'User' }, total: Number, status: String }, { discriminatorKey: 'orderType' }); const Order = mongoose.model('Order', orderSchema); // 在线订单 const onlineOrderSchema = new Schema({ shippingAddress: String, trackingNumber: String }); const OnlineOrder = Order.discriminator('OnlineOrder', onlineOrderSchema); // 到店订单 const inStoreOrderSchema = new Schema({ pickupTime: Date, storeLocation: String }); const InStoreOrder = Order.discriminator('InStoreOrder', inStoreOrderSchema); ``` ## 鉴别器 vs 嵌入文档 ### 选择指南 **使用鉴别器当:** - 需要在同一个集合中查询所有类型 - 不同类型有大量共同字段 - 需要统一的索引和查询 - 类型数量相对较少 **使用嵌入文档当:** - 每种类型有完全不同的结构 - 不需要跨类型查询 - 需要更好的性能隔离 - 类型数量很多 ## 最佳实践 1. **合理设计基础 Schema**:基础 Schema 应包含所有类型的共同字段 2. **使用清晰的 discriminatorKey**:选择有意义的字段名来区分类型 3. **为鉴别器添加验证**:确保每种类型的数据完整性 4. **利用中间件**:为不同类型添加特定的业务逻辑 5. **考虑性能**:鉴别器在同一个集合中,可能影响查询性能 6. **文档清晰**:为每个鉴别器添加清晰的注释 7. **测试覆盖**:为每种鉴别器编写测试
服务端 · 2月22日 20:12
Mongoose Model 有哪些常用的 CRUD 操作方法?Mongoose Model 是由 Schema 编译而成的构造函数,用于创建和操作 MongoDB 文档。Model 实例代表数据库中的文档,并提供了丰富的 CRUD 操作方法。 ## 创建 Model ```javascript const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ name: String, email: String, age: Number }); // 创建 Model,第一个参数是集合名称(会自动转为复数) const User = mongoose.model('User', userSchema); ``` ## Model 的主要方法 ### 创建文档 ```javascript // 方法1:使用 new 关键字 const user = new User({ name: 'John', email: 'john@example.com' }); await user.save(); // 方法2:使用 create 方法 const user = await User.create({ name: 'John', email: 'john@example.com' }); // 方法3:使用 insertMany const users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' } ]); ``` ### 查询文档 ```javascript // 查找所有 const users = await User.find(); // 条件查询 const user = await User.findOne({ email: 'john@example.com' }); const users = await User.find({ age: { $gte: 18 } }); // 按 ID 查找 const user = await User.findById('507f1f77bcf86cd799439011'); // 链式查询 const users = await User.find({ age: { $gte: 18 } }) .select('name email') .sort({ name: 1 }) .limit(10); ``` ### 更新文档 ```javascript // 更新单个文档 const user = await User.findByIdAndUpdate( '507f1f77bcf86cd799439011', { age: 25 }, { new: true } // 返回更新后的文档 ); // 条件更新 const result = await User.updateOne( { email: 'john@example.com' }, { age: 25 } ); // 批量更新 const result = await User.updateMany( { age: { $lt: 18 } }, { status: 'minor' } ); // findOneAndUpdate const user = await User.findOneAndUpdate( { email: 'john@example.com' }, { age: 25 }, { new: true } ); ``` ### 删除文档 ```javascript // 按 ID 删除 const user = await User.findByIdAndDelete('507f1f77bcf86cd799439011'); // 条件删除 const result = await User.deleteOne({ email: 'john@example.com' }); // 批量删除 const result = await User.deleteMany({ age: { $lt: 18 } }); // findOneAndDelete const user = await User.findOneAndDelete({ email: 'john@example.com' }); ``` ### 统计文档 ```javascript const count = await User.countDocuments({ age: { $gte: 18 } }); const count = await User.estimatedDocumentCount(); // 快速估算 ``` ## Model 的静态方法 可以在 Schema 上添加自定义静态方法: ```javascript userSchema.statics.findByEmail = function(email) { return this.findOne({ email }); }; const User = mongoose.model('User', userSchema); const user = await User.findByEmail('john@example.com'); ```
服务端 · 2月22日 20:12
Mongoose 实例方法和静态方法有什么区别?Mongoose 提供了实例方法和静态方法两种方式来扩展模型的功能。理解这两种方法的区别和使用场景对于编写可维护的代码非常重要。 ## 实例方法(Instance Methods) 实例方法是添加到文档实例上的方法,可以在单个文档上调用。这些方法可以访问 `this` 关键字来引用当前文档。 ### 定义实例方法 ```javascript const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, createdAt: { type: Date, default: Date.now } }); // 添加实例方法 userSchema.methods.getFullName = function() { return `${this.firstName} ${this.lastName}`; }; userSchema.methods.isNewUser = function() { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); return this.createdAt > oneDayAgo; }; userSchema.methods.comparePassword = function(candidatePassword) { // 使用 bcrypt 比较密码 return bcrypt.compare(candidatePassword, this.password); }; const User = mongoose.model('User', userSchema); // 使用实例方法 const user = await User.findById(userId); console.log(user.getFullName()); // "John Doe" console.log(user.isNewUser()); // true/false const isMatch = await user.comparePassword('password123'); ``` ### 实例方法的应用场景 1. **文档特定操作**:对单个文档执行操作 2. **数据验证**:验证文档数据 3. **数据转换**:转换文档数据格式 4. **业务逻辑**:封装业务逻辑 5. **状态检查**:检查文档状态 ```javascript // 示例:订单实例方法 const orderSchema = new Schema({ items: [{ product: { type: Schema.Types.ObjectId, ref: 'Product' }, quantity: Number, price: Number }], status: { type: String, enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'] }, createdAt: { type: Date, default: Date.now } }); orderSchema.methods.getTotalPrice = function() { return this.items.reduce((sum, item) => { return sum + (item.price * item.quantity); }, 0); }; orderSchema.methods.canBeCancelled = function() { return ['pending', 'paid'].includes(this.status); }; orderSchema.methods.markAsShipped = function() { if (this.status !== 'paid') { throw new Error('Order must be paid before shipping'); } this.status = 'shipped'; return this.save(); }; ``` ## 静态方法(Static Methods) 静态方法是添加到模型类上的方法,可以直接在模型上调用,不需要实例化文档。这些方法通常用于查询或批量操作。 ### 定义静态方法 ```javascript // 添加静态方法 userSchema.statics.findByEmail = function(email) { return this.findOne({ email }); }; userSchema.statics.getActiveUsers = function() { return this.find({ status: 'active' }); }; userSchema.statics.countByStatus = function(status) { return this.countDocuments({ status }); }; userSchema.statics.findAdultUsers = function() { return this.find({ age: { $gte: 18 } }); }; // 使用静态方法 const user = await User.findByEmail('john@example.com'); const activeUsers = await User.getActiveUsers(); const activeCount = await User.countByStatus('active'); const adultUsers = await User.findAdultUsers(); ``` ### 静态方法的应用场景 1. **查询操作**:封装常用查询 2. **批量操作**:执行批量更新或删除 3. **统计操作**:计算统计数据 4. **业务规则**:实现业务规则查询 5. **复杂查询**:封装复杂查询逻辑 ```javascript // 示例:产品静态方法 const productSchema = new Schema({ name: String, price: Number, category: String, stock: Number, active: { type: Boolean, default: true } }); productSchema.statics.findByCategory = function(category) { return this.find({ category, active: true }); }; productSchema.statics.findInPriceRange = function(min, max) { return this.find({ price: { $gte: min, $lte: max }, active: true }); }; productSchema.statics.findLowStock = function(threshold = 10) { return this.find({ stock: { $lte: threshold }, active: true }); }; productSchema.statics.updateStock = function(productId, quantity) { return this.findByIdAndUpdate( productId, { $inc: { stock: quantity } }, { new: true } ); }; ``` ## 实例方法 vs 静态方法 ### 区别对比 | 特性 | 实例方法 | 静态方法 | |------|---------|---------| | 调用方式 | `document.method()` | `Model.method()` | | 访问 this | 可以访问文档实例 | 不能访问文档实例 | | 使用场景 | 单文档操作 | 查询和批量操作 | | 定义位置 | `schema.methods` | `schema.statics` | | 返回值 | 通常返回文档或修改后的值 | 通常返回查询结果 | ### 选择指南 **使用实例方法当:** - 需要操作单个文档 - 需要访问文档的属性 - 方法与特定文档相关 - 需要修改文档状态 **使用静态方法当:** - 需要查询多个文档 - 需要执行批量操作 - 方法与文档集合相关 - 不需要访问特定文档 ## 高级用法 ### 异步方法 ```javascript // 异步实例方法 userSchema.methods.sendWelcomeEmail = async function() { const emailService = require('./services/email'); await emailService.send({ to: this.email, subject: 'Welcome!', body: `Hello ${this.firstName}!` }); return this; }; // 异步静态方法 userSchema.statics.sendNewsletter = async function(subject, content) { const users = await this.find({ subscribed: true }); const emailService = require('./services/email'); for (const user of users) { await emailService.send({ to: user.email, subject, body: content }); } return users.length; }; ``` ### 链式调用 ```javascript // 静态方法返回查询构建器 userSchema.statics.queryActive = function() { return this.find({ active: true }); }; // 使用链式调用 const users = await User.queryActive() .select('name email') .sort({ name: 1 }) .limit(10); ``` ### 组合使用 ```javascript // 静态方法查询,实例方法处理 const users = await User.findByEmail('john@example.com'); if (user) { await user.sendWelcomeEmail(); } ``` ## 最佳实践 1. **命名清晰**:使用描述性的方法名 2. **单一职责**:每个方法只做一件事 3. **错误处理**:妥善处理错误情况 4. **文档注释**:为方法添加清晰的注释 5. **类型安全**:使用 TypeScript 或 JSDoc 6. **测试覆盖**:为自定义方法编写测试 7. **避免重复**:不要重复已有的 Mongoose 方法 8. **性能考虑**:注意方法对性能的影响
服务端 · 2月22日 20:12
Mongoose 如何处理文档关联和 Populate 功能?Mongoose 提供了多种方式来处理文档之间的关联关系,包括引用(Reference)、嵌入(Embedding)和 Populate 功能。 ## 关联类型 ### 1. 嵌入式关联(Embedding) 将相关数据直接嵌入到父文档中,适合一对一或一对多关系,且子文档较小的情况。 ```javascript 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 引用其他文档,适合一对多或多对多关系。 ```javascript 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 ```javascript // 创建作者和书籍 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" ``` ### 多字段 Populate ```javascript const 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'); ``` ### 嵌套 Populate ```javascript const 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' } }); ``` ### 选择字段 ```javascript const book = await Book.findById(id) .populate({ path: 'author', select: 'name email' // 只选择特定字段 }); ``` ### 条件 Populate ```javascript const books = await Book.find() .populate({ path: 'author', match: { status: 'active' } // 只填充符合条件的作者 }); ``` ## 多对多关系 ### 使用数组引用 ```javascript 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'); ``` ### 使用中间集合 ```javascript 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 使用虚拟字段创建动态关联: ```javascript 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'); ``` ## 性能优化 1. **选择性 Populate**:只填充需要的字段 2. **限制数量**:使用 `limit` 限制填充的文档数量 3. **分页**:使用 `skip` 和 `limit` 实现分页 4. **避免 N+1 查询**:合理设计数据结构 5. **使用索引**:为引用字段创建索引 ```javascript const books = await Book.find() .populate({ path: 'author', select: 'name', options: { limit: 10 } }); ``` ## 最佳实践 1. 根据数据访问模式选择嵌入或引用 2. 避免过度嵌套和过深的 populate 3. 考虑使用虚拟字段处理复杂关联 4. 为引用字段创建索引以提高查询性能 5. 在多对多关系中考虑使用中间集合 6. 注意 populate 可能导致的性能问题
服务端 · 2月22日 20:12
Mongoose 性能优化有哪些最佳实践?Mongoose 性能优化是开发高效应用的关键。通过合理的配置和最佳实践,可以显著提升查询速度和整体性能。 ## 连接优化 ### 连接池配置 ```javascript mongoose.connect('mongodb://localhost:27017/mydb', { maxPoolSize: 100, // 最大连接数 minPoolSize: 10, // 最小连接数 socketTimeoutMS: 45000, // 套接字超时 serverSelectionTimeoutMS: 5000, // 服务器选择超时 connectTimeoutMS: 10000 // 连接超时 }); ``` ### 连接重用 ```javascript // 在应用启动时建立连接 mongoose.connect('mongodb://localhost:27017/mydb'); // 不要频繁关闭和重新连接 // 避免在每次请求时都创建新连接 ``` ## 索引优化 ### 创建索引 ```javascript 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' }); ``` ### 索引策略 1. 为常用查询字段创建索引 2. 使用复合索引优化多字段查询 3. 避免过多索引影响写入性能 4. 定期分析查询性能,优化索引 ```javascript // 分析查询计划 const query = User.find({ email: 'john@example.com' }); const explanation = await query.explain('executionStats'); console.log(explanation.executionStats); ``` ## 查询优化 ### 使用 lean() ```javascript // 返回普通 JavaScript 对象,性能更好 const users = await User.find().lean(); // 只读查询使用 lean() const users = await User.find({ status: 'active' }).lean(); ``` ### 选择性查询 ```javascript // 只查询需要的字段 const users = await User.find() .select('name email age') .lean(); // 排除大字段 const users = await User.find() .select('-largeField -anotherLargeField'); ``` ### 限制结果数量 ```javascript // 使用 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); ``` ### 使用投影 ```javascript // 投影减少数据传输 const users = await User.find( { status: 'active' }, { name: 1, email: 1, _id: 0 } ); ``` ## 批量操作 ### 批量插入 ```javascript // 使用 insertMany 代替多次 insertOne const users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }, // ... 更多用户 ]); ``` ### 批量更新 ```javascript // 使用 updateMany 代替多次 updateOne await User.updateMany( { status: 'pending' }, { status: 'active' } ); ``` ### 批量删除 ```javascript // 使用 deleteMany 代替多次 deleteOne await User.deleteMany({ status: 'deleted' }); ``` ## 缓存策略 ### 查询缓存 ```javascript 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秒 ``` ### 应用层缓存 ```javascript 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 引用 ```javascript // 嵌入适合一对一或一对多,子文档较小 const userSchema = new Schema({ name: String, profile: { bio: String, avatar: String } }); // 引用适合一对多或多对多,子文档较大 const postSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'User' } }); ``` ### 避免过深嵌套 ```javascript // 避免过深的嵌套结构 // 不推荐 const badSchema = new Schema({ level1: { level2: { level3: { level4: { data: String } } } } }); // 推荐:扁平化结构 const goodSchema = new Schema({ level1: String, level2: String, level3: String, level4: String }); ``` ## 监控和调优 ### 查询性能监控 ```javascript // 启用调试模式 mongoose.set('debug', true); // 自定义调试函数 mongoose.set('debug', (collectionName, method, query, doc) => { console.log(`${collectionName}.${method}`, JSON.stringify(query)); }); ``` ### 慢查询日志 ```javascript // 记录慢查询 mongoose.connection.on('connected', () => { mongoose.connection.db.admin().command({ profile: 1, slowms: 100 // 超过100ms的查询 }); }); ``` ## 最佳实践总结 1. **连接管理**:使用连接池,避免频繁连接断开 2. **索引优化**:为常用查询创建合适的索引 3. **查询优化**:使用 lean()、选择性查询、限制结果 4. **批量操作**:使用批量操作代替多次单条操作 5. **缓存策略**:合理使用查询缓存和应用层缓存 6. **数据模型**:根据访问模式选择嵌入或引用 7. **监控调优**:持续监控查询性能,及时优化 8. **避免 N+1 查询**:合理设计数据结构,避免循环查询
服务端 · 2月22日 20:12
Mongoose 子文档如何使用,有哪些应用场景?Mongoose 子文档(Subdocuments)是嵌套在父文档中的文档,它们可以是单个文档或文档数组。子文档提供了一种组织相关数据的方式,同时保持数据的完整性。 ## 子文档类型 ### 1. 嵌套 Schema(单个子文档) ```javascript 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. 子文档数组 ```javascript 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' } ] }); ``` ## 子文档操作 ### 访问子文档 ```javascript // 访问单个子文档 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!" ``` ### 修改子文档 ```javascript // 修改单个子文档 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(); ``` ### 添加子文档到数组 ```javascript // 添加新评论 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(); ``` ### 删除子文档 ```javascript // 删除数组中的子文档 const post = await Post.findById(postId); post.comments.splice(1, 1); // 删除第二个评论 await post.save(); // 使用 pull 删除符合条件的子文档 post.comments.pull({ author: 'Alice' }); await post.save(); ``` ## 子文档中间件 ### 子文档级别的中间件 ```javascript commentSchema.pre('save', function(next) { console.log('Saving comment:', this.text); next(); }); commentSchema.post('save', function(doc) { console.log('Comment saved:', doc.text); }); ``` ### 父文档中间件 ```javascript postSchema.pre('save', function(next) { console.log('Saving post with', this.comments.length, 'comments'); next(); }); ``` ## 子文档验证 ### 子文档级别的验证 ```javascript 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); } ``` ## 子文档方法 ### 为子文档添加方法 ```javascript commentSchema.methods.getFormattedDate = function() { return this.createdAt.toLocaleDateString(); }; const post = await Post.findById(postId); console.log(post.comments[0].getFormattedDate()); ``` ### 为子文档添加静态方法 ```javascript commentSchema.statics.findByAuthor = function(author) { return this.find({ author }); }; // 注意:子文档的静态方法通常不直接使用 // 更常见的是在父文档上定义方法来操作子文档 ``` ## 子文档引用 ### 使用 ObjectId 引用 ```javascript 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 引用 ### 选择指南 **使用子文档当:** - 数据总是与父文档一起访问 - 子文档数量有限且相对较小 - 需要原子性更新 - 数据不需要独立查询 **使用引用当:** - 子文档可能独立访问 - 子文档数量可能很大 - 需要跨多个文档查询 - 需要更好的性能 ```javascript // 子文档示例 - 适合少量评论 const postSchema = new Schema({ title: String, comments: [commentSchema] }); // 引用示例 - 适合大量评论 const postSchema = new Schema({ title: String, comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }] }); ``` ## 高级用法 ### 子文档数组操作 ```javascript // 使用 $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' } } ); ``` ### 子文档验证器 ```javascript 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'); ``` ## 最佳实践 1. **合理选择结构**:根据访问模式选择子文档或引用 2. **限制数组大小**:避免子文档数组过大 3. **使用验证**:为子文档添加适当的验证规则 4. **考虑性能**:大型子文档数组可能影响性能 5. **使用中间件**:利用中间件处理子文档逻辑 6. **文档清晰**:为子文档 Schema 添加清晰的注释 7. **测试覆盖**:为子文档操作编写测试
服务端 · 2月22日 20:12
Mongoose 和原生 MongoDB 驱动有什么区别?Mongoose 和原生 MongoDB 驱动都是 Node.js 中与 MongoDB 交互的工具,但它们在设计理念、使用方式和适用场景上有显著差异。 ## 主要区别 ### 1. 抽象层次 **Mongoose(ODM - 对象数据模型)** ```javascript 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 驱动** ```javascript 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. 数据验证 **Mongoose** ```javascript const 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 驱动** ```javascript // 没有内置验证,需要手动实现 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. 类型安全 **Mongoose** ```javascript const user = await User.findById(userId); user.age = 'twenty-five'; // 自动转换为数字或报错 await user.save(); ``` **原生 MongoDB 驱动** ```javascript const user = await db.collection('users').findOne({ _id: userId }); user.age = 'twenty-five'; // 不会有类型检查 await db.collection('users').updateOne( { _id: userId }, { $set: user } ); ``` ### 4. 中间件和钩子 **Mongoose** ```javascript userSchema.pre('save', function(next) { this.email = this.email.toLowerCase(); next(); }); userSchema.post('save', function(doc) { console.log('User saved:', doc.email); }); ``` **原生 MongoDB 驱动** ```javascript // 需要手动实现类似功能 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. 查询构建器 **Mongoose** ```javascript const users = await User.find({ age: { $gte: 18 } }) .select('name email') .sort({ name: 1 }) .limit(10) .lean(); ``` **原生 MongoDB 驱动** ```javascript const users = await db.collection('users') .find({ age: { $gte: 18 } }) .project({ name: 1, email: 1 }) .sort({ name: 1 }) .limit(10) .toArray(); ``` ## 性能对比 ### 查询性能 **Mongoose** ```javascript // 有额外的抽象层开销 const users = await User.find({ age: { $gte: 18 } }); ``` **原生 MongoDB 驱动** ```javascript // 直接操作,性能更好 const users = await db.collection('users').find({ age: { $gte: 18 } }).toArray(); ``` ### 批量操作 **Mongoose** ```javascript // 使用 insertMany const users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' } ]); ``` **原生 MongoDB 驱动** ```javascript // 使用 bulkWrite await db.collection('users').bulkWrite([ { insertOne: { document: { name: 'John', email: 'john@example.com' } } }, { insertOne: { document: { name: 'Jane', email: 'jane@example.com' } } } ]); ``` ## 适用场景 ### 使用 Mongoose 当: 1. **需要数据验证**:需要强制数据结构和类型 2. **团队协作**:多人开发,需要统一的接口 3. **快速开发**:需要快速构建原型 4. **复杂业务逻辑**:需要中间件和钩子 5. **类型安全**:使用 TypeScript 时需要类型定义 ```javascript // 适合使用 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 驱动当: 1. **性能关键**:需要最佳性能 2. **灵活的数据结构**:数据结构经常变化 3. **简单操作**:只需要基本的 CRUD 操作 4. **学习 MongoDB**:想深入了解 MongoDB 5. **微服务**:需要轻量级依赖 ```javascript // 适合使用原生驱动的场景 const users = await db.collection('users') .find({ age: { $gte: 18 } }) .project({ name: 1, email: 1 }) .toArray(); ``` ## 迁移指南 ### 从 Mongoose 到原生驱动 ```javascript // Mongoose const user = await User.findById(userId); // 原生驱动 const user = await db.collection('users').findOne({ _id: new ObjectId(userId) }); ``` ### 从原生驱动到 Mongoose ```javascript // 原生驱动 const users = await db.collection('users').find({}).toArray(); // Mongoose const users = await User.find().lean(); ``` ## 混合使用 可以在同一项目中同时使用两者: ```javascript // 使用 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) | 低(直接驱动) | | 数据验证 | 内置 | 需手动实现 | | 类型安全 | 强 | 弱 | | 中间件 | 支持 | 不支持 | | 学习曲线 | 较陡 | 较平 | | 性能 | 较低 | 较高 | | 灵活性 | 较低 | 较高 | | 开发效率 | 高 | 中等 | ## 最佳实践 1. **根据项目需求选择**:考虑团队规模、性能要求、开发速度 2. **可以混合使用**:在不同场景使用最适合的工具 3. **性能测试**:对性能关键路径进行测试 4. **团队共识**:确保团队对选择有共识 5. **文档完善**:为选择提供充分的文档和理由
服务端 · 2月22日 20:12
Mongoose 中间件和钩子如何工作,有哪些应用场景?Mongoose 中间件(Middleware)和钩子(Hooks)是强大的功能,允许在执行某些操作之前或之后执行自定义逻辑。中间件分为两类:文档中间件和查询中间件。 ## 中间件类型 ### 1. 文档中间件(Document Middleware) 在文档实例上执行的操作,如 `save()`、`validate()`、`remove()` 等。 ```javascript 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()` 等。 ```javascript 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`(文档实例或查询对象) ```javascript 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`(文档实例或查询对象) ```javascript userSchema.post('save', function(doc) { console.log('User saved with ID:', doc._id); // 发送通知、记录日志等 }); ``` ## 异步中间件 Mongoose 中间件支持异步操作: ```javascript // 使用 async/await userSchema.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(); }); // 返回 Promise userSchema.pre('save', function() { return checkEmailAvailability(this.email).then(isAvailable => { if (!isAvailable) { throw new Error('Email already exists'); } }); }); ``` ## 实际应用场景 1. **密码哈希**:在保存用户前对密码进行加密 2. **时间戳**:自动设置 createdAt 和 updatedAt 3. **软删除**:在删除前标记为已删除 4. **数据验证**:执行复杂的验证逻辑 5. **日志记录**:记录操作历史 6. **缓存失效**:更新相关缓存 7. **关联数据**:自动更新关联文档 8. **通知发送**:在操作后发送通知 ## 注意事项 1. 中间件按定义顺序执行 2. `pre` 钩子中的错误会中止操作 3. 查询中间件不会触发文档中间件 4. 使用 `findOneAndUpdate` 等方法时,需要设置 `{ runValidators: true }` 来触发验证 5. 中间件中避免无限循环
服务端 · 2月22日 20:12
Mongoose Schema 是什么,如何定义和使用?Mongoose Schema(模式)是 Mongoose 的核心概念,用于定义 MongoDB 文档的结构、数据类型、验证规则和默认值。Schema 本身不是数据库中的集合,而是一个蓝图,用于创建 Model。 ## Schema 的基本定义 ```javascript 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 的主要属性 1. **字段类型**:String、Number、Date、Buffer、Boolean、Mixed、ObjectId、Array 2. **验证器**:required、min、max、enum、match、validate 3. **修饰符**:lowercase、uppercase、trim、default 4. **索引**:unique、sparse、index 5. **虚拟字段**:不存储在数据库中的计算字段 6. **实例方法**:添加到文档实例的方法 7. **静态方法**:添加到模型类的方法 8. **中间件**:pre 和 post 钩子 ## Schema 与 Model 的关系 - Schema 是定义,Model 是构造函数 - 通过 `mongoose.model('User', userSchema)` 创建 Model - Model 的实例是 Document,代表数据库中的实际文档 - 一个 Schema 可以创建多个 Model(不推荐) ## Schema 的优势 1. **数据一致性**:强制文档结构一致 2. **数据验证**:在应用层验证数据 3. **类型安全**:提供类型检查和转换 4. **中间件支持**:可以在操作前后执行逻辑 5. **可扩展性**:可以添加方法和虚拟字段
服务端 · 2月22日 20:12
Mongoose 数据验证有哪些类型,如何实现自定义验证?Mongoose 提供了强大的数据验证功能,可以在保存数据到数据库之前验证数据的完整性和正确性。验证可以在 Schema 层面定义,也可以自定义验证器。 ## 内置验证器 ### 1. 必填验证(required) ```javascript const userSchema = new Schema({ name: { type: String, required: [true, 'Name is required'] }, email: { type: String, required: true } }); ``` ### 2. 类型验证(type) ```javascript const userSchema = new Schema({ age: Number, isActive: Boolean, birthDate: Date }); ``` ### 3. 枚举验证(enum) ```javascript 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) ```javascript 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) ```javascript 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) ```javascript 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) ```javascript const userSchema = new Schema({ email: { type: String, unique: true, index: true } }); ``` ### 8. 默认值(default) ```javascript const userSchema = new Schema({ status: { type: String, default: 'active' }, createdAt: { type: Date, default: Date.now } }); ``` ## 自定义验证器 ### 单字段验证器 ```javascript const userSchema = new Schema({ password: { type: String, validate: { validator: function(v) { return v.length >= 8; }, message: 'Password must be at least 8 characters long' } } }); ``` ### 异步验证器 ```javascript 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' } } }); ``` ### 多字段验证器 ```javascript 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()` - 同步验证时 ```javascript 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" } ``` ## 跳过验证 在某些情况下,可以跳过验证: ```javascript // 跳过验证保存 await user.save({ validateBeforeSave: false }); // 跳过验证更新 await User.findByIdAndUpdate(id, { age: 25 }, { runValidators: false }); ``` ## 验证错误处理 ```javascript 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}`); }); } } ``` ## 最佳实践 1. 在 Schema 层面定义验证规则 2. 提供清晰的错误消息 3. 使用异步验证器检查唯一性 4. 在前端和后端都进行验证 5. 考虑性能影响,避免过于复杂的验证 6. 使用自定义验证器处理业务逻辑 7. 记录验证失败的情况
服务端 · 2月22日 20:12