flyway flywaydb migration database spring boot

資料庫也能做版控!? Flyway使資料庫遷移更容易

莊宗霖 2021/05/06 17:44:34
284

本文架構

Flyway是一套開源數據庫遷移工具,能夠做到資料庫的版本管理,並即時的完成資料遷移作業。

本文將在20分鐘內,透過2個觀念建立、3個實際用例,帶你明白Flyway是什麼,並彙整3步驟實踐資料庫版控、4步驟立即導入既有專案中。

首先,為何需要資料庫遷移?

在Flyway中,任何對資料庫進行的異動都稱為Migration(遷移)

在開發新專案、維護既有系統、處理各式CR的開發週期中,面對各種不同狀況,勢必會變更到資料庫的設計,例如: 異動欄位、索引變更等等...

而上述這些異動也都將由不同的開發人員進行,通常會需要將異動統一進行維護,以利開發週期各個階段執行。

在實務上,不同開發人員、多個環境,此時手動執行資料庫的異動將可能出現如下狀況

  1. 線上問題修復的資料庫異動沒有同步到各個測試環境
  2. 更新版本尚未預先執行異動腳本導致出錯
  3. 異動腳本指令錯誤並未被及時發現

要減少上述問題,我們希望能夠實現下列事項:

  1. 有效追蹤資料庫異動歷程。
  2. 各個環境的資料庫異動不必手動執行,降低錯誤率。
  3. 每次程式進行更版時能夠立即執行DDL & DML變更。

為了達到上述目標,我們將使用Flyway來替我們實現。

 

Flyway資料庫遷移運作方式?

❖ 基本運作原理

❶ 透過歷史紀錄表 (flyway_schema_history) 紀錄每一次的資料庫異動

Flyway首先會檢查資料庫使否有此紀錄表 (flyway_schema_history) ,若沒有則會優先建立此表,接著針對需要被執行的SQL檔案,會先計算出checksum作為驗證,於每次Flyway啟動時根據checksum驗證資料庫是否需要被更改。

備註:checksum計算採用crc-32 checksum。

❷ 透過SQL或Java編寫DDL/DML,並定義其版號使Flyway進行掃描並執行

Flyway將可設定執行順序,預設依據版號進行排序執行,並且寫入歷史紀錄表 (flyway_schema_history) ,Flyway提供多種Migration的方式,以下將針對Migration的類型、實作做更詳細的介紹。

 

❖ 遷移的類型

❶ Versioned Migrations(版本遷移)

用於創建、更新、刪除:表、索引、外鍵。

❷ Undo Migrations(撤銷遷移)

即為Versioned Migratios的回滾機制。

❸ Repeatable Migrations(重複遷移)

用於建立views/procedures/functions/packages/…以及批次寫入特定數據。

 

❖ 遷移的實作

❶ SQL-based migrations

這是最常使用也最便利的實作方式,主要用於實作DDL的變更及簡單的資料異動。

命名規則: (擷取官方圖片)

❷ Java-based migrations

實作情境用於BLOB & CLOB 的變更以及較為複雜的資料異動。

類別命名規則: (擷取官方圖片)

❸ Script migrations

目前官方支援 .ps1, .bat, .cmd, .sh, .bash, .py 的腳本撰寫,其命名方式與SQL-based migrations相同,只差在腳本的副檔名不同。

 

如何開始使用Flyway ?

瞭解基本Flyway的Migration類型與實作種類,接著我將以Spring Boot與測試框架TestContainers進行實務上開發與測試的說明。

❖ 實作一. 搭配Spring Boot

以下三步驟即完成Flyway基於Spring Boot的實際用例配置

❶ Maven配置

<properties>
    <java.version>11</java.version>
    <testcontainers.version>1.15.2</testcontainers.version>
