我原本是用俄文寫這篇文章的。因此,如果您的母語是英語,您可以透過此連結閱讀。
在過去一年左右的時間裡,我看到一些文章和演講表明 JOOQ 是 Hibernate 的現代且優越的替代品。參數通常包括:
首先聲明,我認為 JOOQ 是一個優秀的函式庫(具體來說是一個函式庫,而不是像 Hibernate 這樣的框架)。它擅長完成自己的任務——以靜態類型的方式使用 SQL,在編譯時捕獲大多數錯誤。
但是,當我聽到Hibernate 的時代已經過去,我們現在應該使用JOOQ 編寫所有內容時,在我看來,這聽起來像是在說關係資料庫的時代已經結束,我們現在應該只使用NoSQL 。聽起來很有趣嗎?然而,不久前,這樣的討論還相當嚴肅。
我認為問題在於對這兩個工具解決的核心問題的誤解。在這篇文章中,我的目的是澄清這些問題。我們將探索:
使用資料庫最簡單、最直覺的方法是交易腳本模式。簡而言之,您將所有業務邏輯組織為組合成單一交易的一組 SQL 命令。通常,類別中的每個方法代表一個業務操作,並且僅限於一個事務。
假設我們正在開發一個應用程序,允許演講者向會議提交演講(為簡單起見,我們只記錄演講的標題)。依照交易腳本模式,提交演講的方法可能如下所示(使用 JDBI for SQL):
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
在此程式碼中:
這裡存在潛在的競爭條件,但為了簡單起見,我們不會關注它。
這種方法的優點:
缺點:
如果您的服務具有非常簡單的邏輯並且預計不會隨著時間的推移變得更加複雜,那麼這種方法是有效的並且有意義的。然而,域通常更大。因此,我們需要一個替代方案。
領域模型模式的想法是我們不再將業務邏輯直接與 SQL 命令綁定。相反,我們建立域物件(在 Java 上下文中為類別)來描述行為並儲存有關域實體的資料。
在本文中,我們不會討論貧血模型和豐富模型之間的差異。如果您有興趣,我已經寫了一篇關於該主題的詳細文章。
業務場景(服務)應該僅使用這些對象,並避免與特定的資料庫查詢綁定。
當然,在現實中,我們可能會混合使用與網域物件的互動和直接資料庫查詢來滿足效能要求。在這裡,我們討論實現領域模型的經典方法,其中不違反封裝和隔離。
例如,如果我們正在討論實體“Speaker”和“Talk”,如前所述,域物件可能如下所示:
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
這裡,Speaker 類別包含提交演講的業務邏輯。資料庫互動抽象化出來,讓領域模型專注於業務規則。
假設這個儲存庫介面:
@AllArgsConstructor public class Speaker { private Long id; private String firstName; private String lastName; private List<Talk> talks; public Talk submitTalk(String title) { boolean experienced = countTalksByStatus(Status.ACCEPTED) >= 10; int maxSubmittedTalksCount = experienced ? 3 : 5; if (countTalksByStatus(Status.SUBMITTED) >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException( "Submitted talks count is maximum: " + maxSubmittedTalksCount); } Talk talk = Talk.newTalk(this, Status.SUBMITTED, title); talks.add(talk); return talk; } private long countTalksByStatus(Talk.Status status) { return talks.stream().filter(t -> t.getStatus().equals(status)).count(); } } @AllArgsConstructor public class Talk { private Long id; private Speaker speaker; private Status status; private String title; private int talkNumber; void setStatus(Function<Status, Status> fnStatus) { this.status = fnStatus.apply(this.status); } public enum Status { SUBMITTED, ACCEPTED, REJECTED } }
那麼SpeakerService可以這樣實作:
public interface SpeakerRepository { Speaker findById(Long id); void save(Speaker speaker); }
領域模型的優點:
總之,優點很多。然而,有一個重要的挑戰。有趣的是,在經常宣揚領域模型模式的領域驅動設計書籍中,這個問題要不是根本沒有提到,就是只是簡單提及。
問題是如何將域物件儲存到資料庫,然後讀取它們?換句話說,如何實作儲存庫?
現在,答案是顯而易見的。只需使用 Hibernate(或更好的是 Spring Data JPA)即可省去麻煩。但讓我們想像一下,我們正處於一個 ORM 框架尚未發明的世界。我們該如何解決這個問題呢?
為了實現SpeakerRepository,我還使用JDBI:
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
方法很簡單。對於每個儲存庫,我們編寫一個單獨的實現,該實作可以使用任何 SQL 庫(如 JOOQ 或 JDBI)與資料庫配合使用。
乍看之下(甚至可能是第二眼),這個解決方案可能看起來相當不錯。考慮一下:
現實世界中的事情會變得更加有趣,你可能會遇到這樣的場景:
最重要的是,隨著業務邏輯和領域物件的發展,您需要維護映射程式碼。
如果您嘗試自己處理這些要點,您最終會發現自己(驚訝!)正在編寫類似 Hibernate 的框架 - 或者更可能是一個更簡單的版本。
JOOQ 解決了編寫 SQL 查詢時缺乏靜態類型的問題。這有助於減少編譯階段的錯誤數量。透過直接從資料庫模式產生程式碼,對模式的任何更新都將立即顯示程式碼需要修復的位置(它根本無法編譯)。
Hibernate 解決了網域物件對應到關聯式資料庫的問題,反之亦然(從資料庫讀取資料並將其對應到網域物件)。
因此,爭論 Hibernate 更差或 JOOQ 更好是沒有意義的。這些工具是為不同的目的而設計的。如果您的應用程式是圍繞著事務腳本範例構建的,那麼 JOOQ 無疑是理想的選擇。但是,如果您想使用網域模型模式並避免使用 Hibernate,則必須在自訂儲存庫實作中享受手動對應的樂趣。當然,如果您的雇主付錢讓您建造另一個 Hibernate 殺手,那就沒有問題了。但最有可能的是,他們希望您專注於業務邏輯,而不是物件到資料庫映射的基礎設施代碼。
順便說一句,我相信 Hibernate 和 JOOQ 的組合對於 CQRS 效果很好。您有一個執行命令的應用程式(或其邏輯部分),例如 CREATE/UPDATE/DELETE 操作 - 這就是 Hibernate 非常適合的地方。另一方面,您有一個讀取資料的查詢服務。在這裡,JOOQ 表現出色。與 Hibernate 相比,它使得建立複雜查詢和最佳化它們變得更加容易。
這是真的。 JOOQ 可讓您產生包含用於從資料庫取得實體的標準查詢的 DAO。您甚至可以用您的方法擴展這些 DAO。此外,JOOQ 將產生可以使用 setter 填充的實體(類似於 Hibernate),並傳遞給 DAO 中的插入或更新方法。這不是像Spring Data嗎?
對於簡單的情況,這確實可行。然而,它與手動實現存儲庫沒有太大區別。問題類似:
所以,如果你想建立一個複雜的領域模型,你必須手動完成。如果沒有 Hibernate,映射的責任將完全落在您身上。當然,使用 JOOQ 比 JDBI 更愉快,但這個過程仍然是勞力密集的。
甚至 JOOQ 的創建者 Lukas Eder 在他的部落格中也提到,DAO 被添加到庫中是因為它是一種流行的模式,而不是因為他一定推薦使用它們。
感謝您閱讀這篇文章。我是 Hibernate 的忠實粉絲,認為它是一個優秀的框架。不過,我知道有些人可能會覺得 JOOQ 比較方便。我文章的重點是 Hibernate 和 JOOQ 不是競爭對手。如果這些工具帶來價值,甚至可以在同一產品中共存。
如果您對內容有任何意見或回饋,我很樂意討論。祝你度過富有成效的一天!
以上是JOOQ 不是 Hibernate 的替代品。他們解決不同的問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!