编程

Java 使用 Liquibase 安全地演化数据库 schema

34 2024-10-17 09:44:00

1. 概述

本文将向你展示如何使用 Liquibase 来演化 Java web 应用的数据库 schema。首先,我们将研究一个通用的 Java 应用,然后重点介绍一些与 Spring 和 Hibernate 很好集成的有趣选项。

 当使用 Liquibase 时,我们可以使用一系列变更日志文件来描述数据库 schema 的演变。尽管这些文件可以用多种格式编写,如 SQL、XML、JSON 和YAML,但在这里我们只关注 XML 格式来描述我们数据库的修改。因此,随着时间的推移,我们可以为数据库更改许多更新日志文件。此外,所有这些文件在名为 master.xml 的单个根更新日志文件中都有一个引用,Liquibase 将使用该文件。

我们可以使用 includeincludeAll 属性将其他更新日志文件嵌套在此根更新日志下。

让我们从在 pom.xml 中引入最新的 liquibase 核心依赖项开始:

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

2. 数据库更新日志

首先,让我们看一个简单的 XML 更新日志文件来了解它的结构。本例中,我们创建了一个名为 20170503041524_added_entity_Car.xml 的文件,该文件添加了一个名为 car 的新表,其字段有 idmakebrandprice

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
   ...
   <!--Added the entity Car-->
   <changeSet id="20170503041524-1" author="user">
      <createTable tableName="car">
         <column name="id" type="bigint" autoIncrement="${autoIncrement}">
            <constraints primaryKey="true" nullable="false" />
         </column>
         <column name="make" type="varchar(255)">
            <constraints nullable="true" />
         </column>
         <column name="brand" type="varchar(255)">
            <constraints nullable="true" />
         </column>
         <column name="price" type="double">
            <constraints nullable="true" />
         </column>
      </createTable>
   </changeSet>
</databaseChangeLog>

让我们暂时忽略所有命名空间定义,专注于 changeSet 标记。值得注意的是,idauthor 属性是如何标识变更集(change set)的。这确保了任何给定的更改只应用一次。此外,我们也可以直接看到这一变化的作者。

现在,要使用此更新日志,我们需要在 master.xml 中定义该文件:

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
   
    <include file="classpath:config/liquibase/changelog/00000000000000_initial_schema.xml" relativeToChangelogFile="false" />
   
    <include file="classpath:config/liquibase/changelog/20170503041524_added_entity_Car.xml" relativeToChangelogFile="false" />

</databaseChangeLog>

现在,Liquibase 使用 master.xml 文件来跟踪已经应用于数据库的变更集。为了减少冲突,我们在包含变更集的文件前添加了数据时间戳。日期时间戳允许 Liquibase 确定哪些变更集已经应用,哪些仍需要应用。这只是我们使用的惯例,可以根据具体的需要进行更改。

一旦应用了变更集,我们通常无法进一步编辑同一个的变更集。因此,对于任何给定的 schema 修改,我们必须创建一个新的变更集文件,这将为我们的数据库版本控制提供一致性。

现在让我们看看如何将其连接到 Spring 应用中,并确保它在应用启动时运行。

3. 使用 Spring Bean 运行 Liquibase

在应用启动时运行更新的第一个选项是通过 Spring bean。

当然,还有许多其他方法,但如果我们使用的是 Spring 应用,这是一种很好、简单的方法:

@Bean
public SpringLiquibase liquibase() {
    SpringLiquibase liquibase = new SpringLiquibase();
    liquibase.setChangeLog("classpath:config/liquibase/master.xml");
    liquibase.setDataSource(dataSource());
    return liquibase;
}

或者,更完整的配置文件需要额外的设置:

@Bean
public SpringLiquibase liquibase(@Qualifier("taskExecutor") TaskExecutor taskExecutor,
        DataSource dataSource, LiquibaseProperties liquibaseProperties) {

    // Use liquibase.integration.spring.SpringLiquibase if you don't want Liquibase to start asynchronously
    SpringLiquibase liquibase = new AsyncSpringLiquibase(taskExecutor, env);
    liquibase.setDataSource(dataSource);
    liquibase.setChangeLog("classpath:config/liquibase/master.xml");
    liquibase.setContexts(liquibaseProperties.getContexts());
    liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
    liquibase.setDropFirst(liquibaseProperties.isDropFirst());
    if (env.acceptsProfiles(JHipsterConstants.SPRING_PROFILE_NO_LIQUIBASE)) {
        liquibase.setShouldRun(false);
    } else {
        liquibase.setShouldRun(liquibaseProperties.isEnabled());
        log.debug("Configuring Liquibase");
    }
    return liquibase;
}

