用户信息CRUD
需求:开发一个用户信息相关的 增删改查 模块;
涉及到的内容:
数据库操作请求参数校验(管道相关知识)
一、完整代码
首先参数校验功能的实现需要安装以下两个包:
npm i class-transformer class-validator -S
目录结构如下:
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-validator 和 class-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,验证器将跳过对象中所有为null或undefined的属性。
- 作用:跳过对缺失属性的验证。如果设置为
-
forbidUnknownValues: boolean- 作用:禁止验证未知值。如果设置为
true,尝试验证一个不是 DTO 类实例的对象会立即失败。
- 作用:禁止验证未知值。如果设置为
-
errorHttpStatusCode: number- 作用:自定义错误状态码。允许你指定在发生验证错误时返回的 HTTP 状态码。默认是
400(Bad Request)。
- 作用:自定义错误状态码。允许你指定在发生验证错误时返回的 HTTP 状态码。默认是
-
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 里才会有内容(取决于查询与驱动)。