</properties>
<dependencies>
    <!--Spring-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--Logger-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>

    <!--Database-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.23</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <!--Flyway-->
    <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-core</artifactId>
        <version>6.5.7</version>
    </dependency>

    <!--Testing-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mysql</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-bom</artifactId>
            <version>${testcontainers.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

❷ 設定檔配置

# ==============================================================
# = 資料庫設定(非Testcontainers資料庫設定)
# ==============================================================
spring.datasource.url=jdbc:mysql://localhost:3307/flyway_db?useUnicode=true&characterEncoding=utf-8&socketTimeout=60000&connectTimeout=30000
spring.datasource.username=flyuser
spring.datasource.password=123456
spring.jpa.show-sql=true
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.connection.release_mode=AUTO
spring.jpa.hibernate.ddl-auto=validate

#最小空閒連接數
spring.datasource.hikari.minimum-idle=10
#連接池最大大小
spring.datasource.hikari.maximum-pool-size=50
#連接最大空閒時長
spring.datasource.hikari.idle-timeout=60000
#連接生命時長
spring.datasource.hikari.max-lifetime=1800000
#連接的超時時長
spring.datasource.hikari.connection-timeout=30000

## ==============================================================
## FLYWAY配置
### ==============================================================
spring.flyway.url=jdbc:mysql://localhost:3307/flyway_db?useUnicode=true&characterEncoding=utf-8&socketTimeout=60000&connectTimeout=30000
spring.flyway.user=flyuser
spring.flyway.password=123456
spring.flyway.locations=classpath:doc/migration/common,classpath:db/migration
spring.flyway.table=flyway_schema_history
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1.0
spring.flyway.out-of-order=true
spring.flyway.validate-on-migrate=true
spring.flyway.enabled=true

# ==============================================================
# = LOGGING
# ==============================================================
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.flywaydb=debug
logging.level.root=debug

❸ 建置Migration檔案(此例使用SQL & Java)

SQL Base Migration

● V1.0__sql_base_migration_ddl.sql

CREATE TABLE IF NOT EXISTS book_case
(
  id char(36) NOT NULL comment '流水號',
  title varchar(20) NOT NULL comment '書名',
  author varchar(20) NOT NULL comment '作者',
  translator varchar(20) NOT NULL comment '譯者',
  publisher varchar(50) NOT NULL comment '出版社',
  publication_date datetime NOT NULL comment '出版日期',
  create_time datetime NOT NULL comment '創建時間',
  update_time datetime NOT NULL comment '更新時間',
  PRIMARY KEY (id),
  KEY index_createTime (create_time)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

V1.1__sql_base_migration_dml.sql

INSERT INTO book_case (
    id, 
    title, 
    author, 
    translator, 
    publisher, 
    publication_date, 
    create_time, 
    update_time) 
  VALUES (
    '0157e79c-b2dd-4efe-ad40-320cc94c051e', 
    'THE INFINITE GAME', 
    'Simon Sinek', 
    'Huang Tingmin', 
    'Commonwealth Magazine', 
    '2020-12-30 00:00:00', 
    '2021-04-13 20:55:36', 
    '2021-04-13 20:55:36');

 

Java Base Migration

● R__java_base_migration_sample.java

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.UUID;

public class R__uuid_generator extends BaseJavaMigration {

    @Override
    public void migrate(Context context) throws Exception {
        final UUID id = UUID.randomUUID();
        LocalDateTime publishDate = LocalDateTime.of(2020, 12, 30, 0, 0, 0);
        String sql = "INSERT INTO book_case (id, title, author, translator, publisher, publication_date, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

        JdbcTemplate template = new JdbcTemplate(context.getConfiguration().getDataSource());
        template.update(sql, new PreparedStatementSetter() {
            public void setValues(PreparedStatement preparedStatement) throws SQLException {
                preparedStatement.setString(1, id.toString());
                preparedStatement.setString(2, "THE INFINITE GAME");
                preparedStatement.setString(3, "Simon Sinek");
                preparedStatement.setString(4, "Huang Tingmin");
                preparedStatement.setString(5, "Commonwealth Magazine");
                preparedStatement.setTimestamp(6, Timestamp.valueOf(publishDate));
                preparedStatement.setTimestamp(7, Timestamp.valueOf(LocalDateTime.now()));
                preparedStatement.setTimestamp(8, Timestamp.valueOf(LocalDateTime.now()));
            }
        });
    }

}

專案架構以及配置參數說明

● 專案結構 (分別列出SQL Base Migration & Java Base Migration)

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   ├── META-INF
│   │   │   ├── com
│   │   │   │   └── tpisoftware
│   │   │   └── db
│   │   │       └── migration
│   │   │           └── R__java_base_migration_sample.java
│   │   └── resources
│   │       ├── doc
│   │       │   ├── migration
│   │       │   │   └── common
│   │       │   │       ├── V1.0__sql_base_migration_ddl.sql
│   │       │   │       ├── V1.1__sql_base_migration_dml.sql

● 配置參數概述

◆ 連線配置
與datasource配置相同,當Application啟動時,flyway將會進行連線,並執行migration
‣ spring.flyway.url
‣ spring.flyway.user
‣ spring.flyway.password

◆ 檔案存取位置
可接受多個參數(請用逗號分隔),指定flyway讀取執行migration的路徑
‣ spring.flyway.locations

◆ 歷史紀錄表定義
Flyway歷史紀錄表預設命名為flyway_schema_history,若有需要可以更改名稱
‣ spring.flyway.table

◆ 是否執行起始版號
當資料庫不為空,是否要執行起始版本,並建立歷史紀錄表,預設為false,如果並非在專案一開始就導入flyway,就需要設定為true
‣ spring.flyway.baseline-on-migrate

◆ 起始版本設定
設定migration的起始版號
‣ spring.flyway.baseline-version

◆ 執行migration是否允許無序執行
‣ spring.flyway.out-of-order

◆ 執行migration是否自動驗證
‣ spring.flyway.validate-on-migrate

◆ 是否啟用Flyway
‣ spring.flyway.enabled

 

❖ 實作二. 搭配測試框架TestContainers的驗證

以下說明Flyway基於Spring Boot Test With TestContainers的測試驗證。

❶ 容器化測試基本建立方式可以點選此查閱此篇文章,有詳細介紹容器化測試框架,此文不贅述。

動態載入datasource參數,指定給flyway的連線配置(與spring datasource相同)

/**
 * 動態設定參數
 * @param registry
 */
@DynamicPropertySource
static void mssqlProperties(DynamicPropertyRegistry registry) {
    //mysql properties setting
    registry.add("spring.datasource.driver-class-name", mySQLContainer::getDriverClassName);
    registry.add("spring.datasource.url", () -> mySQLContainer.getJdbcUrl());
    registry.add("spring.datasource.username", mySQLContainer::getUsername);
    registry.add("spring.datasource.password", mySQLContainer::getPassword);

    //flyway properties setting
    registry.add("spring.flyway.url", () -> mySQLContainer.getJdbcUrl());
    registry.add("spring.flyway.user", mySQLContainer::getUsername);
    registry.add("spring.flyway.password", mySQLContainer::getPassword);
    registry.add("spring.flyway.enabled", () -> "true");
}

❸ 完成配置運行Spring Test時會進行以下動作:

TestContainers啟動本地端的MySQL Container

Flyway進行驗證

Flyway執行Migration

運行測試程式

 

❖ 實作三. 使用Maven運行Flyway

以下為Flyway基於Maven的實際用例。

❶ 新增Maven-Plugin

<plugin>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-maven-plugin</artifactId>
    <version>6.5.7</version>
</plugin>

 

Maven Flyway Configuration配置

請點選這裡查看資料庫支援及設定說明(本例使用MySQL,並在本地端啟用Docker mysql/mysql-server:8.0作測試)

<configuration>
    <driver>com.mysql.jdbc.Driver</driver>
    <url>jdbc:mysql://localhost:3307/flyway_db</url>
    <user>flyuser</user>
    <password>123456</password>
    <connectRetries>3</connectRetries>
    <createSchemas>true</createSchemas>
    <table>flyway_schema_history</table>
    <locations>
        <location>classpath:doc/migration/common</location>
        <location>classpath:db/migration</location>
    </locations>
    <encoding>utf-8</encoding>
    <target>1.0</target>
    <outOfOrder>true</outOfOrder>
    <validateOnMigrate>true</validateOnMigrate>
    <baselineOnMigrate>true</baselineOnMigrate>
    <baselineVersion>1.0</baselineVersion>
    <baselineDescription>Let's go!</baselineDescription>
</configuration>

 

❸ 執行Migration(-X 查看完整紀錄)

mvn flyway:migrate -X

 

❹ 運行結果(附上分段截圖)

 

Maven執行,Flyway讀取Migration檔案中

 

Flyway的Migration檔案驗證通過後,會開始執行Migration

 

查看資料庫,確認DDL & DML確實執行完畢

 

 

瞭解上述的運作原理及實作方式,另外可依據專案需求自行決定以下執行Migration的方式

 

❶ Command-line tool

Linux / Docker / Windows /Mac OS 皆有支援。

點選此直接查看官網使用說明。

 

 

❷ Gradle

支援Gradle 3.x, Gradle 4.x, Gradle 5.x, and Gradle 6.x 可運行在 Java 8, Java 9, Java 10, Java 11 or Java 12.

點選此直接查看官網使用說明。

 

既有系統如何導入Flyway ?

前面的情境都是以資料庫初始化的狀態說明,那如果既有系統也想導入呢? 以下列出四個步驟

❶ 備份DML 以及 匯出DDL。

❷ 先在本地端實際運行一次Migration,執行DML&DDL。

❸ 匯出上一步驟寫入flyway_schema_history的資料。

❹ 連線至測試環境手動建立flyway_schema_history並匯入資料。

執行完上述步驟,Flyway啟用後即不會再重複執行DML & DDL,只會運行後續新增的版本。

 

總結

本篇文章整理出Flyway的實際案例,盡可能點出大部分的使用情境,從Migration觀念建立、搭配專案常使用的Spring Boot、測試框架驗證、Flyway Maven Plugin使用,以及既有系統的導入指引。

Flyway本身有提供付費版本,在官方文件中皆以[Flyway Teams]標記標註,另外Flyway還提供了Callback功能,方便我們可以自行依據需求在Migration前執行一些動作,本篇文章介紹的Migration功能基本上就能應付大多數情境,而Maven的方式則可以搭配CICD,相信能夠一定程度節省專案開發協作與交付的時間。

更多的細節,以下提供官方提供的文檔以及相關資訊作為參考。

 

參考資源

Flyway文件 https://flywaydb.org/documentation/

Flyway Spring Boot: https://flywaydb.org/documentation/usage/plugins/springboot

莊宗霖