Spring Data JDBC - 如何維護資料庫 Schema

工程技術 | 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 - 如何為我的領域模型生成 Schema?(本文)

如果您剛接觸 Spring Data JDBC,建議先閱讀其介紹這篇解釋聚合在 Spring Data JDBC 中的相關性的文章,以理解基本概念。

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

  1. 以 Java 類形式表示的領域模型。
  2. 由表、列、索引和約束組成的資料庫 schema。

3.2.0-M1 版本 Spring Data Relational 開始將幫助您完成此操作。本文將向您展示如何實現它。

建立初始 Schema

首先要做的是找到一個地方來放置生成 schema 的程式碼。我們建議為此使用一個測試。您可以從中利用主應用程式的配置,並且它不會在生產環境中意外執行。

接下來要做的是獲取一個RelationalMappingContext。這個類是 Spring Data Relational 的核心,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 的 schema 初始化,這將失敗,因為它找不到任何變更集。您可以透過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 的 schema 初始化,以便實際使用此變更集。

建立更新 Schema

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

為了建立這種增量 schema 更新,我們需要提供資料庫的當前狀態。這可以透過一個 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。您應該使用與您實際資料庫匹配的實現。

預設情況下,變更集永遠不會從您的 schema 中刪除列或表。僅僅因為它們沒有在領域模型中建模,並不意味著您不需要它們,對嗎?但是,如果您確實想刪除 Java 領域模型中不存在的部分或全部表和列,可以註冊一個 DropTableFilterDropColumnFilter,就像下面的例子一樣,它會刪除所有未對映的列,但名為 special 的列除外。

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

定製 Schema 生成

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 時,後者將用於所有其他情況,如示例所示。

使用註解控制 Schema 型別

RelationalPersistentProperty 提供了一些非常有用的方法,例如 findAnnotation,用於訪問屬性或其所屬實體上的註解(包括元註解)。您可以利用此功能使用自己的註解和元註解來控制您的領域模型使用的資料庫型別。

例如,您可以建立一層指定資料庫級別型別的註解,然後使用第一層註解建立另一層領域特定的註解集合,如下面的程式碼片段所示:

@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()));

限制

目前 Schema 生成不支援引用。這些引用目前將被靜默忽略。當然,我們將來會改進這一點。

為什麼這麼複雜?

如果您來自 JPA/Hibernate,您可能習慣於透過簡單的配置直接在資料庫中生成 schema,並且將 schema 資訊作為對映註解的一部分。很自然會問,我們為什麼選擇了不同的方式。

有幾個原因可以回答這個問題:

  1. Schema 更改具有潛在危險性。

您很容易做一些只有透過應用資料庫備份才能恢復的操作。我們認為讓開發者在不真正看到(更不用說思考)他們應用的更改的情況下就習慣於這樣做不是一件好事。這就是為什麼我們建立更改,但將應用更改留作一個單獨的步驟。

  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 社群所有即將舉行的活動。

檢視全部