【NestJS】16 MongoDB + Mongoose 实战(上)
1817 字
9 分钟
【NestJS】16 MongoDB + Mongoose 实战(上)
深入理解 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 文档,随时添加字段
// 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 快速上手
安装与配置
pnpm add @nestjs/mongoose mongoose@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost:27017/mydb', { autoIndex: true, maxPoolSize: 10, }), ],})export class AppModule {}创建 Schema
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';import { Document } from 'mongoose';
@Schema({ timestamps: true }) // 自动添加 createdAt/updatedAtexport 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);注册到模块
@Module({ imports: [ MongooseModule.forFeature([ { name: User.name, schema: UserSchema } ]), ], providers: [UserService], controllers: [UserController],})export class UserModule {}Service 使用
@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 设计核心
字段验证
@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;}嵌套文档
// 定义子 Schema@Schema({ _id: false }) // 不生成单独的 _idexport 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[]; // 数组}存储结果:
{ "username": "alice", "profile": { "bio": "Developer", "phone": "1234567890" }, "tags": ["nodejs", "mongodb"]}索引设计
为什么需要索引?
- 无索引:扫描所有数据(慢)
- 有索引:直接定位(快)
@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 = 降序使用场景:
// 经常这样查询?→ 需要索引find({ status: 'active' }).sort({ createdAt: -1 })// 创建索引:{ status: 1, createdAt: -1 }四、查询操作
基础查询
// 查询所有await this.userModel.find();
// 条件查询await this.userModel.find({ status: 'active' });
// 根据 IDawait this.userModel.findById(userId);
// 查询一条await this.userModel.findOne({ username: 'alice' });查询操作符
// 比较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'] } }) // 包含所有排序与分页
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), }, };}聚合查询
// 统计每个状态的用户数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)
适用场景:数据经常更新、一对多关系
// 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 查询:
// 自动关联查询const interview = await this.interviewModel .findById(id) .populate('userId') // 自动填充用户信息 .exec();
// 只返回部分字段.populate('userId', 'username email')
// 多层关联.populate({ path: 'userId', populate: { path: 'companyId' }})优缺点:
- ✅ 数据不重复,节省空间
- ✅ 更新方便(只改一处)
- ❌ 需要多次查询
嵌入文档(Embedding)
适用场景:数据小且不常变化、总是一起查询
@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;}优缺点:
- ✅ 查询快(一次获取所有数据)
- ✅ 代码简单
- ❌ 数据重复
- ❌ 更新麻烦(需更新所有相关文档)
混合方案
@Schema()export class Interview extends Document { @Prop({ type: Schema.Types.ObjectId, ref: 'User', }) userId: string; // 引用:获取最新数据
@Prop({ type: UserInfo }) userSnapshot: UserInfo; // 嵌入:快速显示
@Prop() position: string;}优势:查询快 + 数据新
六、完整示例
@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 });@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)
- ✅ 嵌套文档和数组
- ✅ 索引优化查询
查询操作:
- ✅ 基础查询和操作符
- ✅ 排序、分页、字段选择
- ✅ 聚合统计
数据关联:
- ✅ 引用:数据不重复,适合常更新
- ✅ 嵌入:查询快,适合不常变化
- ✅ 混合方案:兼顾性能和灵活性
下篇预告
中间件与钩子:自动加密密码、数据验证 性能优化:索引设计、查询分析、常见问题 生产实践:连接池、错误处理、监控备份
参考资源:
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
【NestJS】16 MongoDB + Mongoose 实战(上)
https://blog.fridolph.top/posts/2026-01-05__nest16-mongo1/ 相关文章 智能推荐
1
【NestJS】17 MongoDB 性能优化与生产环境最佳实践
NestJS 2026-01-06
2
【NestJS】19 用户认证系统设计与实践总结
NestJS 2026-02-05
3
【NestJS】18 项目架构全景图:从零散知识到完整体系
NestJS 2026-02-01
4
【NestJS】15 最佳实践与学习总结
NestJS 2026-01-20
5
「部署认知」一、前端转全栈必会的 Docker + CI/CD 20% 核心
工程化 2026-04-17
随机文章 随机推荐