不要在REST API中公開您的JPA實體 - Thorben Janssen

banq發表於2019-12-10

在REST API中公開實體,還是使用DTO類?(banq注:如果瞭解單一職責或DDD和Clean架構,基礎設施應該和業務邏輯分離,API JPA等屬於不同的基礎設施,應該都和領域物件分離)

這些問題以及由此引發的所有討論有兩個主要原因:

  1. 實體是POJO。通常看起來,它們可以輕鬆地序列化和反序列化為JSON文件。如果真的很容易實現,那麼REST端點的實現將變得非常簡單。
  2. 在API公開實體會和永續性模型之間建立牢固的耦合。兩種模型之間的任何差異都會帶來額外的複雜性,因此您需要找到一種彌合兩者之間差距的方法。不幸的是,您的API和永續性模型之間總是存在差異。最明顯的是實體之間關聯的處理。

即使很容易公開您的實體,但對於複雜度至少中等以上的所有應用程式以及需要長時間支援的所有應用程式,都應避免公開實體。在API上公開實體使您無法在設計API時實現一些最佳做法;它降低了實體類可讀性,減慢了應用程式的速度,並使實現真正的REST體系結構變得困難。

可以通過設計DTO類來避免所有這些問題,然後在API上進行序列化和反序列化。這要求您在DTO和內部資料結構之間實現對映。但是,如果您考慮在API中公開實體的所有弊端,那是值得的。

隱藏實施細節

作為一般的最佳實踐,您的API不應公開您應用程式的任何實現細節。您用來保留資料的結構就是這樣的細節。在您的API中公開您的實體顯然不遵循這種最佳做法。

幾乎每次我在討論中提出這個論點時,都會有人懷疑地抬起眉毛或直接問這是否真的有那麼大的意義。

好吧,如果您希望能夠在不更改API的情況下新增,刪除或更改實體的任何屬性,或者要在不更改資料庫的情況下更改REST終結點返回的資料,這只是一個大問題。

換句話說:是的,將API與持久層分開是實現可維護應用程式所必需的。如果您不這樣做,則REST API的每次更改都會影響您的實體模型,反之亦然。這意味著您的API和持久層將不再能夠彼此獨立地發展。

不要使用其他註釋來誇大您的實體

而且,如果您考慮僅在實體與REST端點的輸入或返回值完美匹配時才公開實體,那麼請注意,您需要為JSON序列化和反序列化新增其他註釋。

大多數實體對映已經需要幾個註釋。為您的JSON對映新增其他類會使實體類更加難以理解。最好保持簡單,將實體類與用於序列化和反序列化JSON文件的類分開。

API和JPA處理關聯關係不同

不在API中公開實體的另一個論點是實體之間關聯的處理。持久層和API對待它們的方式有所不同。如果要實現REST API,則尤其如此。

對於JPA和Hibernate,通常使用由實體屬性表示的託管關聯。這樣一來,您就可以輕鬆地在查詢中加入實體,並可以使用實體屬性遍歷業務程式碼中的關聯。根據配置的訪存型別和查詢,此關聯可以完全初始化,也可以在第一次訪問時延遲獲取。

在REST API中,您以不同的方式處理這些關聯。正確的方法是為每個關聯提供一個連結。羅伊·菲爾丁(Roy Fielding)將其描述為HATEOAS。它是REST體系結構的重要組成部分之一。但是大多數團隊決定要麼根本不對關聯建模,要麼只包括​​id引用。

連結和ID參考提供了類似的挑戰。將實體序列化為JSON文件時,您需要獲取關聯的實體併為每個實體建立引用。在反序列化期間,您需要獲取引用併為其獲取實體。根據所需查詢的數量,這可能會使您的應用程式變慢。

這就是為什麼團隊在序列化和反序列化期間經常排除關聯的原因。這對於您的客戶端應用程式可能是可以的,但是如果您嘗試合併通過反序列化JSON物件建立的實體,則會造成問題。Hibernate期望託管關聯引用其他實體物件或動態建立的代理物件或特定於Hibernate的List或Set實現。但是,如果您反序列化JSON物件並忽略實體上的託管關聯,則關聯將設定為null。然後,您需要手動設定它們,否則Hibernate將從資料庫中刪除該關聯。

如您所見,管理關聯可能很棘手。不要誤會我的意思;這些問題都可以解決。但這需要額外的工作,而且如果您僅忘記其中之一,則會丟失一些資料。

設計您的API

公開API的另一個缺點是,大多數團隊將其用作未設計REST端點響應的藉口。它們僅返回序列化的實體物件。

但是,如果您未實現非常簡單的CRUD操作,則客戶很可能會從精心設計的響應中受益。以下是基本書店應用程式的一些示例:

  • 當您返回搜尋書的結果時,您可能只想返回書的標題和價格,書作者和出版商的名稱以及平均客戶評價。使用專門設計的JSON文件,您可以避免不必要的資訊,並嵌入作者,釋出者和平均評分的資訊,而不用提供指向他們的連結。
  • 當客戶請求有關一本書的詳細資訊時,響應很可能與實體的序列化表示非常相似。但是會有一些重要的差異。您的JSON文件可能包含書名,標題,其他說明以及有關這本書的其他資訊。但是,有些資訊您不想共享,例如批發價或該書的當前庫存。您可能還希望排除與本書作者和評論的關聯。

基於用例特定的DTO類建立這些不同的表示非常簡單。但是,基於實體物件圖進行相同操作要困難得多,並且很可能需要一些手動對映。

支援您的API的多個版本

如果您的應用程式使用了一段時間,則需要新增新的REST端點並更改現有的REST端點。如果不能總是同時更新所有客戶端,這將迫使您支援API的多個版本。

在API中公開實體的同時做到這一點是一項艱鉅的挑戰。然後,您的實體將成為當前使用的和舊的,已過時的,使用@Transient[url=https://thoughts-on-java.org/hibernate-tips-map-1-attribute-2-columns/]註釋的[/url]屬性的混合,這樣它們就不會持久化在資料庫中。

如果要公開DTO,則支援API的多個版本要容易得多。這將持久層與API分離開來,您可以嚮應用程式引入遷移層。這一層將將呼叫從舊API對映到新API所需的所有操作分開。這使您可以提供當前API的簡單有效的實現。而且,每當您停用舊API時,都可以刪除遷移層。

結論

如您所見,我不喜歡在API中公開實體的原因有很多。但我也同意,它們都不會產生無法解決的問題。這就是為什麼關於此主題的討論如此之多的原因。

如果您在團隊中進行討論,您需要問自己:您是否想花費更多的精力來解決所有這些問題,以避免實體和DTO類之間非常基本的對映?

以我的經驗,這是不值得的。我更喜歡將API與持久層分開,並實現一些基本的實體到DTO的對映。這使我的程式碼易於閱讀,並且使我可以靈活地更改應用程式的所有內部部分,而不必擔心任何客戶端。

相關文章