1. 概述

在软件开发领域,实体(Entity)和DTO(数据传输对象)有着明确的区别。理解它们各自的角色和差异,能帮助我们构建更高效、更易维护的软件系统。

本文将深入探讨实体与DTO的区别,通过一个基于Spring Boot和JPA的用户管理示例,清晰说明二者的设计目的和应用场景。

2. 实体

实体是应用领域中真实世界对象或概念的核心表示。它们通常直接对应数据库表或领域对象,主要职责是封装和管理这些对象的状态与行为。

2.1. 实体示例

为项目创建实体:一个用户拥有多本书。先定义Book实体:

@Entity
@Table(name = "books")
public class Book {

    @Id
    private String name;
    private String author;

    // 标准构造器/getter/setter
}

再定义User实体:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String address;

    @OneToMany(cascade=CascadeType.ALL)
    private List<Book> books;
    
    public String getNameOfMostOwnedBook() {
        Map<String, Long> bookOwnershipCount = books.stream()
          .collect(Collectors.groupingBy(Book::getName, Collectors.counting()));
        return bookOwnershipCount.entrySet().stream()
          .max(Map.Entry.comparingByValue())
          .map(Map.Entry::getKey)
          .orElse(null);
    }

    // 标准构造器/getter/setter
}

2.2. 实体特征

实体具有以下显著特点:

ORM注解

  • @Entity标记类为实体,建立Java类与数据库表的直接关联
  • @Table指定关联的数据库表名
  • @Id定义主键字段
    这些注解简化了数据库映射过程

实体关系
通过@OneToMany等注解建立实体间关联(如用户与书籍的拥有关系)

业务逻辑封装
实体可包含领域特定逻辑(如getNameOfMostOwnedBook()方法),符合OOP原则和DDD思想,将领域操作内聚在实体中

⚠️ 其他特性:
可能包含验证约束生命周期方法

3. DTO

DTO本质是纯数据载体,不包含任何业务逻辑,用于在不同应用或应用组件间传输数据。

简单应用中常直接使用领域对象作为DTO,但随着系统复杂度增加,从安全和封装角度考虑,直接暴露整个领域模型给外部客户端并不可取。

3.1. DTO示例

实现用户创建和查询功能。先定义书籍DTO:

public class BookDto {

    @JsonProperty("NAME")
    private final String name;

    @JsonProperty("AUTHOR")
    private final String author;

    // 标准构造器/getter
}

为用户定义两个DTO:创建用和响应用:

public class UserCreationDto {

    @JsonProperty("FIRST_NAME")
    private final String firstName;

    @JsonProperty("LAST_NAME")
    private final String lastName;

    @JsonProperty("ADDRESS")
    private final String address;

    @JsonProperty("BOOKS")
    private final List<BookDto> books;

    // 标准构造器/getter
}
public class UserResponseDto {

    @JsonProperty("ID")
    private final Long id;

    @JsonProperty("FIRST_NAME")
    private final String firstName;

    @JsonProperty("LAST_NAME")
    private final String lastName;

    @JsonProperty("BOOKS")
    private final List<BookDto> books;

    // 标准构造器/getter
}

3.2. DTO特征

基于示例可总结以下特点:

不可变性

  • 最佳实践是保持DTO不可变
  • 实现方式:声明属性为final且不提供setter,或使用Lombok的@Value注解/Java 14+的record类型

验证能力
通过验证注解确保传输数据符合规范,防止无效数据污染领域模型

JSON映射注解
使用@JsonProperty等注解控制JSON属性与DTO字段的映射关系

4. 仓储、映射器与控制器

为展示实体与DTO的协作价值,补充完整代码:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

创建实体与DTO的映射器:

public class UserMapper {

    public static UserResponseDto toDto(User entity) {
        return new UserResponseDto(
          entity.getId(),
          entity.getFirstName(),
          entity.getLastName(),
          entity.getBooks().stream().map(UserMapper::toDto).collect(Collectors.toList())
        );
    }

    public static User toEntity(UserCreationDto dto) {
        return new User(
          dto.getFirstName(),
          dto.getLastName(),
          dto.getAddress(),
          dto.getBooks().stream().map(UserMapper::toEntity).collect(Collectors.toList())
        );
    }

    public static BookDto toDto(Book entity) {
        return new BookDto(entity.getName(), entity.getAuthor());
    }

    public static Book toEntity(BookDto dto) {
        return new Book(dto.getName(), dto.getAuthor());
    }
}

💡 复杂模型可使用MapStruct等工具避免样板代码

最后实现控制器:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping
    public List<UserResponseDto> getUsers() {
        return userRepository.findAll().stream().map(UserMapper::toDto).collect(Collectors.toList());
    }

    @PostMapping
    public UserResponseDto createUser(@RequestBody UserCreationDto userCreationDto) {
        return UserMapper.toDto(userRepository.save(UserMapper.toEntity(userCreationDto)));
    }
}

⚠️ 注意:findAll()在大数据量时可能影响性能,建议添加分页机制。

5. 为何需要同时使用实体和DTO?

5.1. 关注点分离

  • 实体:紧密耦合数据库模式和领域操作
  • DTO:专为数据传输设计

在六边形架构等范式中,领域模型层完全解耦技术细节,使核心业务逻辑独立于数据库/框架实现。

5.2. 敏感数据隐藏

实体可能包含敏感信息或内部逻辑,DTO作为屏障确保仅向客户端暴露安全且必要的数据

5.3. 性能优化

DTO模式(Martin Fowler提出)的核心是将多个参数打包为单次调用

  • 避免多次网络请求获取零散数据
  • 通过GraphQL等技术实现客户端按需查询

典型场景:将关联数据聚合到DTO中单次传输,减少网络开销。

6. 总结

实体与DTO承担不同职责且差异显著

  • 实体:领域核心,包含状态、行为和持久化逻辑
  • DTO:数据传输载体,注重安全性和性能

二者结合使用能确保数据安全、关注点分离和高效管理,构建更健壮、可维护的软件系统。

完整代码示例见:GitHub仓库


原始标题:Differences Between Entities and DTOs