nestjs学习-写一个CRUD

0 阅读6分钟

用户信息CRUD

需求:开发一个用户信息相关的 增删改查 模块;

涉及到的内容:

  • 数据库操作
  • 请求参数校验(管道相关知识)

一、完整代码

首先参数校验功能的实现需要安装以下两个包:

npm i class-transformer class-validator -S

目录结构如下:

crud_1.png

Users.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
​
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
​
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
​
  @Get()
  findAll() {
    return this.usersService.findAll();
  }
​
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }
​
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateUserDto: any) {
    return this.usersService.update(+id, updateUserDto);
  }
​
  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}

users.service.ts

import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserEntity } from './entities/user.entity';
​
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
  ) {}
​
  create(createUserDto: CreateUserDto) {
    // 创建用户
    const user = this.userRepository.create(createUserDto);
    return this.userRepository.save(user);
  }
​
  findAll() {
    return this.userRepository.find();
  }
​
  findOne(id: number) {
    return this.userRepository.findOne({ where: { id } });
  }
​
  async update(id: number, updateUserDto: any) {
    const result = await this.userRepository.update(id, updateUserDto);
    if (result.affected === 0) {
      return false;
    }
    
    return true;
  }
​
  async remove(id: number) {
    const result = await this.userRepository.delete(id);
    if (result.affected === 0) {
      return false;
    }
    return true;
  }
}

Create-user.dto.ts

import { IsString, IsNotEmpty, IsOptional, Matches, MinLength, MaxLength } from 'class-validator';
​
export class CreateUserDto {
  // 字符串,并且不能为空字符串
  @IsString({ message: '名称必须是字符串' })
  @Matches(/^[^\s]+$/)
  @MinLength(4)
  @MaxLength(20)
  name: string;
​
  @IsString({ message: '密码必须是字符串' })
  @IsNotEmpty({ message: '密码不能为空' })
  @Matches(/^[a-zA-Z0-9]{8,16}$/, { message: '密码只能包含字母和数字,长度为8-16' })
  password: string;
​
  @IsString()
  @IsOptional() // 可选
  email: string;
​
  @IsString()
  @IsOptional() // 可选
  phone: string;
}

以上代码完成后,我就启动服务调用接口,但是发现 dto 文件内的校验 一直没有生效; 经过排查才发现,管道的使用,需要先全局声明,所以:

main.ts

补充代码如下:

// ...
import { ValidationPipe } from '@nestjs/common';
​
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 关键代码
  app.useGlobalPipes(
    new ValidationPipe({
      // ...
    }),
  );
  await app.listen(3000);
}
bootstrap();

至此,经过测试,CRUD接口都能正常工作了;

二、开发记录

1. 管道自动验证请求

相关代码在 main.ts 中;

import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(
    new ValidationPipe({
      // ...
    })
)

ValidationPipe 是一个功能强大的内置管道,用于自动验证请求数据。它依赖于 class-validatorclass-transformer 这两个库,因此在使用前需要确保它们已被安装。

以下是 ValidationPipe 的主要配置项及其作用的详细解释:

1.1 核心配置项

这些是日常开发中最常用的配置,对于构建健壮且安全的 API 至关重要。

  • transform: boolean

    • 作用:启用自动类型转换。
    • 解释:当设置为 true 时,ValidationPipe 会利用 class-transformer 库将传入的普通 JavaScript 对象(例如请求体)转换为你定义的 DTO(数据传输对象)类的实例。这不仅能确保数据类型正确(例如,将字符串 "25" 转换为数字 25),还能让你获得类实例的所有特性。
  • whitelist: boolean

    • 作用:启用白名单过滤。
    • 解释:当设置为 true 时,管道会自动从传入的对象中剥离(删除)那些在 DTO 中没有定义且没有使用任何验证装饰器的属性。这是一种安全措施,可以防止意外的属性注入。
  • forbidNonWhitelisted: boolean

    • 作用:禁止非白名单属性。
    • 解释:这是对 whitelist 功能的增强。当设置为 true 时,如果传入的数据包含了 DTO 中未定义的属性,ValidationPipe 不会默默地删除它们,而是直接抛出一个 400 Bad Request 异常,向客户端明确指出发送了无效数据。
1.2 其他常用配置项

