nestjs接入数据库、MONGO和验证

nestjs接入数据库

TypeORM 集成

安装所需的依赖项

1
npm install --save @nestjs/typeorm typeorm mysql2

安装完成后,我们可以把 TypeORMModule 导入到 AppModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';

@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'db_nest',
entities: [User],
synchronize: true,
}),
],
})
export class AppModule {}

实体表如何引入

但是我们发现一个问题,如果表很多怎么办,总不能全部import进来之后写进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { User } from './users/user.entity';
import { Todo } from './todo/todo.entity';
// 其他entity实体
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'db_nest',
entities: [User, Todo, // 其他实体],
synchronize: true,
}),
],
})

// 我们也可以自动加载实体

// 方式一:添加 autoLoadEntities 为 true
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
imports: [
TypeOrmModule.forRoot({
...
autoLoadEntities: true,
}),
],
})

// 方式二:获取所有项目下的 entity.ts 文件为实体
@Module({
imports: [
TypeOrmModule.forRoot({
...
entities: [__dirname + '/**/*.entity{.ts,.js}'],
}),
],
})

上面两种方式都可以成功集成TypeORM

TypeORM如何定义表结构

我们主要了解一下TypeORM的主要内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { Photo } from '../photos/photo.entity';

@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column()
firstName: string;

@Column()
lastName: string;

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

@OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
}

这里就定义了一个users表,如果你想要自定义表名称,可以在@Entity(‘user’)中添加 user,这样就是加载 user 表。

主要涉及到的是主键列,普通列,外键列(一对一、一对多、多对一、多对多),这里表示 users 表中的 photo 字段,关联到的是 Photo 实体,也就是 photos 表,如果它没有显示指定,然后是一对多的关系,表示一个用户会有多张照片,关联的字段是 photos 表中的 user 字段

在column装饰器中有很多有趣的属性,例如字段类型,字段名称,字段长度,是否为空,默认值等等,基本跟数据表的属性差不太多。

外键绑定的注意点

我们需要注意以下两种情况
1:级联删除
默认我们在有外键关联的数据表中,不会去主动设置cascade为true、onDelete为CASCADE
因为一旦设置过,删除数据时,外键关联到的数据也会被删除

2:级联查询
默认我们在查询数据的时候,有的时候我们并不希望将外键关联的数据给查询出来,所以默认查询单条数据时不会自动加载一对多关联的数据
但是当我指定 relations 时,它会去查询关联的数据,只有你加上了 eager为 true 的时候会自动查询

例如查询用户关联的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['photos'],
});

// 查询结果是这样的
{
"id": 1,
"photos": [
{ "id": 10, "url": "xxx" },
{ "id": 11, "url": "yyy" }
]
}

// 如果不带 relations 的时候,查询结果是这样的
{
"id": 1
// 没有 photos
}

TypeORM还有一种方式可以支持你查询关联数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;

@OneToMany(() => Photo, photo => photo.user)
photos: Promise<Photo[]>; // 注意这里是 Promise
}

// 当我把 photos 设置成了 Promse 然后发起查询

const user = await userRepository.findOne({ where: { id: 1 } });

// 此时 user 里并没有真的装载 photos
// 但是当你写了以下代码的时候,它会执行以下sql查询语句

const photos = await user.photos;

// SELECT * FROM photo WHERE userId = 1;

但是 lazy loading 有一个缺点,就是它是单个查询,如果我需要查询一批用户,然后再用循环语句去获取每个用户 photos 的时候,你的查询会变成 N+1 查询。也就是一次用户数据查询加上n次的用户photos查询,此用法会造成性能问题并且SQL很难优化。

所以,我还是更加推荐显示写 relations ,这样 SQL 才会可控。

多个数据库如何连接

通常我们的项目中可能会连接多个数据库,我们可以通过以下方式来创建多个连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Module({
imports: [
TypeOrmModule.forRoot({
...defaultOptions,
name: 'default',
host: 'user_db_host',
entities: [User],
}),
TypeOrmModule.forRoot({
...defaultOptions,
name: 'albumsConnection',
host: 'album_db_host',
entities: [Album],
}),
],
})

我们发现配置中多了name属性,这是因为如果只有一个连接,你可以不写name,默认就是 default,但是如果有多个连接,我们应该区分名称,这样连接不会被覆盖

然后我们在使用表的时候,需要告诉 TypeOrmModule.forFeature() 方法和 @InjectRepository() 装饰器应该使用哪个数据源,如果不写,默认是 default

1
2
3
4
5
6
7
@Module({
imports: [
TypeOrmModule.forFeature([User]), // default名称的连接库中找 users 表
TypeOrmModule.forFeature([Album], 'albumsConnection'), // albumsConnection名称的连接库中找 albums 表
],
})
export class AppModule {}

如何使用TypeORM来编写业务代码

在处理完连接后,我们应该这样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
// 这里引入 users 表
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}

然后在userService 中我们可以直接使用表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
constructor(
// 注入 users 表
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}

findAll(): Promise<User[]> {
return this.usersRepository.find();
}

findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}

async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}

其他功能

当然我们后面还有transaction,异步配置等等,等需要的时候可以阅读文档进行使用或接入

nestjs接入验证

validation的集成

1
$ npm i --save class-validator class-transformer

一般参数的验证都是可以放到全局的,有两种方式可以全局应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 方式一:在根模块的providers中注册
{
provide: APP_PIPE,
useFactory: () => {
return new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
disableErrorMessages: false,
});
}
}

