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

查看全部