03.01 被關係數據庫建表和升級折磨?因為你沒用大道至簡的Flyway

點贊關注轉發~ 每日更新Java前沿技術,日後粉絲福利多多!

前言

六年前Flyway已經是我TDD開發、持續集成工具棧中的重要一環了,作為早期用戶,我早就應該為它做個”廣告“,可惜對創業者來說時間太寶貴了,現在趁著疫情才有機會在家裡總結點東西。雖然現在Flyway已經是Spring-Boot集成工具的一環,但是我發現還是少有人瞭解它的威力。

被關係數據庫建表和升級折磨?因為你沒用大道至簡的Flyway

關係數據庫之殤

你在使用關係數據庫的過程中,是否曾經遇到以下情況,甚至因此一度想要放棄或已經放棄關係數據庫?

場景一:開發環境,多人共用一套數據庫

<code>開發正調試著,忽然代碼報錯“XX字段不存在”:誰TMD又把表結構給改了…/<code>

場景二:開發環境,每個人各自搭建自己的數據庫

<code>開發完一個功能,提交代碼、更新,重啟準備調試下,代碼報錯“XX表不存在”
吼一嗓子:誰又改表結構了?什麼?每個人都要把xxx.sql執行一遍?
...
新員工:我要搭一套開發數據庫,到底應該執行哪些SQL腳本?/<code>

場景三:開發轉測試

<code>測試:你看這個功能是不是有個Bug?
開發1:哦,你要執行一下這個SQL腳本。
測試:嗯,現在沒問題了,但是怎麼保證這個腳本沒有Bug,我能再重現、測試一遍嗎?

開發:額~,你重新搭一遍數據庫吧.../<code>

場景四:搭建一套演示環境

<code>執行SQL腳本1、SQL腳本2、SQL腳本3…啟動服務失敗!
什麼?這個腳本N是測試版本的,war包是已經上線的版本?
刪庫再來一遍.../<code>

場景五:放棄關係數據庫的坑

<code>受不了關係數據庫了,我們切MongoDB吧…嗯,控制檯果然清靜了
...
幾個版本後:
生產環境某些數據查不到了、還有類型不匹配,神馬?
A字段改過名,B字段換了個類型?/<code>

如果上面的問題,你一個都沒遇到過,要麼是你所做的項目太簡單,要麼你們的開發流程非常規範,或者變更控制得太好了,本文你大可跳過了。

如果你正被上面的問題困擾,你可能會因此想要入另一個坑:NoSQL,那麼恭喜你將遇到更多的坑,比如關聯查詢問題、數據版本問題...

<code>注:這裡並不否定NoSQL的價值,各種NoSQL是關係數據庫的良好補充。 

但是如果想將NoSQL做為關係數據庫的替代,那麼你將會陷入比關係數據庫還多的線上問題之中。/<code>

如果你看到這裡,說明你不想逃避問題,那麼讓我們一起來認識這個關係數據庫升級管理的利器——Flyway

Flyway原理介紹

Flyway是什麼?一句話概括,Flyway就是一個數據庫版本管理組件。它的原理非常簡單


被關係數據庫建表和升級折磨?因為你沒用大道至簡的Flyway


  1. 項目啟動時拉起Flyway,先檢查數據庫裡面有沒有Flyway元數據表,沒有則創建;
  2. 檢查Flyway元數據表中的記錄,哪些腳本已經執行過,當前版本是什麼;
  3. 查找代碼中的(名稱滿足規則的)數據庫升級腳本,找出版本號大於(Flyway元數據)當前版本的腳本,逐個執行並記錄執行結果到Flyway元數據表。

你沒看錯,以上三點就是Flyway最核心的功能,我深信熟練掌握我另一篇博客《TDD兩小時實現自定義表達式模板解析器》同學不出一天,就能自己實現這三點功能,對非JVM開發者我推薦在理解以上思想的基本上自己開發一套。

大道至簡,最簡單的設計往往是最有效的。通過以上功能,我們可以很容易做到:

  1. 代碼與數據庫建表&升級腳本放在一起同步管理,通過代碼(SQL)就可以瞭解到表結構;
  2. 無須人工執行任何腳本,運行代碼或服務即可完成(數據庫表結構的)環境搭建;
  3. 從任一版本的環境(表結構),都可以通過運行指定(新)版本的代碼或服務來自動升級到指定新版本;
  4. (配合內存數據庫/Docker/清庫腳本)數據庫搭建&升級腳本很容易與代碼一起反覆測試。