同样,我们在项目的类路径(classpath)上创建了 master.xml 文件。因此,如果位置有所不同,我们可能需要提供正确的路径。

4. 在 Spring Boot 中使用 Liquibase

使用 Spring Boot 时,我们不需要为 Liquibase 定义 bean,引入 Liquibase 核心依赖项就可以了。但我们需要将 master.xml 具体放在 src/main/resources/config/liquebase/master.xml 中,这样 Liquibase 就可以读取它并在启动时自动运行。

或者,我们也可以通过设置 liquiase.change-log 属性来更改默认的更新日志的文件位置:

liquibase.change-log=classpath:liquibase-changeLog.xml

5. 在 Spring Boot 中禁用 Liquibase

有时,我们可能不想在启动时运行 Liquibase 迁移。因此,要禁用 Liquibase,我们可以使用 spring.liquibase.enabled 属性:

spring.liquibase.enabled=false

这样,Liquibase 配置就不会应用,数据库的 schema 保持不变。

我们应该记住,Spring Boot 2.x 之前的应用使用的是 liquibase.enabled 属性:

liquibase.enabled=false

6. 使用 Maven 插件生成 changelog

手动写入 changelog 文件可能既费时又容易出错。因此,我们可以使用 Liquibase Maven 插件从现有数据库生成一个,从而节省大量工作。

6.1. 插件配置

pom.xml 中使用最新版的 liquibase-maven-plugin 依赖:

<plugin>
   <groupId>org.liquibase</groupId>
   <artifactId>liquibase-maven-plugin</artifactId>
   <version>4.27.0</version>
    ...
   <configuration>
      ...
      <driver>org.h2.Driver</driver>
      <url>jdbc:h2:file:./target/h2db/db/carapp</url>
      <username>carapp</username>
      <password />
      <outputChangeLogFile>src/main/resources/liquibase-outputChangeLog.xml</outputChangeLogFile>
   </configuration>
</plugin>

上面,我们只是描述了数据库驱动及其相关配置和 changelog 文件的输出。

6.2. 从现有的数据库中生成 Changelog

Let’s use a mvn command to generate a changelog from an existing database:

mvn liquibase:generateChangeLog

现在,我们可以使用生成的 changelog 文件来创建初始数据库 schema 或填充数据。

这将在配置的位置创建一个 changeLog 文件:

<databaseChangeLog ...>
   <changeSet id="00000000000001" author="jhipster">
      <createTable tableName="jhi_persistent_audit_event">
         <column name="event_id" type="bigint" autoIncrement="${autoIncrement}">
            <constraints primaryKey="true" nullable="false" />
         </column>
         <column name="principal" type="varchar(50)">
            <constraints nullable="false" />
         </column>
         <column name="event_date" type="timestamp" />
         <column name="event_type" type="varchar(255)" />
      </createTable>
      <createTable tableName="jhi_persistent_audit_evt_data">
         <column name="event_id" type="bigint">
            <constraints nullable="false" />
         </column>
         <column name="name" type="varchar(150)">
            <constraints nullable="false" />
         </column>
         <column name="value" type="varchar(255)" />
      </createTable>
      <addPrimaryKey columnNames="event_id, name" tableName="jhi_persistent_audit_evt_data" />
      <createIndex indexName="idx_persistent_audit_event" tableName="jhi_persistent_audit_event" unique="false">
         <column name="principal" type="varchar(50)" />
         <column name="event_date" type="timestamp" />
      </createIndex>
      <createIndex indexName="idx_persistent_audit_evt_data" tableName="jhi_persistent_audit_evt_data" unique="false">
         <column name="event_id" type="bigint" />
      </createIndex>
      <addForeignKeyConstraint baseColumnNames="event_id" baseTableName="jhi_persistent_audit_evt_data" constraintName="fk_evt_pers_audit_evt_data" referencedColumnNames="event_id" referencedTableName="jhi_persistent_audit_event" />
   </changeSet>

   <changeSet id="20170503041524-1" author="jhipster">
      <createTable tableName="car">
         <column name="id" type="bigint" autoIncrement="${autoIncrement}">
            <constraints primaryKey="true" nullable="false" />
         </column>
         <column name="make" type="varchar(255)">
            <constraints nullable="true" />
         </column>
         <column name="brand" type="varchar(255)">
            <constraints nullable="true" />
         </column>
         <column name="price" type="double">
            <constraints nullable="true" />
         </column>
         <!-- jhipster-needle-liquibase-add-column - JHipster will add columns here, do not remove-->
      </createTable>
   </changeSet>
