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-transformerclass-validator 库,它们也在 NestJS 管道的幕后使用,并且可以与 TypeORM.

完美结合

1:根据@Carlo Corradini 的评论及其相应链接

现在,由于您的 DTO 实例是您要向消费者公开的数据的表示,因此您必须在检索到您的用户实体后实例化它。

  1. 创建一个新的 user-response.dto.ts 文件并在其中声明一个要导出的 UserResponseDto class。假设如果您想公开之前检索到的 User 实体
  2. 中的所有内容,代码将如下所示

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 在我们验证给定字段的类型时验证它们。

  1. 将您的用户实体转换为 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

的选项的更多信息

因此,通过这种方式,您的传入参数和有效负载数据可以在处理之前进行验证:)