DTO,DAO是什么?MVC是什么架构?

一、DTO(Data Transfer Object,数据传输对象)

1. 什么是 DTO?

DTO 是一个普通对象 (通常是一个类),用于在进程之间(如客户端 ↔ 控制器、控制器 ↔ 服务)封装数据。它的主要目的是:

  • 定义数据的形状(哪些字段、什么类型)。

  • 验证输入数据(结合验证管道)。

  • 隐藏内部实现细节(如密码字段的排除)。

  • 合并或裁剪数据,避免过度传输。

在 NestJS 中,DTO 最常用于:

  • 请求体(Body) 的验证和类型约束。

  • 查询参数(Query) 的结构化。

  • 响应数据 的格式规范(尽管响应通常使用 interfaceclass,但也可以使用 DTO 类)。

2. 为什么需要 DTO?

  • 类型安全:配合 TypeScript 提供编译时检查。

  • 自动验证 :结合 ValidationPipeclass-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 应该只包含数据字段和验证规则,不应包含业务逻辑方法。

最佳实践

  1. 为每个端点定义独立的 DTO(创建、更新、响应可不同)。

  2. 使用 class-validatorclass-transformer 进行自动验证和转换。

  3. 使用 PartialType, PickType, OmitType 继承已有 DTO,避免重复代码

  4. DAO 优先使用 ORM 内置 Repository,除非需要高度定制的查询。

  5. 将 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 应用的分层清晰、可测试、易维护。

相关推荐
Ticnix3 天前
NestJs--Prisma 7的安装与数据库配置(超完整)
前端·nestjs
小蜜蜂dry4 天前
nestjs实战-登录、鉴权(二)
前端·后端·nestjs
全栈王校长4 天前
Nest 文件上传 - 就是增强版的 el-upload
前端·后端·nestjs
小蜜蜂dry5 天前
nestjs实战-登录、鉴权(一)
前端·后端·nestjs
小蜜蜂dry5 天前
nestjs实战 - 拦截器,统一处理接口请求与响应结果
前端·后端·nestjs
当时只道寻常5 天前
NestJS + OpenAI 实现流式输出
openai·nestjs
dyb-dev6 天前
我是如何学习 NestJS 的
前端·nestjs·全栈
全栈王校长7 天前
Nest 中间件 Middleware - 就像 Vue 的路由守卫
后端·nestjs
全栈王校长7 天前
Nest ValidationPipe 参数验证 - 就像前端的表单校验
后端·nestjs