真理第一,或者說為什麼一個系統需要基於資料庫設計來設計

嘿哈布爾!

我們繼續研究這個主題 Java的 и 彈簧,包括資料庫層級。 今天,我們邀請您了解為什麼在設計大型應用程式時,資料庫結構而不是 Java 程式碼具有決定性的重要性,這是如何完成的,以及此規則有哪些例外情況。

在這篇相當晚的文章中,我將解釋為什麼我相信在幾乎所有情況下,應用程式中的資料模型應該「從資料庫」設計,而不是「從Java 的功能」(或您使用的任何客戶端語言)與)一起工作。 如果採用第二種方法,一旦你的專案開始發展,你就會陷入一條漫長的痛苦之路。

這篇文章是根據 一個問題,在 Stack Overflow 上給。

Reddit 上有趣的討論 /r/java и / r /編程.

程式碼生成

讓我感到多麼驚訝的是,竟然有一小部分用戶在熟悉了 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)) {
    // ...
}

程式碼可以在程序集外部手動生成,也可以在每個程序集手動生成。 例如,這種再生可以緊接在 Flyway資料庫遷移,也可以手動或自動完成.

原始碼生成

這些程式碼產生方法(手動和自動)有各種原理、優點和缺點,我不會在本文中詳細討論。 但是,總的來說,生成的程式碼的全部意義在於,它允許我們在 Java 中重現我們認為理所當然的“真理”,無論是在我們的系統內還是在系統外。 從某種意義上說,這就是編譯器在生成字節碼、機器碼或其他形式的源代碼時所做的事情 - 我們得到了另一種語言的“真相”表示,無論具體原因如何。

有很多這樣的程式碼產生器。 例如, XJC可以根據XSD或WSDL檔案產生Java程式碼。 原理總是一樣的:

  • 有一些事實(內部或外部)—例如規格、資料模型等。
  • 我們需要用我們的程式語言來本地表達這一事實。

此外,幾乎總是建議產生這樣的表示以避免冗餘。

類型提供者和註釋處理

注意:另一種更現代、更具體的為 jOOQ 生成程式碼的方法是使用類型提供程序, 因為它們是在 F# 中實現的。 在這種情況下,程式碼是由編譯器產生的,實際上是在編譯階段。 原則上,此類程式碼不以原始碼形式存在。 Java 有類似的工具,儘管沒有那麼優雅 - 例如註釋處理器, 龍目島.

從某種意義上說,這裡發生的事情與第一種情況相同,除了:

  • 你看不到產生的程式碼(也許這種情況對某些人來說似乎不那麼令人反感?)
  • 您必須確保可以提供類型,即“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;
}

涼爽的。 再生。 同樣,在這種情況下,一開始會很容易。

但你稍後必須付費

遲早你將不得不投入生產。 到那時這個模型將停止運作。 因為:

在生產中,如果有必要,將不再可能丟棄舊資料庫並從頭開始。 您的資料庫將成為遺產。

從現在開始直到永遠你都必須寫 DDL遷移腳本,例如使用Flyway。 在這種情況下,您的實體會發生什麼事? 您可以手動調整它們(從而使您的工作量加倍),也可以告訴 Hibernate 為您重新生成它們(以這種方式產生的滿足您期望的可能性有多大?)無論哪種方式,您都會失敗。

因此,一旦投入生產,您將需要熱補丁。 而且它們需要很快投入生產。 由於您還沒準備好,也沒有組織順利的生產遷移管道,所以您對所有內容進行了瘋狂的修補。 然後你就不再有時間正確地做每件事。 你批評 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,或使用 jOOQ 元模組。

根據此類查詢的結果,產生資料庫模型的任何客戶端表示都相對容易,無論您在客戶端上使用什麼技術。

  • 如果您使用 JDBC 或 Spring,則可以建立一組字串常數
  • 如果您使用JPA,您可以自己產生實體
  • 如果您使用jOOQ,則可以產生jOOQ元模型

根據您的客戶端 API(例如 jOOQ 或 JPA)提供的功能數量,產生的元模型可能非常豐富且完整。 以隱式連接的可能性為例, jOOQ 3.11中引入,它依賴於產生的有關表之間存在的外鍵關係的元資訊。

現在任何資料庫增量都會自動更新客戶端程式碼。 想像一下例如:

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 元模型:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • 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

添加評論