前言
六年前Flyway已经是我TDD开发、持续集成工具栈中的重要一环了,作为早期用户,我早就应该为它做个”广告“,可惜对创业者来说时间太宝贵了,现在趁着疫情才有机会在家里总结点东西。虽然现在Flyway已经是Spring-Boot集成工具的一环,但是我发现还是少有人了解它的威力。
关系数据库之殇
你在使用关系数据库的过程中,是否曾经遇到以下情况,甚至因此一度想要放弃或已经放弃关系数据库?
场景一:开发环境,多人共用一套数据库
开发正调试着,忽然代码报错“XX字段不存在”:谁TMD又把表结构给改了…
场景二:开发环境,每个人各自搭建自己的数据库
开发完一个功能,提交代码、更新,重启准备调试下,代码报错“XX表不存在”
吼一嗓子:谁又改表结构了?什么?每个人都要把xxx.sql执行一遍?
...
新员工:我要搭一套开发数据库,到底应该执行哪些SQL脚本?
场景三:开发转测试
测试:你看这个功能是不是有个Bug? 开发1:哦,你要执行一下这个SQL脚本。 测试:嗯,现在没问题了,但是怎么保证这个脚本没有Bug,我能再重现、测试一遍吗? 开发:额~,你重新搭一遍数据库吧...
场景四:搭建一套演示环境
执行SQL脚本1、SQL脚本2、SQL脚本3…启动服务失败!
什么?这个脚本N是测试版本的,war包是已经上线的版本?
删库再来一遍...
场景五:放弃关系数据库的坑
受不了关系数据库了,我们切MongoDB吧…嗯,控制台果然清静了 ... 几个版本后: 生产环境某些数据查不到了、还有类型不匹配,神马? A字段改过名,B字段换了个类型?
如果上面的问题,你一个都没遇到过,要么是你所做的项目太简单,要么你们的开发流程非常规范,或者变更控制得太好了,本文你大可跳过了。
如果你正被上面的问题困扰,你可能会因此想要入另一个坑:NoSQL,那么恭喜你将遇到更多的坑,比如关联查询问题、数据版本问题...
注:这里并不否定NoSQL的价值,各种NoSQL是关系数据库的良好补充。 但是如果想将NoSQL做为关系数据库的替代,那么你将会陷入比关系数据库还多的线上问题之中。
如果你看到这里,说明你不想逃避问题,那么让我们一起来认识这个关系数据库升级管理的利器——Flyway
Flyway原理介绍
Flyway是什么?一句话概括,Flyway就是一个数据库版本管理组件。它的原理非常简单:
- 项目启动时拉起Flyway,先检查数据库里面有没有Flyway元数据表,没有则创建;
- 检查Flyway元数据表中的记录,哪些脚本已经执行过,当前版本是什么;
- 查找代码中的(名称满足规则的)数据库升级脚本,找出版本号大于(Flyway元数据)当前版本的脚本,逐个执行并记录执行结果到Flyway元数据表。
你没看错,以上三点就是Flyway最核心的功能,我深信熟练掌握我另一篇博客《TDD两小时实现自定义表达式模板解析器》的同学不出一天,就能自己实现这三点功能,对非JVM开发者我推荐在理解以上思想的基础上自己开发一套。
大道至简,最简单的设计往往是最有效的。通过以上功能,我们可以很容易做到:
- 代码与数据库建表&升级脚本放在一起同步管理,通过代码(SQL)就可以了解到表结构;
- 无须人工执行任何脚本,运行代码或服务即可完成(数据库表结构的)环境搭建;
- 从任一版本的环境(表结构),都可以通过运行指定(新)版本的代码或服务来自动升级到指定新版本;
- (配合内存数据库/Docker/清库脚本)数据库搭建&升级脚本很容易与代码一起反复测试。
总之,用上Flyway之后,关系数据库的”关系“,不再是限制你开发效率的瓶颈,反而成为开发&测试的必要约定,提升版本质量的重要保障。
快速上手
不管工具多强大,如何用起来,是我们首要关心的,让我们以各种Java项目环境,来看一下如何在代码中将Flyway用起来,再由各位自己去细品Flyway对关系数据库版本管理带来的巨大改变。
注:所有示例都基于Maven,用Gradle的自己翻译下依赖,二者都不用的嘛...先研究下POM的依赖关系,再自己去下载jar包吧
1. 原生Java项目(不用Spring、Spring-Boot)
pom.xml文件中增加flyway的依赖:
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<!--通常情况下推荐最新发布版,此处是从旧代码中复制-->
<version>3.2.1</version>
</dependency>
java代码拉起Flyway:
DataSource dataSource = ...
...
//在数据连接创建之后,其它代码运行之前,先调用Flyway升级
Flyway flyway = new Flyway();
flyway.setDataSource(dataSource);
flyway.migrate();
...
编写建表脚本和数据初始化脚本
src
|-main
|-java
|-resources
|-db
|-migration
|-V0.0.1__init-schema.sql
|-V0.0.2__init-data.sql
注:脚本中的内容,就是正常的建表脚本,或者对上一版本的表结构变更、数据升级。
2. Spring项目
pom.xml文件中增加flyway的依赖(与原生java项目一样):
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<!--通常情况下推荐最新发布版,此处是从旧代码中复制-->
<version>3.2.1</version>
</dependency>
Spring配置文件拉起Flyway:
<!-- 创建Flyway的bean,并调用其migrate方法 -->
<bean id="flyway" class="org.flywaydb.core.Flyway" init-method="migrate">
<!-- 脚本文件校验和验证默认开启,可以防止脚本被修改。请视情况关闭校验 -->
<property name="validateOnMigrate" value="false" />
<property name="dataSource" ref="dataSource" />
</bean>
编写建表脚本和数据初始化脚本(与原生java项目一致)
src
|-main
|-java
|-resources
|-db
|-migration
|-V0.0.1__init-schema.sql
|-V0.0.2__init-data.sql
注:脚本中的内容,就是正常的建表脚本,或者对上一版本的表结构变更、数据升级。
3. Spring-Boot项目
Flyway已经被Spring-Boot整合,成为Spring标准的数据库升级工具,在Spring-Boot中使用Flyway更简单,只需添加依赖、编写数据库脚本即可,省去了拉起这一步。
pom.xml添加依赖(Spring-Boot已经整合,无须版本号)
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
如果你使用IDEA,其还为你提供了创建Flyway升级脚本的功能,直接以当时日期时间为你生成SQL升级脚本:
生成的脚本名称如下:
src
|-main
|-java
|-resources
|-db
|-migration
|-V20190315174656__init-schema.sql
|-V20190315201742__init-data.sql
|-V20191225205157__update-userid-to-bitint.sql
4. 升级脚本示例
为Flyway编写的SQL脚本并没有什么特殊的要求,与正常SQL并无二致,只不过每个脚本编写时考虑的永远是对前一个版本表结构的升级,这也是传统方式下严谨的升级脚本应该满足的要求。
建表脚本示例:
BEGIN;
-- 新数据库建表
CREATE SCHEMA IF NOT EXISTS staff;
CREATE TABLE IF NOT EXISTS staff.staffs
(
id BIGINT AUTO_INCREMENT NOT NULL,
staffId VARCHAR(10) NOT NULL,
createTime TIMESTAMP NOT NULL,
lastUpdateTime TIMESTAMP NOT NULL,
name VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
);
CREATE SCHEMA IF NOT EXISTS duty;
CREATE TABLE IF NOT EXISTS duty.onDutyDef
(
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50) NOT NULL,
startTime TIME NOT NULL,
endTime TIME NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS duty.breakDef
(
dutyId BIGINT NOT NULL,
name VARCHAR(10) NOT NULL,
startTime TIME NOT NULL,
endTime TIME NOT NULL,
PRIMARY KEY (dutyId, name)
);
-- 插入默认配置数据
INSERT INTO duty.onDutyDef
(name, startTime, endTime)
VALUES ('普通班', '09:00:00', '18:30:00');
INSERT INTO duty.breakDef
(dutyId, name, startTime, endTime)
VALUES (1, '午餐', '12:30:00', '14:00:00'),
(1, '晚餐', '18:30:00', '19:30:00');
COMMIT;
升级脚本示例
BEGIN;
-- 已有表字段变更
ALTER TABLE duty.signrecords ADD COLUMN clientId VARCHAR(40);
ALTER TABLE staff.staffs ADD COLUMN supervisor VARCHAR(100);
ALTER TABLE staff.staffs ADD COLUMN password VARCHAR(64);
-- 数据升级
UPDATE staff.staffs SET supervisor='00001,00002';
UPDATE staff.staffs SET password='8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92';
-- 新增表
CREATE SCHEMA IF NOT EXISTS users;
CREATE TABLE IF NOT EXISTS users.userroles (
staffId VARCHAR(15) NOT NULL,
rolename VARCHAR(15) NOT NULL,
PRIMARY KEY (staffId, rolename)
);
-- 新增表补充默认数据
INSERT INTO users.userroles(staffId,rolename)
VALUES
('00001','hr'),
('00001','supervisor'),
('00002','hr'),
('00002','supervisor'),
('00003','supervisor');
COMMIT;
推荐的搭档
根据这么多年我的使用经验,有下面这些用法,可以最大地发挥Flyway的作用
1. H2+Flyway
H2是一个用Java实现的、类似Derby的数据库,其最大的特点有三个:
- 纯Java实现,只需在代码中引入一个jar文件,你就有了一个数据库;
- 支持内存、文件、C/S三种模式;
- 对SQL标准的兼容,以及对其它数据库的兼容模式。
以上三个特点,决定了它特别适合做持续集成,或者一键部署的项目:
- 开发环境用内存模式,配合Flyway自动建表,与TDD一起带来极速的开发体验;
- 测试环境用内存模式或文件模式,自动化测试一天重新运行个一百遍不是梦;
- 生产环境用文件模式或C/S模式,(Web)服务器动态扩容也不是问题。
如果配合Spring-Boot的Profile机制,一套代码在开发环境、测试环境、生产环境完美无暇地切换:
#application.properties:
#主配置文件中配置好所有默认参数
spring.profiles.active=dev
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.initialize=false
flyway.baseline-version=0.0.0
flyway.baseline-on-migrate=true
flyway.validate-on-migrate=false
#application-dev.properties:
#开发环境使用内存模式,支持一键运行
spring.datasource.url=jdbc:h2:mem:kq
#application-test.properties:
#测试环境使用内存模式或文件模式,支持反复运行或数据持久化
spring.datasource.url=jdbc:h2:~/data/h2/kq
#保密要求高时,请使用JAVA虚拟机参数配置账号密码,如: -Dspring.datasource.username=test
spring.datasource.username=test
spring.datasource.password=123456
#application-prod.properties:
#生产环境建议所有数据库参数都使用JAVA虚拟机参数配置
spring.datasource.url=jdbc:h2:/var/lib/h2/kq
#尤其是账号密码,一定不要写死在配置文件中
#使用JAVA虚拟机参数配置账号密码,如: -Dspring.datasource.username=test
#开发环境运行项目:
mvn spring-boot:run
#测试环境运行项目:
mvn spring-boot:run -Dspring.profiles.active=test
#生产环境通过jar包运行项目:
java -jar kq.jar -Dspring.profiles.active=prod -Dspring.datasource.username=secret ...
#生产环境部署在tomcat下,在setenv.sh中配置参数:
#tomcat/bin/setenv.sh:
JAVA_OPTS="${JAVA_OPTS} -Dspring.profiles.active=prod"
JAVA_OPTS="${JAVA_OPTS} -Dspring.datasource.username=secret"
JAVA_OPTS="${JAVA_OPTS} -Dspring.datasource.password=secret"
...
2. H2+Postgresql+Flyway
H2在开发环境和一些小项目中,是一个非常好的选择,尤其是其内存或文件模式由于没有网络开销,启动、运行快得一塌糊涂。但是当数据量达到百万级时,其性能就明显不如RDBMS头部的几大C/S数据库了。
由于H2和Postgresql对SQL标准的良好兼容性,从H2切换到Postgresql并不是难事,这使我们在同一个项目中享受H2+Flyway带来的极速开发模式,和Postgresql的稳定和大数据量的支持并不冲突,同样配合Spring-Boot我们可以这样配置:
#application.properties:
#主配置文件中配置好所有默认参数
spring.profiles.active=dev
spring.datasource.initialize=false
flyway.baseline-version=0.0.0
flyway.baseline-on-migrate=true
flyway.validate-on-migrate=false
#application-dev.properties:
#开发环境使用内存模式,支持一键运行
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:mydb;MODEL=;MODE=PostgreSQL
#application-sit.properties:
#自动化测试环境使用内存模式,支持一键运行
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:mydb;MODEL=;MODE=PostgreSQL
#application-uat.properties:
#用户模拟测试使用postgresql数据库,保证代码与postgresql的兼容性
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localsrv:5432/mydb
#保密要求高时,请使用JAVA虚拟机参数配置账号密码,如: -Dspring.datasource.username=test
spring.datasource.username=test
spring.datasource.password=123456
#application-prod.properties:
#生产环境建议所有数据库参数都使用JAVA虚拟机参数配置
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localsrv:5432/mydb
#尤其是账号密码,一定不要写死在配置文件中
#开发环境运行项目:
mvn spring-boot:run
#自动化测试环境运行项目:
mvn spring-boot:run -Dspring.profiles.active=sit
#用户模拟测试环境运行项目:
mvn spring-boot:run -Dspring.profiles.active=uat
#生产环境通过jar包运行项目:
java -jar kq.jar -Dspring.profiles.active=prod -Dspring.datasource.username=secret ...
#生产环境部署在tomcat下,在setenv.sh中配置参数:
#tomcat/bin/setenv.sh:
JAVA_OPTS="${JAVA_OPTS} -Dspring.profiles.active=prod"
JAVA_OPTS="${JAVA_OPTS} -Dspring.datasource.username=secret"
JAVA_OPTS="${JAVA_OPTS} -Dspring.datasource.password=secret"
...
需要注意的是,存储过程、非SQL标准的类型、函数二者还是有差异。如果没用到这些,可以不改一行代码在H2和Postgresql之间平滑切换;如果用到了特殊的函数,可以通过Java来扩展H2函数来保证SQL与Postgresql一致;存储过程和特殊类型,就要大家自己去研究了。
3. Docker+Mysql/Postgresql+Flyway
自从六年前用上Docker,我就喜欢上它一句命令就运行/停止/重置/"卸载"一套开源软件(如数据库、Web服务)的能力。通过Docker可以不留痕迹地尝试或使用绝大部分开源的新东西而不需要安装/卸载它们,加上现在Docker对Windows、Mac、Linux的全面支持,现在要用什么软件我首先会去找有没有现成的Docker镜像。
使用Docker+Flyway来做持续集成(数据库的反复重建),也是一个非常好的选择,Docker配置一两句脚本,就可以达到安装或重置数据库的效果:
#删除已经存在的数据库容器,即使不存在也没什么影响
docker rm mydb
#启动数据库容器
docker run --name=mydb \
-p 5432:5432 \
-d --restart=unless-stopped \
-e POSTGRES_USER=${dbuser} \
-e POSTGRES_PASSWORD=${dbpwd} \
-e POSTGRES_DB=${database} \
postgres:alpine
使用Docker还带来另一个好处——开发、测试、生产环境使用相同的镜像,则可以保证三者环境的一致,不太会遇到环境相关的问题。
使用Flyway需要考虑的问题
当然,想要用好Flyway,有些问题也需要提前考虑:
-
如何在现有项目上使用Flyway
我也是从项目开始一段时间之后,才开始使用Flyway的,上线Flyway的时候并没有遇到比手工执行脚本更困难的事情。现有项目想要使用Flyway,可以遵循以下三步:
- 导出现有数据库的建表脚本,和新项目必须的基础数据,放入Flyway升级脚本中,如:src/main/resources/db/migration/V0.0.0__init.sql
- 根据项目实际情况,按前述方法引入Flyway;
- 在Flyway配置中,在调用migrate之前,设置baselineVersion为一个大于步骤1中版本的值,如:V0.0.1。
baselineVerion参数设置方法,同样分Java代码、Spring Xml配置、Spring-Boot配置文件三种,看到这里你一定有能力自己去设置,我就不再复述。
-
升级脚本越来越多怎么办
当经历几十个版本之后,"src/main/resources/db/migration"下面脚本越来越多,带来两个问题:
- 脚本太多,难以维护;
- 通过SQL脚本无法直观地看出最新的表结构。
我的经验和建议是:
- 关闭Flyway的脚本文件校验和检查,即设置validateOnMigrate=false;
- 定期合并历史脚本,比如将V1.0.1,V1.0.2,V1.0.3的脚本都合并到V1.0中,并删除前三个文件。
脚本合并非Flyway才需要的技能,我有一些技巧让SQL更简洁:
- 将ALTER语句合并到CREATE语句中;
- DROP与CTEATE+ALTER抵消;
- UPDATE视情况合并到INSERT语句中(基础数据升级),或者直接去掉(业务数据升级)。
结语
如果你在用关系数据库,赶紧把Flyway用起来,开始你的极速编码体验吧!
如果你因为RDBMS数据升级问题而切换到NoSQL,那赶紧找机会切回RDBMS,用Flyway来管理升级吧!要不然更多线上Bug正等着你!
参考资料
- Flyway官方文档: https://flywaydb.org/getstarted/how
- 实际案例:一键部署极简考勤系统 https://gitee.com/chentao106/kq