NestJS MongoDB集成 #

MongoDB简介 #

MongoDB是一个基于文档的NoSQL数据库,使用BSON格式存储数据。Mongoose是MongoDB的对象建模工具,提供了Schema定义、数据验证、查询构建等功能。

安装依赖 #

bash
npm install @nestjs/mongoose mongoose
npm install -D @types/mongoose

配置Mongoose #

基本配置 #

typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost:27017/nest'),
  ],
})
export class AppModule {}

使用环境变量 #

typescript
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'),
        useNewUrlParser: true,
        useUnifiedTopology: true,
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

定义Schema #

基本Schema #

typescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

@Schema()
export class User extends Document {
  @Prop({ required: true })
  name: string;

  @Prop({ required: true, unique: true })
  email: string;

  @Prop({ required: true, select: false })
  password: string;

  @Prop({ default: true })
  isActive: boolean;

  @Prop({ type: Date, default: Date.now })
  createdAt: Date;

  @Prop({ type: Date, default: Date.now })
  updatedAt: Date;
}

export const UserSchema = SchemaFactory.createForClass(User);

Schema选项 #

typescript
@Schema({
  collection: 'users',
  timestamps: true,
  versionKey: false,
})
export class User extends Document {
  // ...
}

属性装饰器选项 #

typescript
@Prop({
  required: true,
  unique: true,
  lowercase: true,
  trim: true,
  minlength: 6,
  maxlength: 100,
  default: 'default value',
  select: false,
  index: true,
})
email: string;

嵌套文档 #

typescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';

class Address {
  @Prop()
  street: string;

  @Prop()
  city: string;

  @Prop()
  country: string;
}

@Schema()
export class User extends Document {
  @Prop()
  name: string;

  @Prop({ type: Address })
  address: Address;
}

export const UserSchema = SchemaFactory.createForClass(User);

关联关系 #

引用关系 #

typescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';

@Schema()
export class User extends Document {
  @Prop()
  name: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

@Schema()
export class Post extends Document {
  @Prop({ required: true })
  title: string;

  @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
  author: mongoose.Schema.Types.ObjectId;
}

export const PostSchema = SchemaFactory.createForClass(Post);

填充引用 #

typescript
const post = await this.postModel
  .findById(id)
  .populate('author')
  .exec();

注册Schema #

typescript
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 {}

CRUD操作 #

注入Model #

typescript
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './schemas/user.schema';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User.name) private userModel: Model<User>,
  ) {}
}

创建 #

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

查询 #

typescript
async findAll(): Promise<User[]> {
  return this.userModel.find().exec();
}

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

更新 #

typescript
async update(id: string, updateUserDto: UpdateUserDto): Promise<User | null> {
  return this.userModel
    .findByIdAndUpdate(id, updateUserDto, { new: true })
    .exec();
}

async updateByEmail(email: string, data: any): Promise<User | null> {
  return this.userModel
    .findOneAndUpdate({ email }, data, { new: true })
    .exec();
}

删除 #

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

查询构建 #

条件查询 #

typescript
async search(query: any) {
  return this.userModel
    .find({
      $or: [
        { name: { $regex: query.keyword, $options: 'i' } },
        { email: { $regex: query.keyword, $options: 'i' } },
      ],
    })
    .exec();
}

分页 #

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

聚合查询 #

typescript
async aggregateStats() {
  return this.userModel.aggregate([
    { $match: { isActive: true } },
    { $group: { _id: '$role', count: { $sum: 1 } } },
    { $sort: { count: -1 } },
  ]);
}

索引 #

定义索引 #

typescript
@Schema()
export class User extends Document {
  @Prop({ index: true })
  email: string;

  @Prop({ index: true })
  name: string;
}

UserSchema.index({ name: 1, email: 1 }, { unique: true });

文本索引 #

typescript
UserSchema.index({ name: 'text', bio: 'text' });

// 使用
const results = await this.userModel
  .find({ $text: { $search: 'keyword' } })
  .exec();

中间件 #

保存前中间件 #

typescript
UserSchema.pre<User>('save', function (next) {
  if (this.isModified('password')) {
    this.password = hashPassword(this.password);
  }
  this.updatedAt = new Date();
  next();
});

查询后中间件 #

typescript
UserSchema.post<User>('find', function (docs) {
  docs.forEach(doc => {
    doc.password = undefined;
  });
});

虚拟属性 #

typescript
UserSchema.virtual('fullName').get(function () {
  return `${this.firstName} ${this.lastName}`;
});

// 启用虚拟属性
UserSchema.set('toJSON', { virtuals: true });
UserSchema.set('toObject', { virtuals: true });

事务处理 #

MongoDB 4.0+支持事务:

typescript
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection } from 'mongoose';

@Injectable()
export class UsersService {
  constructor(@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();
    }
  }
}

GridFS文件存储 #

typescript
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Connection, Types } from 'mongoose';
import { GridFSBucket, GridFSBucketReadStream } from 'mongoose';

@Injectable()
export class FileService {
  private bucket: GridFSBucket;

  constructor(@InjectConnection() private connection: Connection) {
    this.bucket = new GridFSBucket(this.connection.db);
  }

  async uploadFile(file: Express.Multer.File): Promise<Types.ObjectId> {
    const uploadStream = this.bucket.openUploadStream(file.originalname, {
      contentType: file.mimetype,
    });

    return new Promise((resolve, reject) => {
      uploadStream.end(file.buffer);
      uploadStream.on('finish', () => resolve(uploadStream.id));
      uploadStream.on('error', reject);
    });
  }

  async downloadFile(id: string): Promise<GridFSBucketReadStream> {
    return this.bucket.openDownloadStream(new Types.ObjectId(id));
  }

  async deleteFile(id: string): Promise<void> {
    await this.bucket.delete(new Types.ObjectId(id));
  }
}

最佳实践 #

1. 使用DTO #

typescript
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

2. 软删除 #

typescript
@Schema()
export class User extends Document {
  @Prop()
  name: string;

  @Prop({ type: Date })
  deletedAt: Date;
}

// 查询时排除已删除
async findActive() {
  return this.userModel.find({ deletedAt: null }).exec();
}

3. 数据转换 #

typescript
UserSchema.set('toJSON', {
  transform: (doc, ret) => {
    ret.id = ret._id.toString();
    delete ret._id;
    delete ret.__v;
    delete ret.password;
    return ret;
  },
});

总结 #

本章学习了NestJS与MongoDB的集成:

  • Mongoose配置
  • Schema定义
  • CRUD操作
  • 查询构建
  • 索引和中间件
  • 事务处理

接下来,让我们学习 Passport认证

最后更新:2026-03-28