總之,用上Flyway之後,關係數據庫的”關係“,不再是限制你開發效率的瓶頸,反而成為開發&測試的必要約定,提升版本質量的重要保障

快速上手

不管工具多強大,如何用起來,是我們首要關心的,讓我們以各種Java項目環境,來看一下如何在代碼中將Flyway用起來,再由各位自己去細品Flyway對關係數據庫版本管理帶來的巨大改變。


被關係數據庫建表和升級折磨?因為你沒用大道至簡的Flyway


<code>注:所有示例都基於Maven,用Gradle的自己翻譯下依賴,二者都不用的嘛...先研究下POM的依賴關係,再自己去下載jar包吧/<code>

1. 原生Java項目(不用Spring、Spring-Boot)

pom.xml文件中增加flyway的依賴:

<code>    <dependency>
<groupid>org.flywaydb/<groupid>
<artifactid>flyway-core/<artifactid>

<version>3.2.1/<version>
/<dependency>/<code>

java代碼拉起Flyway:

<code>DataSource dataSource = ...
...
//在數據連接創建之後,其它代碼運行之前,先調用Flyway升級
Flyway flyway = new Flyway();
flyway.setDataSource(dataSource);
flyway.migrate();
.../<code>

編寫建表腳本和數據初始化腳本

<code>src
|-main
|-java
|-resources
|-db
|-migration
|-V0.0.1__init-schema.sql

|-V0.0.2__init-data.sql/<code>
<code>注:腳本中的內容,就是正常的建表腳本,或者對上一版本的表結構變更、數據升級。/<code>

2. Spring項目

pom.xml文件中增加flyway的依賴(與原生java項目一樣):

<code>    <dependency>
<groupid>org.flywaydb/<groupid>
<artifactid>flyway-core/<artifactid>

<version>3.2.1/<version>
/<dependency>/<code>

Spring配置文件拉起Flyway:

<code>
<bean>

<property>
<property>
/<bean>/<code>

編寫建表腳本和數據初始化腳本(與原生java項目一致)

<code>src
|-main
|-java
|-resources
|-db
|-migration
|-V0.0.1__init-schema.sql

|-V0.0.2__init-data.sql/<code>
<code>注:腳本中的內容,就是正常的建表腳本,或者對上一版本的表結構變更、數據升級。/<code>

3. Spring-Boot項目

Flyway已經被Spring-Boot整合,成為Spring標準的數據庫升級工具,在Spring-Boot中使用Flyway更簡單,只需添加依賴、編寫數據庫腳本即可,省去了拉起這一步。

pom.xml添加依賴(Spring-Boot已經整合,無須版本號)

<code>    <dependency>
<groupid>org.flywaydb/<groupid>
<artifactid>flyway-core/<artifactid>
/<dependency>/<code>

如果你使用IDEA,其還為你提供了創建Flyway升級腳本的功能,直接以當時日期時間為你生成SQL升級腳本:

被關係數據庫建表和升級折磨?因為你沒用大道至簡的Flyway

生成的腳本名稱如下:

<code>src
|-main
|-java
|-resources
|-db
|-migration
|-V20190315174656__init-schema.sql
|-V20190315201742__init-data.sql
|-V20191225205157__update-userid-to-bitint.sql/<code>

4. 升級腳本示例

為Flyway編寫的SQL腳本並沒有什麼特殊的要求,與正常SQL並無二致,只不過每個腳本編寫時考慮的永遠是對前一個版本表結構的升級,這也是傳統方式下嚴謹的升級腳本應該滿足的要求。

建表腳本示例:

<code>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;/<code>

升級腳本示例

<code>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;/<code>

推薦的搭檔

根據這麼多年我的使用經驗,有下面這些用法,可以最大地發揮Flyway的作用

1. H2+Flyway

H2是一個純Java實現的類似Derby的數據庫,其最大的特點有三個:

  • 純Java實現,只需在代碼中引入一個jar文件,你就有了一個數據庫;
  • 支持內存、文件、C/S三種模式;
  • 對SQL標準的兼容,以及對其它數據庫的兼容模式。

