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 社区所有即将举行的活动。

查看全部