// 方式二:在 main.ts 主文件中注册
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

我们发现 ValidationPipe 允许我们传递一些参数

  • enableDebugMessages(boolean):在出现问题时向控制台打印额外的警告信息
  • skipUndefinedProperties(boolean):跳过验证对象中所有未定义属性的验证
  • whitelist(boolean):删除非白名单属性
  • disableErrorMessages(boolean):不返回错误信息给客户端
  • 还有其他的属性可以控制验证的行为

validation的使用

一般验证都是 dto 文件

1
2
3
4
@Post()
create(@Body() createUserDto: CreateUserDto) {
return 'This action adds a new user';
}

dto 文件应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
  IsEnum, // 枚举
  IsNotEmpty, // 不为空
  IsNumber, // 数值
  IsOptional, // 可选
  IsString, // 字符串
} from 'class-validator';

enum Status {
  OK = 1,
  NOTOK = 2,
}

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string;
 
  @IsOptional()
  @IsNumber()
  @IsEnum(Status) // 这个告诉用户status是可选的,如果传了,必须是1或2
  status?: Status; // 1=ok, 2=not ok
}

然后我们在请求创建新用户的时候,就会走 dto 的验证

1
2
3
4
5
6
7
8
9
10
11
12
13
// 传递参数如下
{
status: 1
}

// 提示 name 必传

{
name: "Justin",
status: 3
}

// 提示 status 必须是 1 或 2

其他用法

当然你还可以针对 CreateUserDto 进行显示的转换,例如比如你还有一个字符串的数值,就可以在service中这么写来进行转换

1
2
3
4
5
6
7
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
) {
console.log(typeof id === 'number'); // true
return 'This action returns a user';
}

当然你可以创建一个新的用户,用的是 CreateUserDto,那么你也可以更新用户信息,用的就是 UpdateUserDto,我们发现,更新和创建的区别就是是否需要传递主键,并且更新的话,每个字段其实都是可选的,按照restful的风格,应该是传递两个参数,第一个是需要更新的数据的主键id,然后就是需要更新的数据

1
export class UpdateUserDto extends PartialType(CreateUserDto) {}

这样我们相当于直接复用了 CreateUserDto 的字段,并将每个字段变成可选类型

nestjs接入MONGO

mongoose 集成

安装所需的依赖项

1
npm i @nestjs/mongoose mongoose

然后在根模块导入进行 mongoose 的使用

1
2
3
4
5
6
7
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

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

如何定义mongo collection

使用 Mongoose,一切都是 Schema,每个 Schema 都映射到一个 MongoDB Collection

例如我们的用户表,其实可以放到 mongo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type UserDocument = HydratedDocument<User>;

@Schema({
  timestamps: true, // 默认添加返回创建时间和更新时间
  collection: 'user', // collection 名称,如果不主动设置,那么就跟mysql entity一样,就是对应的 users collection
  toJSON: { // 设置之后,exec查询结果后,可以通过调用 toJSON 方法进行转换调用
    virtuals: true,
    versionKey: false,
    transform: (_, ret: any) => {
      ret.id = ret._id.toString();
      delete ret._id;
      return ret;
    },
  },
})
export class User {
@Prop()
name: string;

@Prop()
age: number;

@Prop()
hobby: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

@Prop 装饰器是用来定义文档中的属性的,一些简单的字段类型它可以自动推断,但是一些复杂的需要我们去主动申明

例如一个数组字符串

1
2
@Prop([String])
tags: string[];

Props 中还允许你传递一系列的 options 参数

1
2
@Prop({ type: String, required: true })
name: string;

如何注册mongoose collection

上面我们定义了一个user collection,那么我们应该怎么将它注册进去呢?

1
2
3
4
5
6
7
8
9
10
11
12
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { User, UserSchema } from './schemas/user.schema';

@Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}

如何使用mongoose来编写业务代码

在上面注册了 User Collection 之后,我们可以在service层编写相关的业务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Model } from 'mongoose';
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { User } from './schemas/user.schema';
import { CreateUserDto } from './dto/create-user.dto';

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

async create(createUserDto: CreateUserDto): Promise<User> {
    const cteatedUser = await this.userModel.create(createUserDto);
    return cteatedUser.toJSON();
}

async findAll(): Promise<User[]> {
    const docs = await this.userModel.find().exec();
    return docs.map((d) => d.toJSON());
}
}

[!warning] 注意
我们通过 Model 查询返回的其实是 mongoose 的 document
所以我们最好是通过 toJSON 对 document 数据进行一个处理返回

如何连接多个数据库

我们通常会连接多个mongoose库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/test', {
connectionName: 'test', // 通过 connectionName 来区分,否则会覆盖
}),
MongooseModule.forRoot('mongodb://localhost/users', {
connectionName: 'users',
}),
],
})
export class AppModule {}

然后当我们主动设置了 connectionName 后,我们在注册的时候,应该带上 connectionName

1
2
3
4
5
6
@Module({
imports: [
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }], 'users'),
],
})
export class UserModule {}

然后在service中这样使用

1
2
3
4
@Injectable()
export class UserService {
constructor(@InjectModel(User.name, 'users') private userModel: Model<User>) {}
}

其他用法

当然在mongoose中还有事务,插件,子文档等功能,等实际用到了可以查看文档进行补充学习


nestjs接入数据库、MONGO和验证
https://shiyuq.github.io/2025/09/15/nestjs接入数据库、MONGO和验证/
作者
Jack
发布于
2025年9月15日
许可协议