以上三個特點,決定了它特別適合做持續集成,或者一鍵部署的項目:

  • 開發環境用內存模式,配合Flyway自動建表,與TDD一起帶來極速的開發體驗;
  • 測試環境用內存模式或文件模式,自動化測試一天重新運行個一百遍不是夢;
  • 生成環境用文件模式或C/S模式,(Web)服務器動態擴容也不是問題。

如果配合Spring-Boot的Profile機制,一套代碼在開發環境、測試環境、生產環境完美無暇地切換:

<code>#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"
.../<code>

2. H2+Postgresql+Flyway

H2在開發環境和一些小項目中,是一個非常好的選擇,尤其是其內存或文件模式由於沒有網絡開銷,啟動、運行快得一塌糊塗。但是當數據量達到百萬級時,其性能就明顯不如RDBMS頭部的幾大C/S數據庫了。

由於H2和Postgresql對SQL標準的良好兼容性,從H2切換到Postgresql並不是難事,這使我們在同一個項目中享受H2+Flyway帶來的極速開發模式,和Postgresql的穩定和大數據量的支持並不衝突,同樣配合Spring-Boot我們可以這樣配置:

<code>#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"
.../<code>

需要注意的是,存儲過程、非SQL標準的類型、函數二者還是有差異。如果沒用到這些,可以不改一行代碼在H2和Postgresql之間平滑切換;如果用到了特殊的函數,可以通過Java來擴展H2函數來保證SQL與Postgresql一致;存儲過程和特殊類型,就要大家自己去研究了。

3. Docker+Mysql/Postgresql+Flyway

自從六年前用上Docker,我就喜歡上它一句命令就運行/停止/重置/"卸載"一套開源軟件(如數據庫、Web服務)的能力。通過Docker可以不留痕跡地嘗試或使用絕大部分開源的新東西而不需要安裝/卸載它們,加上現在Docker對Windows、Mac、Linux的全面支持,現在要用什麼軟件我首先會去找有沒有現成的Docker鏡像。

使用Docker+Flyway來做持續集成(數據庫的反覆重建),也是一個非常好的選擇,Docker配置一兩句腳本,就可以達到安裝或重置數據庫的效果:

<code>#刪除已經存在的數據庫容器,即使不存在也沒什麼影響
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/<code>

使用Docker還帶來另一個好處——開發、測試、生產環境使用相同的鏡像,則可以保證三者環境的一致,不太會遇到環境相關的問題。

使用Flyway需要考慮的問題

當然,想要用好Flyway,有些問題也需要提前考慮:

  • 如何在現有項目上使用Flyway

我也是從項目開始一段時間之後,才開始使用Flyway的,上線Flyway的時候並沒有遇到比手工執行腳本更困難的事情。現有項目想要使用Flyway,可以遵循以下三步:

  1. 導出現有數據庫的建表腳本,和新項目必須的基礎數據,放入Flyway升級腳本中,如:src/main/resources/db/migration/V0.0.0__init.sql
  2. 根據項目實際情況,按前述方法引入Flyway;
  3. 在Flyway配置中,在調用migrate之前,設置baselineVersion為一個大於步驟1中版本的值,如:V0.0.1。

baselineVerion參數設置方法,同樣分Java代碼、Spring Xml配置、Spring-Boot配置文件三種,看到這裡你一定有能力自己去設置,我就不在複述。

  • 升級腳本越來越多怎麼辦

當經歷幾十個版本之後,"src/main/resources/db/migration"下面腳本越來越多,帶來兩個問題:

  1. 腳本太多,難以維護;
  2. 通過SQL腳本無法直觀地看出最新的表結構。

我的經驗和建議是:

  1. 關閉Flyway的腳本文件校驗和檢查,即設置validateOnMigrate=false;
  2. 定期將合併歷史腳本,比如將V1.0.1,V1.0.2,V1.0.3的腳本都合併到V1.0中,並刪除前三個文件。

腳本合併非Flyway才需要的技能,我有一些技巧讓SQL更簡潔:

  1. 將ALTER語句合併到CREATE語句中;
  2. DROP與CTEATE+ALTER抵消;
  3. UPDATE視情況合併到INSERT語句中(基礎數據升級),或者直接去掉(業務數據升級)。

結語

如果你在用關係數據庫,趕緊把Flyway用起來,開始你的極速編碼體驗吧!

如果你因為RDBMS數據升級問題而切換到NoSQL,那趕緊換機會切回RDBMS,用Flyway來管理升級吧!要不然更多線上Bug正等著你!


分享到:


相關文章: