NestJS 我需要 DTO 和实体吗?
NestJS Do I need DTO's along with entities?
我正在创建简单的服务,它将执行简单的 CRUD。
到目前为止,我有 entity user:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column({ name: "first_name" })
firstName: string;
@Column({ name: "last_name" })
lastName: string;
@Column({ name: "date_of_birth" })
birthDate: string;
}
控制器:
import { Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('api/v1/backoffice')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':username')
findOne(@Query('username') username: string) {
return this.usersService.findByUsername(username);
}
}
服务:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
findByUsername(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({ username });
}
}
在这个基本示例中,我 return 来自数据库的值,其中一些列被重命名:first_name --> firstName
它确实符合我的目的,但在很多地方,我看到使用了 DTO。我知道我没有做正确的事情,我也应该开始使用它。
我将如何在示例中使用 DTO 方法?
我试图掌握这里的概念。
首先,@Carlo Corradini 的评论是正确的,你应该看看 class-transformer
和 class-validator
库,它们也在 NestJS 管道的幕后使用,并且可以与 TypeORM
.
完美结合
1:根据@Carlo Corradini 的评论及其相应链接
现在,由于您的 DTO 实例是您要向消费者公开的数据的表示,因此您必须在检索到您的用户实体后实例化它。
- 创建一个新的
user-response.dto.ts
文件并在其中声明一个要导出的 UserResponseDto
class。假设如果您想公开之前检索到的 User
实体 中的所有内容,代码将如下所示
user-response.dto.ts
import { IsNumber, IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class UserResponseDto {
@Expose()
@IsNumber()
id: number;
@Expose()
@IsString()
username: string;
@Expose()
@IsString()
firstName: string;
@Expose()
@IsString()
lastName: string;
@Expose()
@IsString()
birthDate: string;
}
这里使用 UserResponseDto
顶部的 @Exclude(),我们告诉 class-transformer
排除任何没有 @Expose()
装饰器的字段DTO 文件,当我们将从任何其他对象实例化 UserResponseDto
时。
然后使用 @IsString()
和 @IsNumber()
,我们告诉 class-validator 在我们验证给定字段的类型时验证它们。
- 将您的用户实体转换为
UserResponseDto
实例:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async findByUsername(username: string): Promise<User | undefined> {
const retrievedUser = await this.usersRepository.findOne({ username });
// instantiate our UserResponseDto from retrievedUser
const userResponseDto = plainToClass(UserResponseDto, retrievedUser);
// validate our newly instantiated UserResponseDto
const errors = await validate(userResponseDto);
if (errors.length) {
throw new BadRequestException('Invalid user',
this.modelHelper.modelErrorsToReadable(errors));
}
return userResponseDto;
}
}
2:另一种实现方式:
您还可以使用 @nestjs/common 中的 ClassSerializerInterceptor
interceptor 来自动转换返回的 Entity
服务中的实例转换为控制器方法中定义的正确返回类型。这意味着您甚至不必费心在您的服务中使用 plainToClass 并让 Nest 的拦截器本身完成工作,官方文档中有一些细节
Note that we must return an instance of the class. If you return a
plain JavaScript object, for example, { user: new UserEntity() }, the
object won't be properly serialized.
代码如下所示:
users.controller.ts
import { ClassSerializerInterceptor, Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':username')
findOne(@Query('username') username: string) {
return this.usersService.findByUsername(username);
}
}
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async findByUsername(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({ username }); // <== must be an instance of the class, not a plain object
}
}
最后的想法:
使用最新的解决方案,您甚至可以在用户实体文件中使用 class-transformer
的装饰器,而不必声明 DTO 文件,但您会失去数据验证。
如果有帮助或者有什么不清楚的地方,请告诉我:)
使用传入的有效负载验证进行编辑并转换为正确的 DTO
您可以声明一个带有用户名属性的 GetUserByUsernameRequestDto
,如下所示:
get-user-by-username.request.dto.ts
import { IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class GetUserByUsernameRequestDto {
@Expose()
@IsString()
@IsNotEmpty()
username: string;
}
users.controller.ts
import { ClassSerializerInterceptor, Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
@UsePipes( // <= this is where magic happens :)
new ValidationPipe({
forbidUnknownValues: true,
forbidNonWhitelisted: true,
transform: true
})
)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':username')
findOne(@Param('username') getUserByUsernameReqDto: GetUserByUsernameRequestDto) {
return this.usersService.findByUsername(getUserByUsernameReqDto.username);
}
}
这里我们使用 Nest's pipes concept - @UsePipes() - 来完成工作。以及来自 Nest 的 built-in ValidationPipe
。
您可以参考 Nest and class-validator 本身的文档,以了解有关传递给 ValidationPipe
的选项的更多信息
因此,通过这种方式,您的传入参数和有效负载数据可以在处理之前进行验证:)
我正在创建简单的服务,它将执行简单的 CRUD。 到目前为止,我有 entity user:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column({ name: "first_name" })
firstName: string;
@Column({ name: "last_name" })
lastName: string;
@Column({ name: "date_of_birth" })
birthDate: string;
}
控制器:
import { Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('api/v1/backoffice')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':username')
findOne(@Query('username') username: string) {
return this.usersService.findByUsername(username);
}
}
服务:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
findByUsername(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({ username });
}
}
在这个基本示例中,我 return 来自数据库的值,其中一些列被重命名:first_name --> firstName
它确实符合我的目的,但在很多地方,我看到使用了 DTO。我知道我没有做正确的事情,我也应该开始使用它。 我将如何在示例中使用 DTO 方法?
我试图掌握这里的概念。
首先,@Carlo Corradini 的评论是正确的,你应该看看 class-transformer
和 class-validator
库,它们也在 NestJS 管道的幕后使用,并且可以与 TypeORM
.
1:根据@Carlo Corradini 的评论及其相应链接
现在,由于您的 DTO 实例是您要向消费者公开的数据的表示,因此您必须在检索到您的用户实体后实例化它。
- 创建一个新的
user-response.dto.ts
文件并在其中声明一个要导出的UserResponseDto
class。假设如果您想公开之前检索到的User
实体 中的所有内容,代码将如下所示
user-response.dto.ts
import { IsNumber, IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class UserResponseDto {
@Expose()
@IsNumber()
id: number;
@Expose()
@IsString()
username: string;
@Expose()
@IsString()
firstName: string;
@Expose()
@IsString()
lastName: string;
@Expose()
@IsString()
birthDate: string;
}
这里使用 UserResponseDto
顶部的 @Exclude(),我们告诉 class-transformer
排除任何没有 @Expose()
装饰器的字段DTO 文件,当我们将从任何其他对象实例化 UserResponseDto
时。
然后使用 @IsString()
和 @IsNumber()
,我们告诉 class-validator 在我们验证给定字段的类型时验证它们。
- 将您的用户实体转换为
UserResponseDto
实例:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async findByUsername(username: string): Promise<User | undefined> {
const retrievedUser = await this.usersRepository.findOne({ username });
// instantiate our UserResponseDto from retrievedUser
const userResponseDto = plainToClass(UserResponseDto, retrievedUser);
// validate our newly instantiated UserResponseDto
const errors = await validate(userResponseDto);
if (errors.length) {
throw new BadRequestException('Invalid user',
this.modelHelper.modelErrorsToReadable(errors));
}
return userResponseDto;
}
}
2:另一种实现方式:
您还可以使用 @nestjs/common 中的 ClassSerializerInterceptor
interceptor 来自动转换返回的 Entity
服务中的实例转换为控制器方法中定义的正确返回类型。这意味着您甚至不必费心在您的服务中使用 plainToClass 并让 Nest 的拦截器本身完成工作,官方文档中有一些细节
Note that we must return an instance of the class. If you return a plain JavaScript object, for example, { user: new UserEntity() }, the object won't be properly serialized.
代码如下所示:
users.controller.ts
import { ClassSerializerInterceptor, Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':username')
findOne(@Query('username') username: string) {
return this.usersService.findByUsername(username);
}
}
users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async findByUsername(username: string): Promise<User | undefined> {
return this.usersRepository.findOne({ username }); // <== must be an instance of the class, not a plain object
}
}
最后的想法:
使用最新的解决方案,您甚至可以在用户实体文件中使用 class-transformer
的装饰器,而不必声明 DTO 文件,但您会失去数据验证。
如果有帮助或者有什么不清楚的地方,请告诉我:)
使用传入的有效负载验证进行编辑并转换为正确的 DTO
您可以声明一个带有用户名属性的 GetUserByUsernameRequestDto
,如下所示:
get-user-by-username.request.dto.ts
import { IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class GetUserByUsernameRequestDto {
@Expose()
@IsString()
@IsNotEmpty()
username: string;
}
users.controller.ts
import { ClassSerializerInterceptor, Controller, Get, Query } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
@UsePipes( // <= this is where magic happens :)
new ValidationPipe({
forbidUnknownValues: true,
forbidNonWhitelisted: true,
transform: true
})
)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':username')
findOne(@Param('username') getUserByUsernameReqDto: GetUserByUsernameRequestDto) {
return this.usersService.findByUsername(getUserByUsernameReqDto.username);
}
}
这里我们使用 Nest's pipes concept - @UsePipes() - 来完成工作。以及来自 Nest 的 built-in ValidationPipe
。
您可以参考 Nest and class-validator 本身的文档,以了解有关传递给 ValidationPipe
因此,通过这种方式,您的传入参数和有效负载数据可以在处理之前进行验证:)