</databaseChangeLog>

6.3. 根据两个数据库之间的差异生成 ChangeLog

接下来,使用 diff 命令,我们可以生成包含两个现有数据库差别的 changeLog 文件:

mvn liquibase:diff

然后我们可以用这个文件去对齐两个数据库。为了使之正确工作,我们需要使用额外属性来配置 liquibase 插件:

changeLogFile=src/main/resources/config/liquibase/master.xml
url=jdbc:h2:file:./target/h2db/db/carapp 
username=carapp 
password= 
driver=com.mysql.jdbc.Driver 
referenceUrl=jdbc:h2:file:./target/h2db/db/carapp2 
referenceDriver=org.h2.Driver 
referenceUsername=tutorialuser2 
referencePassword=tutorialmy5ql2

再次,这将生成 changeLog 文件:

<databaseChangeLog ...>
    <changeSet author="John" id="1439227853089-1">
        <dropColumn columnName="brand" tableName="car"/>
    </changeSet>
</databaseChangeLog>

这是一种超级强大的方式来更新我们的数据库。例如,我们可以允许 Hibernate 自动生成一个新的 schema 用于开发,然后将其用作旧 schema 的参考点。

7. 使用 Liquibase Hibernate 插件

如果应用使用了 Hibernate,我们可以使用 liquibase-hibernate5 来生成 changeLog 文件。

7.1. 插件配置

首先,在 pom.xml 中添加依赖的最新版:

<plugin>
   <groupId>org.liquibase</groupId>
   <artifactId>liquibase-maven-plugin</artifactId>
   <version>4.27.0</version>
   <dependencies>
      ...
      <dependency>
         <groupId>org.liquibase.ext</groupId>
         <artifactId>liquibase-hibernate5</artifactId>
         <version>3.6</version>
      </dependency>
     ...
   </dependencies>
   ...
</plugin>

7.2. 根据数据库与持久化实体之间的差异生成 ChangeLog

现在,我们可以使用此插件根据现有数据库(例如生产环境)和新的持久性实体之间的差异生成 changelog 文件。

因此,为了简单起见,一旦修改了实体,我们就可以对旧的 DB schema 进行更改,从而获得一种干净、强大的方式来在生产中演化我们的 schema

以下是 pom.xml 中插件配置中的 liquibase 属性:

<configuration>
   <changeLogFile>src/main/resources/config/liquibase/master.xml</changeLogFile>
   <diffChangeLogFile>src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml</diffChangeLogFile>
   <driver>org.h2.Driver</driver>
   <url>jdbc:h2:file:./target/h2db/db/carapp</url>
   <defaultSchemaName />
   <username>carapp</username>
   <password />
   <referenceUrl>hibernate:spring:com.car.app.domain?dialect=org.hibernate.dialect.H2Dialect
    &hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
    &hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
   </referenceUrl>
   <verbose>true</verbose>
   <logging>debug</logging>
</configuration>

值得注意的是,referenceUrl 使用包扫描,因此需要 dialect 参数。

8. 在 IntelliJ IDEA 中使用 JPA Buddy 插件生成 changelog

如果我们使用的是非 Hibernate ORM(例如 EclipseLink 或 OpenJPA),或者我们不想添加额外的依赖项,如 liquibase-hibernate 插件,我们可以使用 JPA Buddy。这个 IntelliJ IDEA 插件将有用的 Liquibase 功能集成到 IDE 中

为了生成差异 changelog,我们安装插件,然后从JPA 结构面板调用操作。我们选择要比较的源(数据库、JPA 实体或 Liquibase 快照)和目标(数据库或 Liquibase 快照)。

JPA Buddy 将生成 changeLog,如下图所示:

JPA Buddy 相对于 liquibase-hibernate 插件的另一个优点是能够重写 Java 和数据库类型之间的默认映射。此外,它还可以与 Hibernate 自定义类型和 JPA 转换器正确配合使用。

9. 小结

本文中,我们描述了使用 Liquibase 的几种方法,并找到了一种安全成熟的方法来演化和重构 Java 应用 的 DB schema

源码:https://github.com/eugenp/tutorials/tree/master/jhipster-8-modules/jhipster-8-microservice/car-app