数据库变更管理:Flyway & Liquibase
Table of Contents
和代码的版本管理一样,对数据库的变更做版本管理
对于大项目或者多个团队的项目开发,由于数据库表比较多,特别是生产环境中已经部署,数据库的变更管理就比较重要。有一点和代码变更不同的是,数据库的变更不只会被记录下来,而且还会被顺次执行,例如如果插入了一个错误的字段,下次变更得把它删除。
目前能实现数据库变更管理的工具就是 Flyway 和 Liquibase,这里做简单的介绍。
Flyway #
数据库的变更可以用 sql 或者 java 来记录,Flyway 通过下面的步骤实现数据库变更:
- Flyway 先在数据库中检查自己的元数据表(默认为SCHEMA_VERSION)是否存在,如果没有,则创建一个;
- 检查 classpath 中所有的变更;
- 对比变更和自己的表,如果变更的版本低于或等于当前版本,不做任何变动;
- 否则,变更会按从低到高排序,并依次执行;
- 执行完,在SCHEMA_VERSION做相应的记录
Flyway 可以通过 Maven/Gradle,程序(Spring),或者命令行来执行变更操作,下面以 Maven 方式为例。
- Flyway Maven Plugin
首先在 Maven 的 pom.xml中引入 Flyway 的 plugin:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>4.4.0</version>
</dependency>
...
<plugins>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>4.4.0</version>
</plugin>
</plugins>
- Configuration
然后配置 Flyway:
<project>
...
<properties>
<flyway.user>databaseUser</flyway.user>
<flyway.password>databasePassword</flyway.password>
<flyway.schemas>schemaName</flyway.schemas>
...
</properties>
...
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>4.0.3</version>
<configuration>
<user>${databaseUser}</user>
<password>${databasePassword}</password>
<schemas>
<schema>${schemaName}</schema>
</schemas>
...
</configuration>
</plugin>
...
</project>
参数当然也可以通过 flyway.properties 或者 system properties 传入,其优先顺序:
1. system properties
2. 配置文件,如 flyway.properties
3. maven properties
4. plugin configuration
以 H2 数据库为例,Maven 的 pom.xml中添加:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
在 src/main/resources/ 里添加以下配置文件 flyway.properties:
flyway.user=databaseUser
flyway.password=databasePassword
flyway.schemas=app-db
flyway.url=jdbc:h2:mem:DATABASE
flyway.locations=src/main/resources/db/migration
如果是 Gradle,不需要 Maven plugin:
implementation('com.h2database:h2')
implementation('org.postgresql:postgresql')
implementation('org.springframework.boot:spring-boot-starter-jdbc')
implementation "org.flywaydb:flyway-core"
- 变更脚本
变更的脚本目录指定在 filesystem:src/main/resources/db/migration,变更脚本文件的命名遵循下面的格式:
<Prefix><Version>__<Description>.sql
- Prefix:默认 V 打头,可以通过 flyway.sqlMigrationPrefix 参数修改
- Version:版本号
- Description:文件描述,用双下划线分割
例如创建一个变更脚本 V1.0__create_company.sql:
create sequence hibernate_sequence start with 1050;
create table company
(
id uuid not null,
name varchar(255) not null,
web_page_address varchar(255),
billing_contact_email_address varchar(255),
primary_contact_email_address varchar(255),
constraint pk_company
primary key (id)
);
运行:
$ mvn clean flyway:migrate -Dflyway.configFile=src/main/resources/flyway.properties
表 company 就会被创建,下次变更可能为插入数据,例如 V2.0__add_company.sql:
INSERT INTO PUBLIC.COMPANY (ID, NAME, WEB_PAGE_ADDRESS, BILLING_CONTACT_EMAIL_ADDRESS, PRIMARY_CONTACT_EMAIL_ADDRESS)
VALUES ('1a689e52-f35b-4bda-934c-ea4f076bdc2c', 'Blue Fish Software Inc', 'bluefish.io', 'bills@bluefish.io',
'hi@bluefish.io');
再次运行:
$ mvn clean flyway:migrate -Dflyway.configFile=src/main/resources/db/migration
数据就会被写入表中。
Liquibase #
工作方式与 Flyway 非常类似,但是 Liquibase 稍微复杂点,下面也以 Maven 方式来实现 Liquibase 的数据库变更。
- 和 Flyway 类似,引入 Liquibase plugin:
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.4.1</version>
</dependency>
...
<plugins>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.4.1</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
</configuration>
<dependencies>
<dependency>
<groupId>org.liquibase.ext</groupId>
<artifactId>liquibase-hibernate4</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.7.RELEASE</version>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.3.RELEASE</version>
</dependency>
</dependencies>
</plugin>
</plugins>
如果是 Gradle,同样简洁很多:
dependencies{
liquibaseRuntime "org.liquibase:liquibase-core"
liquibaseRuntime "mysql:mysql-connector-java"
liquibaseRuntime "com.h2database:h2"
liquibaseRuntime "org.liquibase:liquibase-groovy-dsl:2.1."
liquibaseRuntime "org.liquibase.ext:liquibase-hibernate5:3.6"
liquibaseRuntime sourceSets.main.output
}
- Configuration
src/main/resources/liquibase.properties:
changeLogFile=src/main/resources/db/migration/changelog-master.xml
url=jdbc:mysql://localhost:3306/oauth_reddit
username=tutorialuser
password=tutorialmy5ql
driver=com.mysql.jdbc.Driver
referenceUrl=jdbc:h2:mem:oauth_reddit
diffChangeLogFile=src/main/resources/changelog-diff.xml
referenceDriver=org.h2.Driver
referenceUsername=sa
referencePassword=
- 变更脚本
changeLogFile 指明了变更脚本:src/main/resources/db/migration/changelog-master.xml:
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
<property name="now" value="now()" dbms="h2"/>
<property name="now" value="current_timestamp()" dbms="mysql"/>
<property name="floatType" value="float4" dbms="h2"/>
<property name="floatType" value="float" dbms="mysql"/>
<property name="clobType" value="longvarchar" dbms="h2"/>
<property name="clobType" value="longtext" dbms="mysql"/>
<property name="uuidType" value="uuid" dbms="h2"/>
<property name="uuidType" value="uuid" dbms="mysql"/>
<changeSet id="00000000000000" author="fastzhong">
<createSequence sequenceName="sequence_generator" startValue="1050" incrementBy="1"/>
</changeSet>
<include file="2021061401_add_entity_Company.xml" relativeToChangelogFile="true"/>
<include file="2021061402_add_entity_Team.xml" relativeToChangelogFile="true"/>
<include file="2021061403_add_entity_Project.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
changelog-master.xml 是个主文件,具体的变更记录在 3 个文件里,例如 V2021061401_add_entity_Company.xml:
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
<changeSet id="2021061401-1" author="fastzhong">
<createTable tableName="company">
<column name="id" type="bigint" autoIncrement="${autoIncrement}">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="web_page_address" type="varchar(255)">
<constraints nullable="true"/>
</column>
<column name="billing_contact_email_address" type="varchar(255)">
<constraints nullable="true"/>
</column>
<column name="primary_contact_email_address" type="varchar(255)">
<constraints nullable="true"/>
</column>
</createTable>
<rollback>
<dropTable tableName="company"/>
</rollback>
</changeSet>
<changeSet id="2021061401-1-data" author="fastzhong" context="faker">
<loadData
file="data/company.csv"
separator=";"
tableName="company">
<column name="id" type="numeric"/>
<column name="name" type="string"/>
<column name="web_page_address" type="string"/>
<column name="billing_contact_email_address" type="string"/>
<column name="primary_contact_email_address" type="string"/>
</loadData>
</changeSet>
</databaseChangeLog>
每个变更记录在 changeSet 里(id 为主键)。Liqudbase 的 XML 如 createTable,loadData,等,好处是可对任何数据库有效,当然也可以直接上 sql:
<changeSet id="2021061401-1-data" author="fastzhong" context="faker">
<sql dbms="mysql">
CREATE TABLE company (
id BIGINT NOT NULL AUTO_INCREMENT,
name CHAR(255) NOT NULL,
web_page_address CHAR(255) NOT NULL,
billing_contact_email_address CHAR(255) NOT NULL,
primary_contact_email_address CHAR(255) NOT NULL,
PRIMARY KEY (id)
);
</sql>
</changeSet>
注意到 changeSet 可以包括 rollback,当出错执行的回滚语句或者可以指定 rollback 时执行。
执行变更操作:
$ mvn liquibase:update
在项目开始时,也可以从数据库已有的表结构反向生成 changeLogFile:
$ mvn liquibase:generateChangeLog
还可以比较两个数据库有什么不同,记录在 diffChangeLogFile 里:
$ mvn liquibase:diff
这个还蛮有用的,例如可以通过 Hibernate Entities 在本地数据库自动生成表结构,然后和生产环境中的数据库比较 - 但是不能做到数据库和 changeLogFile 直接比较。
如果觉得 XML 繁琐的话,Liquibase 也支持 Json,YAML 格式。
对比 #
两者的基本功能其实都差不多:
- 都是 Java 开发的开源数据库变更管理工具
- 支持大部分的数据库
- 和 Maven/Gradle 无缝集成
- 和 Spring 无缝集成
- 非常类似的变更实现方式
- 复杂变更如果 SQL 不能满足的话,都可以用 Java 代码实现
较大区别是 Flyway 的变更以纯 SQL 为脚本,简单直接;Liquibase 比较厚重,当然花样也比较多,包括:
- 可指定不同的 profile
- 具有通用的变更操作支持不同的数据库,如 createTable
- Liquibase 开源版本支持 diff 模式,而此特性 Flyway 必须用商业版
- Liquibase 开源版本支持回滚 rollback,而此特性 Flyway 必须用商业版
- 两者指定变更执行顺序的方法不同,Flyway 通过固定的文件名格式来确定顺序,而 Liquibase 就是通过给定文件的顺序来执行,所以开发人员还要遵守好命名规则,例如按照日期/时间顺序命名
两者都提供开源和商业版本,具体参见:
小结:两者差别不大,因功能多,个人“稍微”倾向 Liquibase。
bytebase #
可关注新的开源项目 bytebase,作为一个更为全面的数据库 schema 管理工具:
providing UX and workflow optimized for the collaboration among DBAs and Developers like how Figma delivers such an experience to Designers, Product Managers and Developers
deliver an end-to-end integration between schema management and VCS like how Terraform delivers such an experience for managing cloud infrastructure)
gather info from the CI context and catch signals from the database instance to provide a holistic view of the schema state across all development environments, spanning all history timelines