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

面试题手册

Mongoose 鉴别器(Discriminators)如何使用?

Mongoose Discriminators(鉴别器)是一种模式继承机制,允许你在同一个集合中存储不同类型的文档,同时保持各自独特的字段和验证规则。这对于处理具有共同基础但又有特定差异的数据模型非常有用。基本概念创建基础 Schemaconst eventSchema = new Schema({ name: { type: String, required: true }, date: { type: Date, required: true }, location: String}, { discriminatorKey: 'kind' // 用于区分不同类型的字段});const Event = mongoose.model('Event', eventSchema);创建鉴别器// 创建会议类型的鉴别器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);使用鉴别器创建文档// 创建基础事件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});查询文档// 查询所有事件const allEvents = await Event.find();// 查询特定类型的事件const conferences = await Conference.find();const meetups = await Meetup.find();// 使用 discriminatorKey 查询const conferences2 = await Event.find({ kind: 'Conference' });嵌套鉴别器在子文档中使用鉴别器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);鉴别器中间件为鉴别器添加中间件// 为会议添加中间件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 的中间件// 基础 Schema 的中间件会应用到所有鉴别器eventSchema.pre('save', function(next) { console.log('Saving event:', this.name); next();});鉴别器方法为鉴别器添加方法// 为会议添加方法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());鉴别器验证为鉴别器添加验证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. 内容管理系统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. 用户角色系统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. 订单系统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 嵌入文档选择指南使用鉴别器当:需要在同一个集合中查询所有类型不同类型有大量共同字段需要统一的索引和查询类型数量相对较少使用嵌入文档当:每种类型有完全不同的结构不需要跨类型查询需要更好的性能隔离类型数量很多最佳实践合理设计基础 Schema:基础 Schema 应包含所有类型的共同字段使用清晰的 discriminatorKey:选择有意义的字段名来区分类型为鉴别器添加验证:确保每种类型的数据完整性利用中间件:为不同类型添加特定的业务逻辑考虑性能:鉴别器在同一个集合中,可能影响查询性能文档清晰:为每个鉴别器添加清晰的注释测试覆盖:为每种鉴别器编写测试
阅读 0·2月22日 20:12

Mongoose Model 有哪些常用的 CRUD 操作方法?

Mongoose Model 是由 Schema 编译而成的构造函数,用于创建和操作 MongoDB 文档。Model 实例代表数据库中的文档,并提供了丰富的 CRUD 操作方法。创建 Modelconst mongoose = require('mongoose');const userSchema = new mongoose.Schema({ name: String, email: String, age: Number});// 创建 Model,第一个参数是集合名称(会自动转为复数)const User = mongoose.model('User', userSchema);Model 的主要方法创建文档// 方法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:使用 insertManyconst users = await User.insertMany([ { name: 'John', email: 'john@example.com' }, { name: 'Jane', email: 'jane@example.com' }]);查询文档// 查找所有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);更新文档// 更新单个文档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' });// findOneAndUpdateconst user = await User.findOneAndUpdate( { email: 'john@example.com' }, { age: 25 }, { new: true });删除文档// 按 ID 删除const user = await User.findByIdAndDelete('507f1f77bcf86cd799439011');// 条件删除const result = await User.deleteOne({ email: 'john@example.com' });// 批量删除const result = await User.deleteMany({ age: { $lt: 18 } });// findOneAndDeleteconst user = await User.findOneAndDelete({ email: 'john@example.com' });统计文档const count = await User.countDocuments({ age: { $gte: 18 } });const count = await User.estimatedDocumentCount(); // 快速估算Model 的静态方法可以在 Schema 上添加自定义静态方法:userSchema.statics.findByEmail = function(email) { return this.findOne({ email });};const User = mongoose.model('User', userSchema);const user = await User.findByEmail('john@example.com');
阅读 0·2月22日 20:12

Mongoose 实例方法和静态方法有什么区别?

Mongoose 提供了实例方法和静态方法两种方式来扩展模型的功能。理解这两种方法的区别和使用场景对于编写可维护的代码非常重要。实例方法(Instance Methods)实例方法是添加到文档实例上的方法,可以在单个文档上调用。这些方法可以访问 this 关键字来引用当前文档。定义实例方法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/falseconst isMatch = await user.comparePassword('password123');实例方法的应用场景文档特定操作:对单个文档执行操作数据验证:验证文档数据数据转换:转换文档数据格式业务逻辑:封装业务逻辑状态检查:检查文档状态// 示例:订单实例方法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)静态方法是添加到模型类上的方法,可以直接在模型上调用,不需要实例化文档。这些方法通常用于查询或批量操作。定义静态方法// 添加静态方法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();静态方法的应用场景查询操作:封装常用查询批量操作:执行批量更新或删除统计操作:计算统计数据业务规则:实现业务规则查询复杂查询:封装复杂查询逻辑// 示例:产品静态方法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 || 返回值 | 通常返回文档或修改后的值 | 通常返回查询结果 |选择指南使用实例方法当:需要操作单个文档需要访问文档的属性方法与特定文档相关需要修改文档状态使用静态方法当:需要查询多个文档需要执行批量操作方法与文档集合相关不需要访问特定文档高级用法异步方法// 异步实例方法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;};链式调用// 静态方法返回查询构建器userSchema.statics.queryActive = function() { return this.find({ active: true });};// 使用链式调用const users = await User.queryActive() .select('name email') .sort({ name: 1 }) .limit(10);组合使用// 静态方法查询,实例方法处理const users = await User.findByEmail('john@example.com');if (user) { await user.sendWelcomeEmail();}最佳实践命名清晰:使用描述性的方法名单一职责:每个方法只做一件事错误处理:妥善处理错误情况文档注释:为方法添加清晰的注释类型安全:使用 TypeScript 或 JSDoc测试覆盖:为自定义方法编写测试避免重复:不要重复已有的 Mongoose 方法性能考虑:注意方法对性能的影响
阅读 0·2月22日 20:12

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