后端系统常见的对象有三种:
Entity:数据实体,和数据库表对应。
DTO: Data Transfer Object,用于封装请求参数。
VO:Value Object,用于封装返回的响应数据。
三者的关系如下:
但文档中并没有提到 VO 对象,这是为什么呢?
因为有替代方案。
我们来看一下:
nest new vo-test
生成一个 user 的 CRUD 模块:
nest g resource user --no-spec
在 entity 里加一些内容:
export class User {
id: number;
username: string;
password: string;
email: string;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
Partial 是把 User 的属性变为可选:
可以传入部分属性,然后 Object.assign 赋值到 this。
然后 CreateUserDto 里包含这些属性:
export class CreateUserDto {
username: string;
password: string;
email: string;
}
实现下 UserService 的 create 和 find 的逻辑:
这里我们直接用数组模拟 database 来保存数据。
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
const database = [];
let id = 0;
@Injectable()
export class UserService {
create(createUserDto: CreateUserDto) {
const user = new User(createUserDto);
user.id = id++;
database.push(user);
return user;
}
findAll() {
return database;
}
findOne(id: number) {
return database.filter(item => item.id === id).at(0);
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}
把服务跑起来:
npm run start:dev
创建两个 user:
查一下:
可以看到,user 的 password 也被返回了。
而这个应该过滤掉。
一般这种情况,我们都会封装个 vo。
创建 vo/user.vo.ts:
export class UserVo {
id: number;
username: string;
email: string;
constructor(partial: Partial<UserVo>) {
Object.assign(this, partial);
}
}
然后把数据封装成 vo 返回:
findAll() {
return database.map(item => {
return new UserVo({
id: item.id,
username: item.username,
email: item.email
});
});
}
findOne(id: number) {
return database.filter(item => item.id === id).map(item => {
return new UserVo({
id: item.id,
username: item.username,
email: item.email
});
}).at(0);
}
试一下:
可以看到,这样就没有 password 了。
但你会发现 UserVo 和 User entity 很类似:
对于 dto 我们可以通过 PartialType、PickType、OmitType、IntersectionType 来组合已有 dto,避免重复。
那 vo 是不是也可以呢?
是的,nest 里可以直接复用 entity 作为 vo。
这里要用到 class-transformer 这个包:
npm install --save class-transformer
然后在 UserController 的查询方法上加上 ClassSerializerInterceptor 就好了:
代码恢复原样:
现在返回的数据就没有 password 字段了:
class-transformer 这个包我们用过,是用于根据 class 创建对应的对象的。
当时是 ValidationPipe 里用它来创建 dto class 对应的对象。
这里也是用它来创建 entity class 对应的对象。
简单看下 ClassSerializerInterceptor 的源码:
它是通过 map 对响应做转换,在 serialize 方法拿到响应的对象,如果是数组就拿到每个元素。
在 transformToPlain 方法里,调用 classToPlain 创建对象。
它会先拿到响应对象的 class、然后根据 class 上的装饰器来创建新的对象。
当然,装饰器不只有 @Exclude,还有几个有用的:
import { Exclude, Expose, Transform } from "class-transformer";
export class User {
id: number;
username: string;
@Exclude()
password: string;
@Expose()
get xxx(): string {
return `${this.username} ${this.email}`;
}
@Transform(({value}) => '邮箱是:' + value)
email: string;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
@Expose 是添加一个导出的字段,这个字段是只读的。
@Transform 是对返回的字段值做一些转换。
测试下:
可以看到,返回的数据多了 xxx 字段,email 字段也做了修改:
这样基于 entity 直接创建 vo 确实方便多了。
此外,你可以可以通过 @SerializeOptions 装饰器加一些序列化参数:
strategy 默认值是 exposeAll,全部导出,除了有 @Exclude 装饰器的。
设置为 excludeAl 就是全部排除,除了有 @Expose 装饰器的。
当然,你可以 ClassSerializerInterceptor 和 SerializeOptions 加到 class 上:
这样,controller 所有的接口返回的对象都会做处理:
swagger 那节当返回对象的时候,我们都是创建了个 vo 的类,在 vo class 上加上 swagger 的装饰器:
其实没必要,完全可以直接用 entity。
安装 swagger 的包:
npm install --save @nestjs/swagger
然后在 main.ts 添加 swagger 的入口代码:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Test example')
.setDescription('The API description')
.setVersion('1.0')
.addTag('test')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('doc', app, document);
await app.listen(3000);
}
bootstrap();
现在 @apiResponse 里就可以直接指定 User 的 entity 了:
import { Controller, Get, Post, Body, Patch, Param, Delete, UseInterceptors, ClassSerializerInterceptor, SerializeOptions, HttpStatus } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { User } from './entities/user.entity';
@Controller('user')
@SerializeOptions({
// strategy: 'excludeAll'
})
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@ApiOperation({summary:'findAll'})
@ApiResponse({
status: HttpStatus.OK,
description: 'ok',
type: User
})
@Get()
findAll() {
return this.userService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}
在 User 里加一下 swagger 的装饰器:
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
import { Exclude, Expose, Transform } from "class-transformer";
export class User {
@ApiProperty()
id: number;
@ApiProperty()
username: string;
@ApiHideProperty()
@Exclude()
password: string;
@ApiProperty()
@Expose()
get xxx(): string {
return `${this.username} ${this.email}`;
}
@ApiProperty()
@Transform(({value}) => '邮箱是:' + value)
email: string;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
注意,这里要用 @ApiHideProperty 把 password 字段隐藏掉。
可以看到,现在的 swagger 文档是对的:
而且我们没有用 vo 对象。
这也是为什么 Nest 文档里没有提到 vo,因为完全可以用 entity 来替代。
案例代码在小册仓库
总结
后端系统中常见 entity、vo、dto 三种对象,vo 是用来封装返回的响应数据的。
但是 Nest 文档里并没有提到 vo 对象,因为完全可以用 entity 来代替。
entity 里加上 @Exclude 可以排除某些字段、@Expose 可以增加一些派生字段、@Transform 可以对已有字段的序列化结果做修改。
然后在 cotnroller 上加上 ClassSerializerInterceptor 的 interceptor,还可以用 @SerializeOptions 来添加 options。
它的底层是基于 class-transfomer 包来实现的,拿到响应对象,plainToClass 拿到 class,然后根据 class 的装饰器再 classToPlain 创建序列化的对象。
swagger 的 @ApiResponse 也完全可以用 entity 来代替 vo,在想排除的字段加一下 @ApiHideProperty 就好了。
Nest 文档里并没有提到 vo 对象,因为完全没有必要,可以直接用序列化的 entity。