这些配置项提供了更细粒度的控制,适用于特定场景。

  • enableDebugMessages: boolean

    • 作用:启用调试信息。如果设置为 true,当验证出现问题时,验证器会向控制台打印额外的警告消息,有助于开发调试。
  • skipUndefinedProperties: boolean

    • 作用:跳过对 undefined 属性的验证。如果设置为 true,验证器将跳过对象中所有值为 undefined 的属性。
  • skipNullProperties: boolean

    • 作用:跳过对 null 属性的验证。如果设置为 true,验证器将跳过对象中所有值为 null 的属性。
  • skipMissingProperties: boolean

    • 作用:跳过对缺失属性的验证。如果设置为 true,验证器将跳过对象中所有为 nullundefined 的属性。
  • forbidUnknownValues: boolean

    • 作用:禁止验证未知值。如果设置为 true,尝试验证一个不是 DTO 类实例的对象会立即失败。
  • errorHttpStatusCode: number

    • 作用:自定义错误状态码。允许你指定在发生验证错误时返回的 HTTP 状态码。默认是 400 (Bad Request)。
  • exceptionFactory: Function

    • 作用:自定义异常工厂。这是一个函数,接收验证错误数组作为参数,并返回一个要抛出的异常对象。这为你提供了完全自定义错误响应的能力。
  • groups: string[]

    • 作用:指定验证组。允许你根据定义的组来执行验证,这在同一个 DTO 需要用于不同场景(如创建和更新)时非常有用。
  • strictGroups: boolean

    • 作用:启用严格组模式。如果 groups 未给出或为空,则忽略所有至少带有一个组的装饰器。
  • always: boolean

    • 作用:设置装饰器 always 选项的默认值。可以在单个装饰器选项中覆盖此默认值。
  • dismissDefaultMessages: boolean

    • 作用:忽略默认错误消息。如果设置为 true,验证将不会使用 class-validator 提供的默认错误消息。
  • validationError: object

    • 作用:控制验证错误对象中暴露的信息。

      • validationError.target: 指示是否应在 ValidationError 中暴露被验证的对象实例。
      • validationError.value: 指示是否应在 ValidationError 中暴露被验证的值。
  • stopAtFirstError: boolean

    • 作用:在第一个错误处停止。当设置为 true 时,对于给定属性的验证在遇到第一个错误后会停止,而不是收集所有错误。
  • disableErrorMessages: boolean

    • 作用:禁用错误消息。如果设置为 true,验证错误将不会返回给客户端。
1.3 最佳实践

在大多数生产环境中,推荐使用以下配置组合,以确保数据的有效性和安全性:

app.useGlobalPipes(
  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
      transformOptions: {
        enableImplicitConversion: true, // 允许隐式类型转换
      },
      // 对单个属性的多条校验规则,命中第一条失败就停止继续校验(减少错误数量、响应更短)。
      stopAtFirstError: true,
      // 详细的错误响应
      exceptionFactory: (errors) => {
        const messages = errors.map((e) => {
          const rule = Object.keys(e.constraints!)[0]
          const msg = e.constraints![rule]
          return msg
        })
        return new HttpException(
          {
            statusCode: HttpStatus.BAD_REQUEST,
            message: messages[0] || 'Validation failed'
          },
          HttpStatus.BAD_REQUEST
        )
      },
    }),
);

2. 更新接口详解

在此接口中有如下代码:

const result = await this.userRepository.update(id, updateUserDto);
console.log('result', result);
if (result.affected === 0) {
  return false;
}
​
return true;

这是 TypeORM 在执行 repository.update(criteria, partialEntity) 后返回的 UpdateResult,不是“更新后的整行数据”,而是这次写操作在驱动层面的元信息。

UpdateResult 里三个字段在说什么

affected: 1

  • 表示 被这次 UPDATE 语句影响到的行数。
  • 1 说明:按你的条件(这里是 id)至少匹配并更新了 1 行(在常见配置下就是“更新了 1 条记录”)。
  • 若为 0,通常表示没有匹配到任何行,或数据库认为没有行被改动(你代码里用 affected === 0 返回 false 就是这个语义)。

generatedMaps: []

  • 用于放 数据库生成/回填的字段 的映射(例如自增主键、某些数据库的 DEFAULT、触发器写的列、@Generated() 等)。
  • update() 一般不把“整行新数据”放在这里;很多场景下就是空数组。
  • 若你用 Insert 且表有自增 id,常见会看到 generatedMaps 里有新 id;单纯 PATCH 更新 经常是 []

raw: []

  • 驱动返回的原始结果(不同数据库、不同驱动差异很大)。
  • 对很多 普通 UPDATE,没有额外要暴露的 raw 行时就是 []
  • 有时在使用 RETURNING 等特性时,raw 里才会有内容(取决于查询与驱动)。