Spring Data JDBC - 如何維護您的資料庫結構描述

工程 | Jens Schauder | 2023 年 8 月 29 日 | ...

這是關於在使用 Spring Data JDBC 時可能遇到的各種挑戰的系列文章中的第五篇。該系列包含:

  1. Spring Data JDBC - 如何使用自訂 ID 生成?

  2. Spring Data JDBC - 如何建立雙向關係?.

  3. Spring Data JDBC - 如何實作快取?

  4. Spring Data JDBC - 如何對聚合根執行部分更新?

  5. Spring Data JDBC - 如何為我的領域模型產生結構描述?(本文)

如果您是 Spring Data JDBC 的新手,您應該首先閱讀其介紹這篇文章,其中解釋了聚合在 Spring Data JDBC 中的相關性,以了解基本概念。

使用任何物件關係對應器 (ORM),您都需要建立兩個東西,並且它們必須相互匹配:

  1. 以 Java 類別形式呈現的領域模型。
  2. 由表格、欄位、索引和約束組成的資料庫結構描述。

3.2.0-M1 Spring Data Relational 版本開始,它將幫助您完成此操作。 本文說明如何使其運作。

建立初始結構描述

首先要做的是找到放置結構描述產生程式碼的位置。 我們建議為此使用測試。 您可以使用主應用程式的配置,並且它不會在生產環境中意外執行。

接下來要做的是取得 RelationalMappingContext。 這是 Spring Data Relational 的核心類別,它是 Spring Data JDBC 和 Spring Data R2DBC 的父項。 此類別包含有關聚合的所有對應元資訊,一旦它完全初始化。 但是此初始化會延遲發生,因此您必須自己註冊聚合根。

然後,您需要從中建立 LiquibaseChangeSetWriter 並使用它來寫出 Liquibase 變更集。

// context is a RelationalMappingContext that you autowire in your test.
context.setInitialEntitySet(Collections.singleton(Minion.class));
LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context);

writer.writeChangeSet(new FileSystemResource("cs-minimum.yaml"));

為使此工作正常進行,您需要在相依性中加入 Liquibase。

<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>

注意:如果您使用 Spring Boot,Liquibase 相依性將觸發使用 Liquibase 的結構描述初始化,這將失敗,因為它找不到任何變更集。 您可以透過將此行新增至您的 application.properties來輕鬆停用此功能。

spring.liquibase.enabled=false

如果您執行此測試,您應該在專案的根資料夾中找到一個名為 cs-minimum.yaml 的檔案。

databaseChangeLog:
- changeSet:
    id: '1692728224754'
    author: Spring Data Relational
    objectQuotingStrategy: LEGACY
    changes:
    - createTable:
        columns:
        - column:
            autoIncrement: true
            constraints:
              nullable: true
              primaryKey: true
            name: id
            type: BIGINT
        - column:
            constraints:
              nullable: true
            name: name
            type: VARCHAR(255 BYTE)
        tableName: minion

您應該檢閱、修改它,並將其放置在適當的位置,以便 Liquibase 取得。 如果您之前停用了它,現在請啟用 Liquibase 中的結構描述初始化,以便實際使用此變更集。

建立更新結構描述

對於應用程式的第二個版本,您可能需要對資料庫結構描述進行一些更新。 Spring Data JDBC 也可以幫助您完成這些工作。

為了建立這樣的增量結構描述更新,我們需要提供資料庫的目前狀態。 這可以使用 liquibase.database.Database 的實例來完成,您可以從 DataSource 建立它。

@Autowired
DataSource ds;

// ...

context.setInitialEntitySet(Collections.singleton(Minion.class));
LiquibaseChangeSetWriter writer = new LiquibaseChangeSetWriter(context);

try (Database db = new HsqlDatabase()) {

	db.setConnection(new JdbcConnection(ds.getConnection()));

	writer.writeChangeSet(new FileSystemResource("cs-diff.yaml"), db);

} catch (IOException | SQLException | LiquibaseException e) {
	throw new RuntimeException("Changeset generation failed", e);
}

上面的範例使用 HsqlDatabase。 您將使用與您的實際資料庫相符的實作。

