MongoDB 集成
本教程共 47 篇 · 第 28 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:在 NestJS 中接入 MongoDB,学会用 Mongoose 定义 Schema、注入 Model、做 CRUD 和关联查询。
前面两章学的 TypeORM 和 Prisma 都是关系型数据库的 ORM。但有时候你不需要严格的表结构,数据格式灵活多变——这时候 NoSQL 数据库就派上用场了。
MongoDB 是最流行的文档数据库之一。NestJS 通过 @nestjs/mongoose 包跟它集成,用的是 Mongoose 这个对象建模工具。
Mongoose 的作用类似于 ORM,但它不叫 ORM(因为 MongoDB 里不是”表”和”行”,而是”集合”和”文档”)。它做的事情差不多:定义数据结构、验证、查询、关联。
安装依赖
npm install @nestjs/mongoose mongoose
就这两个包。@nestjs/mongoose 是 NestJS 的集成层,mongoose 是 Mongoose 本体。
配置连接
在根模块里用 MongooseModule.forRoot() 连接数据库:
// app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/nest'),
],
})
export class AppModule {}
连接字符串的格式是 mongodb://host:port/database。
用环境变量配置
实际项目不会写死连接地址。用 forRootAsync() 从配置服务读取:
// app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
MongooseModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
uri: configService.get<string>('MONGODB_URI'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
.env 文件里配置:
MONGODB_URI=mongodb://localhost:27017/nest
如果是带认证的 MongoDB Atlas 云数据库,URI 格式类似:
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/nest?retryWrites=true
定义 Schema
Mongoose 里一切从 Schema 开始。Schema 定义了集合里文档的结构。@nestjs/mongoose 提供了装饰器来定义 Schema,写起来跟定义 TypeScript 类差不多。
基本 Schema
// schemas/user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema({ timestamps: true })
export class User {
@Prop({ required: true })
name: string;
@Prop({ required: true, unique: true })
email: string;
@Prop({ required: true, select: false })
password: string;
@Prop({ default: true })
isActive: boolean;
}
export const UserSchema = SchemaFactory.createForClass(User);
几个关键点:
@Schema()把类标记为 Schema 定义,默认会映射到同名集合(加 s),比如User->users@Prop()定义文档中的一个字段SchemaFactory.createForClass()把装饰器类转成 Mongoose 能用的 Schema 对象timestamps: true会自动添加createdAt和updatedAt字段
Tip
HydratedDocument<User>是 Mongoose 推荐的文档类型定义,比直接extends Document更干净。UserDocument表示数据库里取出来的完整文档对象。
属性选项
@Prop() 可以传很多选项:
@Prop({
required: true, // 必填
unique: true, // 唯一
lowercase: true, // 自动转小写
trim: true, // 自动去首尾空格
minlength: 6, // 最小长度
maxlength: 100, // 最大长度
default: 'active', // 默认值
select: false, // 查询时默认不包含
index: true, // 建索引
})
email: string;
嵌套文档
MongoDB 是文档数据库,支持嵌套结构。比如用户有个地址字段,不需要单独建表:
class Address {
@Prop()
street: string;
@Prop()
city: string;
@Prop()
country: string;
}
@Schema()
export class User {
@Prop()
name: string;
@Prop({ type: Address })
address: Address;
}
嵌套文档直接嵌在父文档里,不存在单独的集合中。
数组字段
TypeScript 的数组类型不能自动推断,需要显式指定:
@Prop([String])
tags: string[];
引用其他集合
MongoDB 里也有”关联”的概念,不过不是 JOIN,而是引用:
import * as mongoose from 'mongoose';
@Schema()
export class Post {
@Prop({ required: true })
title: string;
@Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
author: User;
}
ref: 'User' 表示这个字段引用 User 集合的文档。查询时用 populate() 把引用展开成完整数据。
注册 Schema 到模块
跟 TypeORM 的 forFeature 类似,Mongoose 也用 forFeature 注册当前模块用到的模型:
// users.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './schemas/user.schema';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [
MongooseModule.forFeature([
{ name: User.name, schema: UserSchema },
]),
],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
name: User.name 就是 'User',Mongoose 用这个名字来标识模型。
注入 Model 并使用
在 Service 里通过 @InjectModel() 拿到 Model 实例:
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './schemas/user.schema';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<User>,
) {}
}
Model<User> 就是操作 users 集合的入口。
创建
async create(createUserDto: CreateUserDto): Promise<User> {
const createdUser = new this.userModel(createUserDto);
return createdUser.save();
}
// 批量创建
async createMany(users: CreateUserDto[]): Promise<User[]> {
return this.userModel.insertMany(users);
}
new this.userModel(data) 创建一个文档实例,.save() 写入数据库。
查询
// 查所有
async findAll(): Promise<User[]> {
return this.userModel.find().exec();
}
// 按 ID 查
async findOne(id: string): Promise<User | null> {
return this.userModel.findById(id).exec();
}
// 按条件查
async findByEmail(email: string): Promise<User | null> {
return this.userModel.findOne({ email }).exec();
}
// 条件 + 排序
async findActive(): Promise<User[]> {
return this.userModel
.find({ isActive: true })
.sort({ createdAt: -1 })
.exec();
}
Note所有 Mongoose 查询方法都要加
.exec()才会真正执行。不加.exec()返回的是 Query 对象,不是 Promise。很多人忘了加,导致返回结果不对。
更新
async update(id: string, updateUserDto: UpdateUserDto): Promise<User | null> {
return this.userModel
.findByIdAndUpdate(id, updateUserDto, { new: true })
.exec();
}
{ new: true } 表示返回更新后的文档。不加的话返回的是更新前的旧数据。
删除
async remove(id: string): Promise<User | null> {
return this.userModel.findByIdAndDelete(id).exec();
}
// 批量删除
async deleteMany(ids: string[]): Promise<any> {
return this.userModel.deleteMany({ _id: { $in: ids } }).exec();
}
关联查询(populate)
MongoDB 的关联不像 SQL 的 JOIN。数据存的是引用(ObjectId),查询时需要用 populate() 把引用展开:
// 查文章时连带查出作者信息
async findAllWithAuthor() {
return this.postModel
.find()
.populate('author')
.exec();
}
// 只展开特定字段
async findAllWithAuthorName() {
return this.postModel
.find()
.populate('author', 'name email')
.exec();
}
// 嵌套 populate
async findAllDeep() {
return this.postModel
.find()
.populate({
path: 'author',
populate: { path: 'profile' },
})
.exec();
}
populate() 本质上还是两次查询——先查主文档,再根据 ObjectId 去查关联文档。跟 SQL 的 JOIN 在数据库层面完成不同。
条件查询
MongoDB 的查询语法跟 SQL 差异较大,用的是对象式的操作符。
常用操作符
// 正则搜索
async search(keyword: string) {
return this.userModel
.find({
$or: [
{ name: { $regex: keyword, $options: 'i' } },
{ email: { $regex: keyword, $options: 'i' } },
],
})
.exec();
}
// 范围查询
async findByAgeRange(min: number, max: number) {
return this.userModel
.find({
age: { $gte: min, $lte: max },
})
.exec();
}
// 包含查询
async findByIds(ids: string[]) {
return this.userModel
.find({ _id: { $in: ids } })
.exec();
}
常用操作符对照:
| MongoDB 操作符 | 等价 SQL |
|---|---|
$eq | = |
$ne | != |
$gt / $gte | > / >= |
$lt / $lte | < / <= |
$in | IN |
$regex | LIKE |
$or | OR |
$and | AND |
分页
async findWithPagination(page: number, limit: number) {
const skip = (page - 1) * limit;
const [data, total] = await Promise.all([
this.userModel.find().skip(skip).limit(limit).exec(),
this.userModel.countDocuments().exec(),
]);
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
skip 跳过,limit 限制条数。用 Promise.all 并行查数据和总数,效率更高。
聚合查询
MongoDB 的聚合管道(Aggregation Pipeline)类似 SQL 的 GROUP BY,但更强大:
async getStatsByRole() {
return this.userModel.aggregate([
{ $match: { isActive: true } },
{ $group: { _id: '$role', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]);
}
$match 过滤,$group 分组,$sort 排序。管道可以叠加很多阶段,做复杂的数据分析。
中间件(Hooks)
Mongoose 的中间件可以在特定操作前后执行自定义逻辑。比如保存前加密密码:
// 在 Schema 定义后添加中间件
UserSchema.pre<User>('save', function (next) {
if (this.isModified('password')) {
this.password = hashPassword(this.password);
}
next();
});
pre('save') 在 save() 之前执行。this.isModified('password') 判断密码字段是否被修改,避免重复加密。
Warning在 NestJS 里,中间件必须在模型注册之前添加。如果用
forFeatureAsync(),可以在工厂函数里添加:MongooseModule.forFeatureAsync([ { name: User.name, useFactory: () => { const schema = UserSchema; schema.pre('save', function () { // 保存前逻辑 }); return schema; }, }, ])
查询后中间件也很有用,比如自动去掉密码字段:
UserSchema.post<User>('find', function (docs) {
docs.forEach(doc => {
doc.password = undefined;
});
});
虚拟属性
虚拟属性是不存到数据库里的计算属性。比如把 firstName 和 lastName 拼成 fullName:
import { Virtual } from '@nestjs/mongoose';
@Schema()
export class Person {
@Prop()
firstName: string;
@Prop()
lastName: string;
@Virtual({
get: function (this: Person) {
return `${this.firstName} ${this.lastName}`;
},
})
fullName: string;
}
fullName 不会存到 MongoDB,但查询结果里会自动计算出来。
索引
MongoDB 的查询性能依赖索引。在 Schema 上定义索引:
// 单字段索引
@Prop({ index: true })
email: string;
// 复合索引
UserSchema.index({ name: 1, email: 1 }, { unique: true });
// 文本索引(全文搜索)
UserSchema.index({ name: 'text', bio: 'text' });
文本索引建好后,可以用 $text 操作符做全文搜索:
const results = await this.userModel
.find({ $text: { $search: 'keyword' } })
.exec();
事务处理
MongoDB 4.0+ 支持多文档事务(需要副本集或分片集群)。在 NestJS 里通过注入 Connection 来使用:
import { Injectable } from '@nestjs/common';
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
import { Connection, Model } from 'mongoose';
import { User } from './schemas/user.schema';
@Injectable()
export class UsersService {
constructor(
@InjectModel(User.name) private userModel: Model<User>,
@InjectConnection() private connection: Connection,
) {}
async createWithTransaction(userData: any) {
const session = await this.connection.startSession();
session.startTransaction();
try {
const user = await this.userModel.create([userData], { session });
// 其他操作...
await session.commitTransaction();
return user;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
}
@InjectConnection() 注入 Mongoose 的 Connection 对象,通过它创建 session、开启事务。模式跟 TypeORM 类似:try 里提交,catch 里回滚,finally 里释放。
NoteMongoDB 事务只在副本集(Replica Set)或分片集群上可用。本地开发如果是单节点,默认不支持事务。可以用
mongod --replSet rs0启动单节点副本集来测试。
数据转换
MongoDB 返回的文档有 _id 和 __v 字段,前端通常不需要。可以在 Schema 上配置转换:
UserSchema.set('toJSON', {
transform: (doc, ret) => {
ret.id = ret._id.toString();
delete ret._id;
delete ret.__v;
delete ret.password;
return ret;
},
});
这样每次返回 JSON 时,_id 会变成 id,__v 和 password 会被去掉。
多数据库连接
有些项目需要连多个 MongoDB 数据库。给每个连接起个名字就行:
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionName: 'cats',
}),
MongooseModule.forRoot('mongodb://localhost/users', {
connectionName: 'users',
}),
],
})
export class AppModule {}
在 forFeature 和 @InjectModel 里指定连接名:
MongooseModule.forFeature(
[{ name: Cat.name, schema: CatSchema }],
'cats', // 指定连接
)
constructor(
@InjectModel(Cat.name, 'cats') private catModel: Model<Cat>,
) {}
测试时的 Mock
用 getModelToken() 生成注入令牌,替换成 mock 对象:
import { getModelToken } from '@nestjs/mongoose';
import { User } from './schemas/user.schema';
const mockUserModel = {
find: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue([]) }),
findById: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue(null) }),
create: jest.fn().mockResolvedValue({}),
findByIdAndUpdate: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue({}) }),
findByIdAndDelete: jest.fn().mockReturnValue({ exec: jest.fn().mockResolvedValue({}) }),
};
@Module({
providers: [
UsersService,
{
provide: getModelToken(User.name),
useValue: mockUserModel,
},
],
})
export class UsersModule {}
Tip注意 mock 的结构要跟 Mongoose 的链式调用匹配。
find()返回的对象要有exec()方法,不然测试会报exec is not a function。
小结
MongoDB 跟 NestJS 的集成通过 @nestjs/mongoose 完成。核心流程:forRoot 建连接、用装饰器定义 Schema、forFeature 注册模型、@InjectModel 注入使用。
关键知识点回顾:
- Mongoose 用 Schema 定义文档结构,
@Schema()和@Prop()是核心装饰器 - 所有查询方法要加
.exec()返回 Promise - 关联用
ref定义引用,查询时用populate()展开 - 中间件(pre/post hooks)在 Schema 注册前添加
- MongoDB 事务需要副本集环境
toJSON转换可以统一处理_id、__v等字段- 多连接用
connectionName区分
下一章学文件上传,这是后端开发中另一个常见需求。