Mongoose
Mongoose 是一个面向 MongoDB 数据库的对象数据模型(ODM)库,用于在 Node.js 环境下建模和操作 MongoDB 文档结构。它提供了一些方便的特性,如数据验证、查询构建、业务逻辑钩子(hooks)和中间件,使得处理 MongoDB 文档更加直观和安全。

查看更多相关内容
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