Differences Between Entities and DTOs – 实体与 DTO 的区别

最后修改: 2023年 12月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In the realm of software development, there is a clear distinction between entities and DTOs (Data Transfer Objects). Understanding their precise roles and differences can help us build more efficient and maintainable software.

在软件开发领域,实体和 DTOs(数据传输对象)之间有着明显的区别。了解它们的确切作用和区别有助于我们构建更高效、可维护的软件。

In this article, we’ll explore the differences between entities and DTOs and try to offer a clear understanding of their purpose, and when to employ them in our software projects. While going through each concept, we’ll sketch a trivial application of user management, using Spring Boot and JPA.

在本文中,我们将探讨实体和 DTO 之间的区别,并尝试清楚地了解它们的用途,以及何时在我们的软件项目中使用它们。在了解每个概念的同时,我们将使用 Spring Boot 和 JPA勾画一个微不足道的用户管理应用程序。

2. Entities

2.实体

Entities are fundamental components that represent real-world objects or concepts within the domain of our application. They often correspond directly to database tables or domain objects. Therefore, their primary purpose is to encapsulate and manage the state and behavior of these objects.

实体是基本组件,代表我们应用程序领域内的现实世界对象或概念。它们通常直接对应于数据库表或域对象。因此,实体的主要目的是封装和管理这些对象的状态和行为。

2.1. Entity Example

2.1.实体示例

Let’s create some entities for our project, representing a user that has multiple books. We’ll start by creating the Book entity:

让我们为项目创建一些实体,代表拥有多本书的用户。我们首先创建 Book 实体:

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

    @Id
    private String name;
    private String author;

    // standard constructors / getters / setters
}

Now, we need to define our User entity:

现在,我们需要定义 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);
    }

    // standard constructors / getters / setters
}

2.2. Entity Characteristics

2.2.实体特征

In our entities, we can identify some distinctive characteristics. In the first place, entities commonly incorporate Object-Relational Mapping (ORM) annotations. For instance, the @Entity annotation marks the class as an entity, creating a direct link between a Java class and a database table.

在我们的实体中,我们可以发现一些与众不同的特征。首先,实体通常包含对象关系映射(ORM)注解。例如,@Entity 注解将类标记为实体,从而在 Java 类和数据库表之间建立了直接链接。

The @Table annotation is used to specify the name of the database table associated with the entity. Additionally, the @Id annotation defines a field as the primary key. These ORM annotations simplify the process of database mapping.

@Table 注解用于指定与实体相关联的数据库表的名称。此外,@Id 注解将一个字段定义为主键。这些 ORM 注释简化了数据库映射过程。

Moreover, entities often need to establish relationships with other entities, reflecting associations between real-world concepts. A common example is the @OneToMany annotation we’ve used to define a one-to-many relationship between a user and the books he owns.

此外,实体经常需要与其他实体建立关系,以反映现实世界中概念之间的关联。一个常见的例子是 @OneToMany 注解,我们用它来定义用户和他拥有的书籍之间的一对多关系。

Furthermore, entities don’t have to serve solely as passive data objects but can also contain domain-specific business logic. For instance, let’s consider a method such as getNameOfMostOwnedBook(). This method, residing within the entity, encapsulates domain-specific logic to find the name of the book the user owns the most. This approach aligns with OOP principles and the DDD approach by keeping domain-specific operations within entities, fostering code organization and encapsulation.

此外,实体不一定只能作为被动的数据对象,也可以包含特定域的业务逻辑。例如,让我们考虑一个方法,如 getNameOfMostOwnedBook()。该方法位于实体中,封装了特定于领域的逻辑,用于查找用户拥有最多的书籍的名称。这种方法符合 OOP 原则和 DDD 方法,它将特定于领域的操作保留在实体中,促进了代码的组织和封装。

Additionally, entities may incorporate other particularities, such as validation constraints or lifecycle methods.

此外,实体还可能包含其他特殊性,例如 验证约束 生命周期方法

3. DTOs

3. DTOs

DTOs primarily act as pure data carriers, without having any business logic. They’re used to transmit data between different applications or parts of the same application.

DTO 主要充当纯粹的数据载体,不包含任何业务逻辑。它们用于在不同应用程序或同一应用程序的不同部分之间传输数据。

In simple applications, it’s common to use the domain objects directly as DTOs. However, as applications grow in complexity, exposing the entire domain model to external clients may become less desirable from a security and encapsulation perspective.

在简单的应用程序中,将域对象直接用作 DTO 是很常见的。但是,随着应用程序复杂性的增加,从安全和封装的角度来看,将整个域模型暴露给外部客户端可能就不那么可取了。

3.1. DTO Example

3.1.DTO 示例

To keep our application as simple as possible, we will implement only the functionalities of creating a new user and retrieving the current users. To do so, let’s start by creating a DTO to represent a book:

为了使我们的应用程序尽可能简单,我们将只实现创建新用户和检索当前用户的功能。为此,我们先创建一个 DTO 来表示一本书:

public class BookDto {

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

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

    // standard constructors / getters
}

For the user, let’s define two DTOs. One is designed for the creation of a user, while the second one is tailored for response purposes:

对于用户,让我们定义两个 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;

    // standard constructors / getters
}
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;

    // standard constructors / getters
}

3.2. DTO Characteristics

3.2.DTO 的特点

Based on our examples, we can identify a few particularities: immutability, validation annotations, and JSON mapping annotations.

根据我们的示例,我们可以确定一些特殊性:不变性、验证注释和 JSON 映射注释。