預設情況下,變更集永遠不會從您的結構描述中刪除欄或表格。 僅僅因為它們未在領域模型中建模並不意味著您不需要它們,對嗎? 但是,如果您實際上想要刪除 Java 領域模型中不存在的部分或所有表格和欄位,請註冊 DropTableFilterDropColumnFilter),如以下範例所示,該範例刪除所有未對應的欄位,但名稱為 special 的欄位除外。

writer.setDropColumnFilter((table, column) -> !column.equalsIgnoreCase("special"));

自訂結構描述產生

Spring Data JDBC 沒有用於指定欄位的確切資料庫類型的註解。 但它提供了一個鉤子來使用您想要的類型。 您可以向 LiquibaseChangeSetWriter 提供 SqlTypeMapping

writer.setSqlTypeMapping(((SqlTypeMapping) property -> {
	if (property.getName().equalsIgnoreCase("name")) {
		return "VARCHAR(500)";
	}
	return null;
}).and(new DefaultSqlTypeMapping()));

您只需要實作該介面的單一方法:String getColumnType(RelationalPersistentProperty property)。 如果您只想修改某些情況的類型,您可以將其與 DefaultSqlTypeMapping 結合使用,該對應將用於您的實作傳回 null 的所有情況,如範例所示。

使用註解來控制結構描述類型

RelationalPersistentProperty 具有一些非常有用的方法,例如 findAnnotation 來存取屬性或其擁有實體上的註解(包括 meta 註解)。 您可以使用此功能使用您自己的註解和 meta 註解來控制用於您的領域模型的資料庫類型。

例如,您可以建立一個註解層,指定資料庫層級類型,以及另一組使用第一個註解的領域特定註解,如以下程式碼片段所示:

@Retention(RetentionPolicy.RUNTIME)
public @interface Varchar {

	/**
	 * the size of the varchar.
	 */
	int value();
}
@Varchar(20)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
}

然後,您可以使用此註解來註解您的領域模型中的屬性,並使用相符的 SqlTypeMapping

@Name
String name;
writer.setSqlTypeMapping(((SqlTypeMapping) property -> {

  if (!property.getType().equals(String.class)) {
    return null;
  }

  // findAnnotation will find meta annotations
  Varchar varchar = property.findAnnotation(Varchar.class);
  int value = varchar.value();

  if (varchar == null) {
    return null;
  }
  return "VARCHAR(" +
      varchar.value() +
      ")";

}).and(new DefaultSqlTypeMapping()));

限制

結構描述產生目前不支援參考。 這些目前將被靜默忽略。 當然,我們將在未來改進這一點。

為什麼這麼複雜?

如果您來自 JPA/Hibernate,您已經習慣於使用簡單的配置來直接在資料庫中產生結構描述,並且還習慣於將結構描述資訊作為對應註解的一部分。 很自然地會問我們為什麼選擇不同的方式。

對此問題有多個答案:

  1. 結構描述變更可能很危險。

您可以輕鬆地執行一些可以透過套用資料庫備份來復原的事情。我們認為讓開發人員在沒有真正看到,更不用說思考他們所套用的變更的情況下,就養成做這種事情的習慣,並不是一件好事。這就是為什麼我們建立變更,但將套用變更作為一個獨立的步驟。

  1. Schema 變更應該由版本控制系統控制,並且需要由專用工具來管理,因為它們不是等冪的。也就是說,您不能重新執行一個新增表格或欄位的 SQL 指令碼,以確保該欄位存在。

這就是為什麼我們選擇 Liquibase 來建立和管理變更。

  1. 資料庫中使用的確切資料類型與物件關係對應器(例如 Spring Data JDBC)無關。

因此,這類資訊不應成為 Spring Data JDBC 使用的對應註釋的一部分。相反,這類資訊應該以一種真正獨立於 Spring Data JDBC 的方式從您的模型中衍生出來。我們認為所展示的中繼註釋方法是一個很好的方法。

結論

透過目前的重要里程碑和即將發布的 GA 版本,Spring Data JDBC 提供了一種靈活而強大的方式,可以從您的領域模型產生資料庫遷移。我們期待聽到您對此的意見和經驗。

完整的範例程式碼可在 Spring Data Example 儲存庫中找到

取得 Spring 電子報

與 Spring 電子報保持聯繫

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中,為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部