Java 使用 Liquibase 安全地演化数据库 schema
1. 概述
本文将向你展示如何使用 Liquibase 来演化 Java web 应用的数据库 schema。首先,我们将研究一个通用的 Java 应用,然后重点介绍一些与 Spring 和 Hibernate 很好集成的有趣选项。
当使用 Liquibase 时,我们可以使用一系列变更日志文件来描述数据库 schema 的演变。尽管这些文件可以用多种格式编写,如 SQL、XML、JSON 和YAML,但在这里我们只关注 XML 格式来描述我们数据库的修改。因此,随着时间的推移,我们可以为数据库更改许多更新日志文件。此外,所有这些文件在名为 master.xml
的单个根更新日志文件中都有一个引用,Liquibase 将使用该文件。
我们可以使用 include
和 includeAll
属性将其他更新日志文件嵌套在此根更新日志下。
让我们从在 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
的新表,其字段有 id
、make
、brand
和 price
:
<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
标记。值得注意的是,id
和 author
属性是如何标识变更集(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