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 用于访问属性或其拥有实体上的注解(包括元注解)。您可以使用此功能来使用您自己的注解和元注解来控制用于您的领域模型的数据库类型。

例如,您可以创建一层注解来指定数据库级别的类型,并使用第一层注解创建另一组特定于领域的注解,如以下代码片段所示:

@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. 模式更改应由版本控制控制,并且需要由专用工具管理,因为它们不是幂等的。也就是说,您不能重新应用添加表或列的 SQL 脚本来确保该列存在。

这就是我们选择 Liquibase 来创建和管理更改的原因。

  1. 数据库中使用的确切数据类型与对象关系映射器(例如 Spring Data JDBC)无关。

因此,这类信息不应作为 Spring Data JDBC 使用的映射注解的一部分。相反,这类信息应以真正独立于 Spring Data JDBC 的方式从您的模型中派生出来。我们认为所示的元注解方法是实现这一目标的好方法。

结论

凭借当前的里程碑和即将发布的 GA 版本,Spring Data JDBC 提供了一种灵活而强大的方法,可以从您的领域模型生成数据库迁移。我们期待听到您对此的意见和经验。

完整的示例代码可在 Spring Data 示例存储库中找到

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有