一、DTO(Data Transfer Object,数据传输对象)
1. 什么是 DTO?
DTO 是一个普通对象 (通常是一个类),用于在进程之间(如客户端 ↔ 控制器、控制器 ↔ 服务)封装数据。它的主要目的是:
-
定义数据的形状(哪些字段、什么类型)。
-
验证输入数据(结合验证管道)。
-
隐藏内部实现细节(如密码字段的排除)。
-
合并或裁剪数据,避免过度传输。
在 NestJS 中,DTO 最常用于:
-
请求体(Body) 的验证和类型约束。
-
查询参数(Query) 的结构化。
-
响应数据 的格式规范(尽管响应通常使用
interface或class,但也可以使用 DTO 类)。
2. 为什么需要 DTO?
-
类型安全:配合 TypeScript 提供编译时检查。
-
自动验证 :结合
ValidationPipe和class-validator装饰器,轻松实现字段校验。 -
文档生成:Swagger/OpenAPI 可以从 DTO 类自动生成 API 文档。
-
解耦:内部实体(Entity)可能与数据库结构耦合,DTO 可以作为对外接口的屏障
3. 在 NestJS 中实现 DTO
通常使用 class 而不是 interface,因为类在运行时保留元数据,便于验证管道工作。示例:创建用户的 DTO
安装依赖:
npm install class-validator class-transformer
TypeScript
// create-user.dto.ts
import { IsString, IsEmail, IsOptional, MinLength, MaxLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsEmail()
email: string;
@IsOptional()
@IsString()
bio?: string;
}
在控制器中使用:
TypeScript
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
全局启用验证(在 main.ts):
TypeScript
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
whitelist: true会自动剔除 DTO 中未定义的字段,防止多余数据注入。
二、DAO(Data Access Object,数据访问对象)
1. 什么是 DAO?
DAO 是一个设计模式 ,它提供了一个抽象接口来访问某种持久化机制(如数据库、文件系统)。在 NestJS 中,DAO 通常指:
-
Repository(TypeORM、Sequelize 等 ORM 中的仓库类)。
-
封装了数据库 CRUD 操作的自定义类。
DAO 的主要目的是将数据访问逻辑与业务逻辑分离。服务层(Service)不应该关心底层是 MySQL 还是 MongoDB,只需要调用 DAO 提供的方法。
2. 在 NestJS 中实现 DAO
NestJS 本身不内置 DAO,但通常配合 ORM(如 TypeORM、Prisma、MikroORM)使用。ORM 提供的 Repository 模式本质上就是 DAO 的一种实现。
方式一:使用 TypeORM 的 Repository
定义实体(Entity):
TypeScript
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false }) // 默认查询时不返回密码
password: string;
}
在模块中注册 Repository:
TypeScript
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])], // 注册 Repository
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
在服务中注入 Repository(作为 DAO):
TypeScript
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>, // 这就是 DAO
) {}
async findAll(): Promise<User[]> {
return this.userRepository.find(); // 数据访问操作
}
async create(userData: Partial<User>): Promise<User> {
const user = this.userRepository.create(userData);
return this.userRepository.save(user);
}
}
三、DTO vs DAO:对比总结
| 特性 | DTO | DAO |
|---|---|---|
| 全称 | Data Transfer Object | Data Access Object |
| 目的 | 在不同层之间传输数据 | 封装数据访问逻辑 |
| 包含 | 字段 + 验证装饰器 | 方法(CRUD、查询等) |
| 是否有状态 | 有数据字段(无方法或很少) | 无状态(通常只有方法) |
| 典型位置 | 控制器层(请求/响应) | 服务层(数据访问) |
| 与 ORM 关系 | 与 ORM 无关 | 通常使用 ORM 的 Repository |
| 生命周期 | 短暂,用于请求/响应 | 长生命周期,单例 |
| NestJS 装饰器 | @Body(), @Query() 等 |
@Injectable(), @InjectRepository() |
一句话区分:
-
DTO :告诉我数据长什么样(形状、验证)。
-
DAO :告诉我怎么拿到/保存数据(方法、逻辑)。
四、常见误区与最佳实践
误区 1:DTO 和 Entity 是同一个东西
错误 :直接使用 Entity 类作为 DTO,暴露了数据库结构(如密码字段、内部关联)。
正确:为每个 API 请求/响应单独定义 DTO,避免泄露内部模型。
误区 2:DAO 就是 Repository
澄清:Repository 是 DAO 的一种实现,但 DAO 可以是更高级的抽象(如封装多个 Repository 的事务操作)。
误区 3:在 DTO 中写业务方法
DTO 应该只包含数据字段和验证规则,不应包含业务逻辑方法。
最佳实践
-
为每个端点定义独立的 DTO(创建、更新、响应可不同)。
-
使用
class-validator和class-transformer进行自动验证和转换。 -
使用
PartialType,PickType,OmitType继承已有 DTO,避免重复代码 -
DAO 优先使用 ORM 内置 Repository,除非需要高度定制的查询。
-
将 DTO 放在
dto/文件夹,DAO 或 Repository 放在与实体相近的位置。
五、完整示例:用户注册流程
TypeScript
// dto/create-user.dto.ts
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
}
// dto/user-response.dto.ts
export class UserResponseDto {
id: number;
name: string;
email: string;
}
// user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
}
// user.dao.ts (实际上是 TypeORM Repository)
@Injectable()
export class UserDao {
constructor(@InjectRepository(User) private repo: Repository<User>) {}
async create(userData: Partial<User>): Promise<User> {
const user = this.repo.create(userData);
return this.repo.save(user);
}
async findByEmail(email: string): Promise<User | null> {
return this.repo.findOneBy({ email });
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private userDao: UserDao) {}
async register(createDto: CreateUserDto): Promise<UserResponseDto> {
const existing = await this.userDao.findByEmail(createDto.email);
if (existing) throw new ConflictException('Email already exists');
const hashedPassword = await bcrypt.hash(createDto.password, 10);
const newUser = await this.userDao.create({
...createDto,
password: hashedPassword,
});
// 返回响应 DTO,排除密码
return { id: newUser.id, name: newUser.name, email: newUser.email };
}
}
六、总结
-
DTO 负责数据形状 和验证,位于控制器边界。
-
DAO 负责数据访问,位于持久化边界,通常由 Repository 实现。
-
两者配合使用,保持 NestJS 应用的分层清晰、可测试、易维护。