深入理解 MongoDB 数据库设计,掌握 Mongoose ODM 框架,学会在 NestJS 中构建高效的数据层。
前言 ✨
在前面的章节中,我们学习了 NestJS 框架的核心概念。现在,是时候为我们的应用添加数据持久化能力了。
这篇文章将带你:
- 🎯 理解 MongoDB 的核心概念:为什么选择 NoSQL
- 📐 掌握 Mongoose ODM:在 NestJS 中优雅地操作数据库
- 🔍 学会 Schema 设计:构建灵活且健壮的数据模型
- 🚀 实战查询与关联:从简单查询到复杂聚合
📌 版本信息
- NestJS: 11.0.1
- Mongoose: 8.16.5
- @nestjs/mongoose: 11.0.3
- Node.js: 20.x
让我们开始吧!
一、为什么选择 MongoDB?
NoSQL vs SQL:核心差异
SQL(关系型):固定表结构,修改需要 ALTER TABLE MongoDB(文档型):灵活的 JSON 文档,随时添加字段
javascript
// MongoDB 文档示例
{
"_id": ObjectId("..."),
"username": "alice",
"email": "alice@example.com",
"tags": ["developer", "nodejs"], // 数组
"profile": { // 嵌套对象
"bio": "Full-stack developer"
}
}核心优势
| 特性 | 说明 |
|---|---|
| 灵活 Schema | 同一集合可以有不同结构 |
| 原生 JSON | 与 JS/TS 完美契合 |
| 水平扩展 | 轻松应对数据增长 |
| 开发效率 | 代码即文档结构 |
基础概念
| SQL | MongoDB |
|---|---|
| 数据库 | 数据库 |
| 表 | 集合 (Collection) |
| 行 | 文档 (Document) |
| 列 | 字段 (Field) |
| 主键 | _id (自动生成) |
二、Mongoose ODM 快速上手
安装与配置
bash
pnpm add @nestjs/mongoose mongoosetypescript
// app.module.ts
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/mydb', {
autoIndex: true,
maxPoolSize: 10,
}),
],
})
export class AppModule {}创建 Schema
typescript
// user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@Schema({ timestamps: true }) // 自动添加 createdAt/updatedAt
export class User extends Document {
@Prop({ required: true, unique: true })
username: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true })
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);注册到模块
typescript
// user.module.ts
@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema }
]),
],
providers: [UserService],
controllers: [UserController],
})
export class UserModule {}Service 使用
typescript
// user.service.ts
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<User>,
) {}
async create(dto: CreateUserDto) {
return this.userModel.create(dto);
}
async findAll() {
return this.userModel.find().exec();
}
async findById(id: string) {
return this.userModel.findById(id).exec();
}
}三、Schema 设计核心
字段验证
typescript
@Schema()
export class User extends Document {
@Prop({
required: [true, '用户名不能为空'],
unique: true,
lowercase: true, // 自动转小写
trim: true, // 去除空格
minlength: 3,
maxlength: 20,
match: /^[a-zA-Z0-9]+$/, // 正则验证
})
username: string;
@Prop({
type: String,
enum: ['active', 'inactive', 'banned'], // 枚举
default: 'active',
})
status: string;
@Prop({ type: Number, min: 0, max: 150 })
age?: number;
}嵌套文档
typescript
// 定义子 Schema
@Schema({ _id: false }) // 不生成单独的 _id
export class Profile {
@Prop() bio?: string;
@Prop() phone?: string;
@Prop() avatar?: string;
}
const ProfileSchema = SchemaFactory.createForClass(Profile);
// 在主 Schema 中使用
@Schema()
export class User extends Document {
@Prop() username: string;
@Prop({ type: ProfileSchema })
profile?: Profile; // 嵌套对象
@Prop({ type: [String], default: [] })
tags: string[]; // 数组
}存储结果:
javascript
{
"username": "alice",
"profile": {
"bio": "Developer",
"phone": "1234567890"
},
"tags": ["nodejs", "mongodb"]
}索引设计
为什么需要索引?
- 无索引:扫描所有数据(慢)
- 有索引:直接定位(快)
typescript
@Schema()
export class User extends Document {
@Prop({ index: true }) // 单字段索引
username: string;
@Prop({ unique: true }) // 唯一索引
email: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
// 复合索引(多字段)
UserSchema.index({ status: 1, createdAt: -1 });
// 1 = 升序, -1 = 降序使用场景:
typescript
// 经常这样查询?→ 需要索引
find({ status: 'active' }).sort({ createdAt: -1 })
// 创建索引:{ status: 1, createdAt: -1 }四、查询操作
基础查询
typescript
// 查询所有
await this.userModel.find();
// 条件查询
await this.userModel.find({ status: 'active' });
// 根据 ID
await this.userModel.findById(userId);
// 查询一条
await this.userModel.findOne({ username: 'alice' });查询操作符
typescript
// 比较
find({ age: { $gt: 25 } }) // 大于
find({ age: { $gte: 20, $lte: 30 } }) // 范围
// IN 查询
find({ username: { $in: ['alice', 'bob'] } })
// 正则
find({ email: { $regex: '@gmail.com$' } })
// 数组
find({ tags: 'developer' }) // 包含某个值
find({ tags: { $all: ['nodejs', 'mongodb'] } }) // 包含所有排序与分页
typescript
async findPaginated(page = 1, pageSize = 10) {
const skip = (page - 1) * pageSize;
const [data, total] = await Promise.all([
this.userModel
.find()
.skip(skip)
.limit(pageSize)
.select('-password') // 排除密码字段
.sort({ createdAt: -1 })
.exec(),
this.userModel.countDocuments(),
]);
return {
data,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
}聚合查询
typescript
// 统计每个状态的用户数
await this.userModel.aggregate([
{
$group: {
_id: '$status', // 按 status 分组
count: { $sum: 1 }, // 计数
avgAge: { $avg: '$age' } // 平均年龄
}
},
{ $sort: { count: -1 } } // 按数量降序
]);
// 结果:
// [
// { _id: 'active', count: 150, avgAge: 28 },
// { _id: 'inactive', count: 30, avgAge: 35 }
// ]常用聚合阶段:
$match:筛选(放最前面,可使用索引)$group:分组统计$project:选择字段$sort:排序$limit / $skip:分页
五、数据关联设计
引用关联(Reference)
适用场景:数据经常更新、一对多关系
typescript
// Interview Schema
@Schema()
export class Interview extends Document {
@Prop() position: string;
@Prop({
type: Schema.Types.ObjectId,
ref: 'User', // 引用 User
})
userId: string; // 只存 ID
@Prop() score: number;
}使用 populate 查询:
typescript
// 自动关联查询
const interview = await this.interviewModel
.findById(id)
.populate('userId') // 自动填充用户信息
.exec();
// 只返回部分字段
.populate('userId', 'username email')
// 多层关联
.populate({
path: 'userId',
populate: { path: 'companyId' }
})优缺点:
- ✅ 数据不重复,节省空间
- ✅ 更新方便(只改一处)
- ❌ 需要多次查询
嵌入文档(Embedding)
适用场景:数据小且不常变化、总是一起查询
typescript
@Schema({ _id: false })
export class UserInfo {
@Prop() username: string;
@Prop() email: string;
}
@Schema()
export class Interview extends Document {
@Prop() position: string;
@Prop({ type: UserInfo })
user: UserInfo; // 直接嵌入
@Prop() score: number;
}优缺点:
- ✅ 查询快(一次获取所有数据)
- ✅ 代码简单
- ❌ 数据重复
- ❌ 更新麻烦(需更新所有相关文档)
混合方案
typescript
@Schema()
export class Interview extends Document {
@Prop({
type: Schema.Types.ObjectId,
ref: 'User',
})
userId: string; // 引用:获取最新数据
@Prop({ type: UserInfo })
userSnapshot: UserInfo; // 嵌入:快速显示
@Prop() position: string;
}优势:查询快 + 数据新
六、完整示例
typescript
// user.schema.ts
@Schema({
timestamps: true,
toJSON: {
transform: (doc, ret) => {
delete ret.password; // 不返回密码
return ret;
}
},
})
export class User extends Document {
@Prop({ required: true, unique: true, index: true })
username: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true })
password: string;
@Prop({ type: [String], default: [] })
tags: string[];
@Prop({
type: String,
enum: ['active', 'inactive', 'banned'],
default: 'active',
})
status: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
UserSchema.index({ status: 1, createdAt: -1 });typescript
// user.service.ts
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<User>,
) {}
async create(dto: CreateUserDto) {
// 检查是否已存在
const existing = await this.userModel.findOne({
$or: [{ username: dto.username }, { email: dto.email }],
});
if (existing) {
throw new ConflictException('用户已存在');
}
return this.userModel.create(dto);
}
async findAll(page = 1, pageSize = 10) {
const skip = (page - 1) * pageSize;
const [data, total] = await Promise.all([
this.userModel
.find()
.skip(skip)
.limit(pageSize)
.select('-password')
.sort({ createdAt: -1 })
.exec(),
this.userModel.countDocuments(),
]);
return { data, total, page, pageSize };
}
async findById(id: string) {
const user = await this.userModel
.findById(id)
.select('-password')
.exec();
if (!user) {
throw new NotFoundException('用户不存在');
}
return user;
}
async update(id: string, dto: UpdateUserDto) {
return this.userModel
.findByIdAndUpdate(id, dto, { new: true })
.select('-password')
.exec();
}
async remove(id: string) {
await this.userModel.findByIdAndDelete(id);
}
}七、总结
核心知识点
MongoDB 基础:
- ✅ 灵活的文档结构
- ✅ 与 JavaScript 完美契合
- ✅ 集合、文档、字段概念
Mongoose ODM:
- ✅ Schema 定义数据结构
- ✅ 自动验证和类型转换
- ✅ 在 NestJS 中集成
Schema 设计:
- ✅ 字段验证(required、unique、enum)
- ✅ 嵌套文档和数组
- ✅ 索引优化查询
查询操作:
- ✅ 基础查询和操作符
- ✅ 排序、分页、字段选择
- ✅ 聚合统计
数据关联:
- ✅ 引用:数据不重复,适合常更新
- ✅ 嵌入:查询快,适合不常变化
- ✅ 混合方案:兼顾性能和灵活性
下篇预告
中间件与钩子:自动加密密码、数据验证 性能优化:索引设计、查询分析、常见问题 生产实践:连接池、错误处理、监控备份
参考资源:
这是关于赞助的一些描述
- 本文链接:https://fridolph.top/posts/2026-01-05__nest16-mongo1
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。