Making DTOs immutable is a best practice. Immutability ensures that the data being transported is not accidentally altered during its journey. One way to achieve this is by declaring all properties as final and not implementing setters. Alternatively, the @Value annotation from Lombok or Java records, introduced in Java 14, offers a concise way to create immutable DTOs.

使 DTO 不可变是一种最佳实践。不可变性可确保传输的数据在传输过程中不会被意外更改。实现这一点的方法之一是将所有属性声明为final,并且不实现setters。另外,Java 14 中引入的 @Value 注解LombokJava records 提供了创建不可变 DTO 的简洁方法。

Moving on, DTOs can also benefit from validation, to ensure that the data transferred via the DTOs meets specific criteria. This way, we can detect and reject invalid data early in the data transfer process, preventing the pollution of the domain with unreliable information.

此外,DTO 还可受益于验证,以确保通过 DTO 传输的数据符合特定标准。这样,我们就可以在数据传输过程的早期检测并拒绝无效数据,防止不可靠的信息污染域

Moreover, we may usually find JSON mapping annotations in DTOs, to map JSON properties to the fields of our DTOs. For example, the @JsonProperty annotation allows us to specify the JSON names of our DTOs.

此外,我们通常可以在 DTO 中找到 JSON 映射注解以将 JSON 属性映射到我们的 DTO 字段。例如,@JsonProperty 注解允许我们指定 DTO 的 JSON 名称。

4. Repository, Mapper, and Controller

4.存储库、映射器和控制器

To demonstrate the utility of having both entities and DTOs represent data within our application, we need to complete our code. We’ll start by creating a repository for our User entity:

为了演示在应用程序中使用实体和 DTO 表示数据的实用性,我们需要完成我们的代码。首先,我们将为 User 实体创建一个存储库:

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

Next, we’ll proceed with creating a mapper to be able to convert from one to another:

接下来,我们将创建一个映射器,以便从一个映射器转换到另一个映射器:

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());
    }
}

In our example, we’ve done the mapping manually between entities and DTOs. For more complex models, to avoid boilerplate code, we could’ve used tools like MapStruct.

在我们的示例中,我们手动完成了实体和 DTO 之间的映射。对于更复杂的模型,为了避免模板代码,我们可以使用像 MapStruct 这样的工具。

Now, we only need to create the controller:

现在,我们只需创建控制器:

@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)));
    }
}

Note that using findAll() can impact performance with large collections. Including something like pagination can help in these cases.

请注意,使用 findAll() 会影响大型集合的性能。在这些情况下,加入类似 分页的内容会有所帮助。

5. Why Do We Need Both Entities and DTOs?

5.为什么需要实体和 DTO?

5.1. Separation of Concerns

5.1.关注点的分离

In our example, the entities are closely tied to the database schema and domain-specific operations. On the other hand, DTOs are designed only for data transfer purposes.

在我们的示例中,实体与数据库模式和特定域操作紧密相关。另一方面,DTO 仅用于数据传输目的

In some architectural paradigms, such as hexagonal architecture, we may find an additional layer, commonly referred to as the Model or Domain Model. This layer serves the crucial purpose of totally decoupling the domain from any intrusive technology. This way, the core business logic remains independent of the implementation details of databases, frameworks, or external systems.

在某些架构范例中,例如六边形架构,我们可能会发现一个额外的层,通常称为模型或领域模型。该层的重要作用是将领域与任何侵入性技术完全解耦。这样,核心业务逻辑就不受数据库、框架或外部系统实施细节的影响。

5.2. Hiding Sensitive Data

5.2.隐藏敏感数据

When dealing with external clients or systems, controlling what data is exposed to the outside world is essential. Entities may contain sensitive information or business logic that should remain hidden from external consumers. DTOs act as a barrier that helps us expose only safe and relevant data to the clients.

在与外部客户或系统打交道时,控制哪些数据会被暴露给外部世界是至关重要的。实体可能包含敏感信息或业务逻辑,这些信息或逻辑应对外部消费者保持隐蔽。DTO 就像一道屏障,帮助我们只向客户暴露安全和相关的数据。

5.3. Performance

5.3.性能

The DTO pattern, as introduced by Martin Fowler, involves batching up multiple parameters in a single call. Instead of making multiple calls to fetch individual pieces of data, we can bundle related data into a DTO and transmit it in a single request. This approach reduces the overhead associated with multiple network calls.

马丁-福勒(Martin Fowler)介绍的 DTO 模式涉及在一次调用中批量处理多个参数。我们可以将相关数据捆绑到一个 DTO 中,并在单个请求中传输,而不是进行多次调用来获取单个数据块。这种方法减少了与多次网络调用相关的开销。

One way of implementing the DTO pattern is through GraphQL, which allows the client to specify the data it desires, allowing multiple queries in a single request.

实现 DTO 模式的一种方法是通过 GraphQL 来实现,它允许客户端指定所需的数据,并允许在单个请求中进行多次查询。

6. Conclusion

6.结论

As we’ve learned throughout this article, entities and DTOs have different roles and can be very distinct. The combination of both entities and DTOs ensures data security, separation of concerns, and efficient data management in complex software systems. This approach leads to more robust and maintainable software solutions.

正如我们在本文中所了解到的,实体和 DTO 具有不同的作用,并且可以非常明显地区别开来。实体和 DTO 的结合可确保复杂软件系统中的数据安全、关注点分离和高效数据管理。这种方法可使软件解决方案更健壮、更易于维护。

As always, the source code is available over on GitHub.

与往常一样,源代码可在 GitHub 上获取