首页 / NestJS 入门教程 / MongoDB 集成

NestJS 入门教程

MongoDB 集成

本教程共 47 篇 · 第 28 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJSMongoDBMongooseNoSQLSchema文档数据库

本节目标:在 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 会自动添加 createdAtupdatedAt 字段
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< / <=
$inIN
$regexLIKE
$orOR
$andAND

分页

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;
  });
});

虚拟属性

虚拟属性是不存到数据库里的计算属性。比如把 firstNamelastName 拼成 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 里释放。

Note

MongoDB 事务只在副本集(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__vpassword 会被去掉。

多数据库连接

有些项目需要连多个 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 区分

下一章学文件上传,这是后端开发中另一个常见需求。