實體和 DTO 之間的差異
1. 概述
在軟體開發領域,實體和 DTO(資料傳輸物件)之間有明顯的差異。了解它們的精確角色和差異可以幫助我們建立更有效率和可維護的軟體。
在本文中,我們將探討實體和 DTO 之間的差異,並嘗試清楚地了解它們的用途以及何時在我們的軟體專案中使用它們。在介紹每個概念時,我們將使用 Spring Boot 和 JPA 繪製一個簡單的使用者管理應用程式。
2. 實體
實體是代表應用程式領域內的現實世界物件或概念的基本元件。它們通常直接對應於資料庫表或域物件。因此,它們的主要目的是封裝和管理這些物件的狀態和行為。
2.1.實體範例
讓我們為我們的專案創建一些實體,代表擁有多本書的使用者。我們先建立Book
實體:
@Entity
@Table(name = "books")
public class Book {
@Id
private String name;
private String author;
// standard constructors / getters / setters
}
現在,我們需要定義我們的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.實體特徵
在我們的實體中,我們可以辨識出一些獨特的特徵。首先,實體通常包含物件關係映射(ORM)註解。例如, @Entity
註解將類別標記為實體,從而在 Java 類別和資料庫表之間建立直接連結。
@Table
註解用於指定與實體關聯的資料庫表的名稱。此外, @Id
註解定義一個欄位作為主鍵。這些 ORM 註解簡化了資料庫映射的過程。
此外,實體通常需要與其他實體建立關係,反映現實世界概念之間的關聯。一個常見的範例是我們用來定義使用者與其擁有的書籍之間的一對多關係的@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;
// standard constructors / getters
}
對於使用者來說,我們定義兩個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特性
根據我們的範例,我們可以識別一些特殊性:不變性、驗證註解和 JSON 映射註解。
使 DTO 不可變是最佳實務。不變性確保正在傳輸的資料在傳輸過程中不會被意外更改。實現此目的的一種方法是將所有屬性聲明為final
屬性並且不實現setters
。或者,Java 14 中引入的來自Lombok或Java records
@Value
註解提供了一種創建不可變 DTO 的簡潔方法。
接下來, DTO 還可以從驗證中受益,以確保透過 DTO 傳輸的資料符合特定標準。這樣,我們就可以在數據傳輸過程的早期檢測並拒絕無效數據,防止不可靠資訊對域的污染。
此外,我們通常可以在 DTO 中找到JSON 映射註釋,將 JSON 屬性對應到 DTO 的欄位。例如, @JsonProperty
註解允許我們指定 DTO 的 JSON 名稱。
4. 儲存庫、映射器和控制器
為了演示在我們的應用程式中使用實體和 DTO 表示資料的實用性,我們需要完成我們的程式碼。我們首先為我們的User
實體建立一個儲存庫:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
接下來,我們將繼續建立一個映射器,以便能夠從一個映射器轉換為另一個映射器:
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());
}
}
在我們的範例中,我們手動完成了實體和 DTO 之間的對應。對於更複雜的模型,為了避免樣板程式碼,我們可以使用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)));
}
}
5. 為什麼我們需要實體和 DTO?
5.1.關注點分離
在我們的範例中,實體與資料庫模式和特定於網域的操作緊密相關。另一方面, DTO 僅為資料傳輸目的而設計。
在某些架構範例中,例如六角形架構,我們可能會發現一個附加層,通常稱為模型或領域模型。該層的關鍵目的是將域與任何侵入技術完全解耦。這樣,核心業務邏輯就保持獨立於資料庫、框架或外部系統的實作細節。
5.2.隱藏敏感數據
在與外部客戶端或系統打交道時,控制向外界公開哪些資料至關重要。實體可能包含敏感資訊或業務邏輯,這些資訊或業務邏輯應對外部消費者隱藏。 DTO 充當屏障,幫助我們僅向客戶公開安全且相關的資料。
5.3.表現
Martin Fowler 引入的 DTO 模式涉及在一次呼叫中批次處理多個參數。我們可以將相關資料捆綁到 DTO 並在單一請求中傳輸,而不是進行多次呼叫來獲取單獨的資料。這種方法減少了與多個網路呼叫相關的開銷。
實現 DTO 模式的一種方法是透過GraphQL ,它允許客戶端指定所需的數據,從而允許在單一請求中進行多個查詢。
六,結論
正如我們在整篇文章中所了解的,實體和 DTO 具有不同的角色,並且可能非常不同。實體和 DTO 的結合確保了複雜軟體系統中的資料安全、關注點分離和高效的資料管理。這種方法可以帶來更強大、更可維護的軟體解決方案。
與往常一樣,原始碼可以在 GitHub 上取得。