嘿哈布爾!
我們繼續研究這個主題
在這篇相當晚的文章中,我將解釋為什麼我相信在幾乎所有情況下,應用程式中的資料模型應該「從資料庫」設計,而不是「從Java 的功能」(或您使用的任何客戶端語言)與)一起工作。 如果採用第二種方法,一旦你的專案開始發展,你就會陷入一條漫長的痛苦之路。
這篇文章是根據
Reddit 上有趣的討論
程式碼生成
讓我感到多麼驚訝的是,竟然有一小部分用戶在熟悉了 jOOQ 後,對 jOOQ 嚴重依賴原始碼生成來運行這一事實感到憤怒。 沒有人會阻止您使用您認為合適的 jOOQ,或強迫您使用程式碼產生。 但是使用 jOOQ 的預設(如手冊中所述)方式是從(遺留)資料庫模式開始,使用 jOOQ 程式碼產生器對其進行反向工程,從而獲得一組表示表的類,然後編寫類型- 對這些表的安全查詢:
for (Record2<String, String> record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Информация о типах выведена на
// основании сгенерированного кода, на который ссылается приведенное
// ниже условие SELECT
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ сгенерированные имена
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}
程式碼可以在程序集外部手動生成,也可以在每個程序集手動生成。 例如,這種再生可以緊接在
原始碼生成
這些程式碼產生方法(手動和自動)有各種原理、優點和缺點,我不會在本文中詳細討論。 但是,總的來說,生成的程式碼的全部意義在於,它允許我們在 Java 中重現我們認為理所當然的“真理”,無論是在我們的系統內還是在系統外。 從某種意義上說,這就是編譯器在生成字節碼、機器碼或其他形式的源代碼時所做的事情 - 我們得到了另一種語言的“真相”表示,無論具體原因如何。
有很多這樣的程式碼產生器。 例如,
- 有一些事實(內部或外部)—例如規格、資料模型等。
- 我們需要用我們的程式語言來本地表達這一事實。
此外,幾乎總是建議產生這樣的表示以避免冗餘。
類型提供者和註釋處理
注意:另一種更現代、更具體的為 jOOQ 生成程式碼的方法是使用類型提供程序,
從某種意義上說,這裡發生的事情與第一種情況相同,除了:
- 你看不到產生的程式碼(也許這種情況對某些人來說似乎不那麼令人反感?)
- 您必須確保可以提供類型,即“true”必須始終可用。 對於 Lombok 來說這很容易,它註釋了「真相」。 對於依賴持續可用的即時連線的資料庫模型來說,情況稍微複雜一些。
程式碼生成有什麼問題?
除瞭如何最好地運行程式碼生成(手動或自動)這個棘手問題之外,我們還必須提到,有些人認為根本不需要程式碼生成。 我最常遇到的這種觀點的理由是,很難建立建造管道。 是的,這確實很難。 會產生額外的基礎設施成本。 如果您剛開始使用特定產品(無論是 jOOQ、JAXB 還是 Hibernate 等),則設定生產環境需要時間,您寧願花時間學習 API 本身,以便從中獲取價值。
如果理解生成器結構的成本太高,那麼 API 在程式碼產生器的可用性方面確實做得很差(後來發現用戶自訂也很複雜)。 對於任何此類 API,可用性應該是最高優先順序。 但這只是反對程式碼生成的理由之一。 否則,編寫內部或外部事實的本地表示絕對是完全手動的。
許多人會說他們沒有時間做這一切。 他們的超級產品的最後期限已經到了。 有一天我們會整理組裝傳送帶,我們會有時間的。 我會回答他們:
但在 Hibernate/JPA 中編寫 Java 程式碼非常容易。
真的。 對 Hibernate 及其用戶來說,這既是福也是禍。 在 Hibernate 中,您可以簡單地編寫幾個實體,如下所示:
@Entity
class Book {
@Id
int id;
String title;
}
並且幾乎一切都準備好了。 現在由 Hibernate 產生複雜的“細節”,說明如何在 SQL“方言”的 DDL 中準確定義該實體:
CREATE TABLE book (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
title VARCHAR(50),
CONSTRAINT pk_book PRIMARY KEY (id)
);
CREATE INDEX i_book_title ON book (title);
...並開始運行應用程式。 這是一個快速入門並嘗試不同事物的絕佳機會。
不過,請允許我。 我在撒謊。
- Hibernate 真的會強制執行這個命名主鍵的定義嗎?
- Hibernate會在TITLE中建立索引嗎? – 我確信我們會需要它。
- Hibernate 會在身分規範中準確地標識這個鍵嗎?
可能不會。 如果您從頭開始開發項目,那麼在添加必要的註釋後,只需丟棄舊資料庫並生成新資料庫總是很方便的。 因此,Book 實體最終將採用以下形式:
@Entity
@Table(name = "book", indexes = {
@Index(name = "i_book_title", columnList = "title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
String title;
}
涼爽的。 再生。 同樣,在這種情況下,一開始會很容易。
但你稍後必須付費
遲早你將不得不投入生產。 到那時這個模型將停止運作。 因為:
在生產中,如果有必要,將不再可能丟棄舊資料庫並從頭開始。 您的資料庫將成為遺產。
從現在開始直到永遠你都必須寫
因此,一旦投入生產,您將需要熱補丁。 而且它們需要很快投入生產。 由於您還沒準備好,也沒有組織順利的生產遷移管道,所以您對所有內容進行了瘋狂的修補。 然後你就不再有時間正確地做每件事。 你批評 Hibernate,因為這總是別人的錯,而不是你......
相反,事情本來可以從一開始就採取完全不同的方式。 例如,在自行車上安裝圓形輪子。
資料庫優先
資料庫模式中真正的「真相」及其「主權」位於資料庫內部。 該模式僅在資料庫本身中定義,而不是在其他地方定義,並且每個客戶端都有該模式的副本,因此強制遵守模式及其完整性是非常有意義的,在資料庫中正確執行它- 資訊所在的位置存儲。
這是古老的、甚至是陳腔濫調的智慧。 主鍵和唯一鍵都很好。 外鍵很好。 檢查限制是好的。
而且,這還不是全部。 例如,使用 Oracle,您可能需要指定:
- 你的表在哪個表空間?
- 它的 PCTFREE 值是多少?
- 您的序列中的快取大小是多少(在 id 後面)
這在小型系統中可能並不重要,但您不必等到進入大數據領域 - 您可以更快地開始受益於供應商提供的儲存最佳化(如上面提到的最佳化)。 我見過的 ORM(包括 jOOQ)都沒有提供對您可能想要在資料庫中使用的全套 DDL 選項的存取。 ORM 提供了一些幫助您編寫 DDL 的工具。
但最終,精心設計的電路是用 DDL 手寫的。 任何產生的 DDL 只是它的近似值。
那麼客戶模型呢?
如上所述,在客戶端上,您將需要資料庫架構的副本,即客戶端視圖。 不用說,這個客戶端視圖必須與實際模型同步。 實現這一目標的最佳方法是什麼? 使用程式碼產生器。
所有資料庫都透過 SQL 提供元資訊。 以下是如何使用不同的 SQL 方言從資料庫中取得所有表格:
-- H2, HSQLDB, MySQL, PostgreSQL, SQL Server
SELECT table_schema, table_name
FROM information_schema.tables
-- DB2
SELECT tabschema, tabname
FROM syscat.tables
-- Oracle
SELECT owner, table_name
FROM all_tables
-- SQLite
SELECT name
FROM sqlite_master
-- Teradata
SELECT databasename, tablename
FROM dbc.tables
這些查詢(或類似的查詢,取決於您是否還必須考慮視圖、物化視圖、表值函數)也透過呼叫來執行 DatabaseMetaData.getTables()
根據此類查詢的結果,產生資料庫模型的任何客戶端表示都相對容易,無論您在客戶端上使用什麼技術。
- 如果您使用 JDBC 或 Spring,則可以建立一組字串常數
- 如果您使用JPA,您可以自己產生實體
- 如果您使用jOOQ,則可以產生jOOQ元模型
根據您的客戶端 API(例如 jOOQ 或 JPA)提供的功能數量,產生的元模型可能非常豐富且完整。 以隱式連接的可能性為例,
現在任何資料庫增量都會自動更新客戶端程式碼。 想像一下例如:
ALTER TABLE book RENAME COLUMN title TO book_title;
你真的願意做兩次這份工作嗎? 不可能。 只需提交 DDL,透過建置管道運行它,並取得更新的實體:
@Entity
@Table(name = "book", indexes = {
// Вы об этом задумывались?
@Index(name = "i_book_title", columnList = "book_title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
@Column("book_title")
String bookTitle;
}
或更新後的 jOOQ 類別。 大多數 DDL 變更也會影響語義,而不僅僅是語法。 因此,查看編譯的程式碼以了解哪些程式碼將(或可能)受到資料庫增量的影響可能很有用。
唯一的真相
無論您使用什麼技術,總有一個模型是某些子系統的唯一事實來源- 或者,至少,我們應該努力實現這一點並避免這種企業混亂,即“事實”無處不在,但同時又無處可尋。 一切都可以變得簡單得多。 如果您只是與其他系統交換 XML 文件,則只需使用 XSD。 查看來自 jOOQ 的 XML 形式的 INFORMATION_SCHEMA 元模型:
- XSD很好理解
- XSD 很好地標記了 XML 內容,並允許使用所有用戶端語言進行驗證
- XSD 版本完善並具有進階向後相容性
- 可以使用 XJC 將 XSD 轉換為 Java 程式碼
最後一點很重要。 當使用 XML 訊息與外部系統通訊時,我們希望確保訊息有效。 使用 JAXB、XJC 和 XSD 可以輕鬆實現這一點。 如果認為透過「Java 優先」的設計方法(將訊息作為 Java 物件),它們可以以某種方式連貫地映射到 XML 並發送到另一個系統以供使用,那真是太瘋狂了。 以這種方式產生的 XML 品質非常差,沒有文件記錄,而且難以開發。 如果這樣的介面有服務等級協定(SLA),我們會立即把它搞砸。
老實說,這就是 JSON API 經常發生的情況,但那是另一個故事了,下次我會吵架...
資料庫:它們是同一件事
使用資料庫時,您會意識到它們基本上都是相似的。 基地擁有其數據並必須管理該方案。 對模式所做的任何修改都必須直接在 DDL 中實現,以便更新單一事實來源。
當來源更新發生時,所有客戶端也必須更新其模型副本。 某些客戶端可以使用 jOOQ 和 Hibernate 或 JDBC(或兩者)以 Java 編寫。 其他客戶端可以用 Perl 編寫(我們只是祝他們好運),而其他客戶端可以用 C# 編寫。 沒關係。 主要模型在資料庫中。 使用 ORM 產生的模型通常品質較差、文件缺乏且難以開發。
所以不要犯錯。 從一開始就不要犯錯。 從資料庫工作。 建置可以自動化的部署管道。 啟用程式碼產生器可以輕鬆複製資料庫模型並將其轉儲到客戶端。 不再擔心代碼產生器。 他們很好。 有了他們,您將變得更有生產力。 您只需要從一開始就花一點時間來設定它們,然後幾年的生產力提高就在等著您,這將構成您專案的歷史。
以後先別謝我。
澄清
需要明確的是:本文絕不主張您需要改變整個系統(即域、業務邏輯等)以適應您的資料庫模型。 我在本文中要說的是,與資料庫互動的客戶端程式碼應該在資料庫模型的基礎上運行,這樣它本身就不會以「一流」的狀態重現資料庫模型。 此邏輯通常位於客戶端的資料存取層。
在某些地方仍然保留的兩級體系結構中,這樣的系統模型可能是唯一可能的。 然而,在大多數系統中,資料存取層在我看來是封裝資料庫模型的「子系統」。
例外
每條規則都有例外,我已經說過資料庫優先和原始碼生成方法有時可能不合適。 以下是一些此類例外情況(可能還有其他例外情況):
- 當模式未知且需要被發現時。 例如,您是幫助使用者導航任何圖表的工具的提供者。 啊。 這裡沒有程式碼生成。 但資料庫仍然是第一位的。
- 當必須動態生成電路來解決某些問題時。 這個例子看起來像是該模式的一個有點奇特的版本
實體屬性值 ,即,您實際上沒有明確定義的方案。 在這種情況下,您通常甚至無法確定 RDBMS 是否適合您。
例外本質上是例外。 在大多數涉及使用 RDBMS 的情況下,模式是預先已知的,它駐留在 RDBMS 中,並且是「真相」的唯一來源,並且所有用戶端都必須取得從中派生的副本。 理想情況下,您需要使用程式碼產生器。
來源: www.habr.com