SpringBoot結合Liquibase實現資料庫變更管理

語言: CN / TW / HK

theme: cyanosis

《從零打造專案》系列文章

工具

ORM框架選型

資料庫變更管理

定時任務框架

快取

安全框架

開發規範

前言

在《SpringBoot專案基礎設施搭建》一文中有提到過 liquibase,以及還自定義了一個 Maven 外掛,可能大家當時看到這塊內容,雖然好奇但不知道該如何使用。本文將帶著大家實操一個 SpringBoot 結合 Liquibase 的專案,看看如何新增資料表、修改表字段、初始化資料等功能,順帶使用一下 Liquibase 模版生成器外掛。

如果對 Liquibase 不瞭解,可以先看一下我的上一篇文章《資料庫變更管理:Liquibase or Flyway》。

實操

本專案包含兩個小專案,一個是 liquibase 模版生成器外掛,專案名叫做 liquibase-changelog-generate,另一個專案是 liquibase 應用,叫做 springboot-liquibase。

Liquibase模版生成器外掛

建立一個 maven 專案 liquibase-changelog-generate,本專案具備生成 xml 和 yaml 兩種格式的 changelog,個人覺得 yaml 格式的 changelog 可讀性更高。

1、匯入依賴

```xml

org.apache.maven maven-plugin-api 3.8.6 org.apache.maven.plugin-tools maven-plugin-annotations 3.6.4 provided cn.hutool hutool-all 5.8.5

org.apache.maven.plugins maven-plugin-plugin 3.6.4 hresh true org.springframework.boot spring-boot-maven-plugin 2.6.3 org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 ```

2、定義一個介面,提前準備好公用程式碼,主要是判斷 changelog id 是否有非法字元,並且生成 changelog name。

```java public interface LiquibaseChangeLog {

default String getChangeLogFileName(String sourceFolderPath) { System.out.println("> Please enter the id of this change:"); Scanner scanner = new Scanner(System.in); String changeId = scanner.nextLine(); if (StrUtil.isBlank(changeId)) { return null; }

String changeIdPattern = "^[a-z][a-z0-9_]*$";
Pattern pattern = Pattern.compile(changeIdPattern);
Matcher matcher = pattern.matcher(changeId);
if (!matcher.find()) {
  System.out.println("Change id should match " + changeIdPattern);
  return null;
}

if (isExistedChangeId(changeId, sourceFolderPath)) {
  System.out.println("Duplicate change id :" + changeId);
  return null;
}

Date now = new Date();
String timestamp = DateUtil.format(now, "yyyyMMdd_HHmmss_SSS");
return timestamp + "__" + changeId;

}

default boolean isExistedChangeId(String changeId, String sourceFolderPath) { File file = new File(sourceFolderPath); File[] files = file.listFiles(); if (null == files) { return false; }

for (File f : files) {
  if (f.isFile()) {
    if (f.getName().contains(changeId)) {
      return true;
    }
  }
}
return false;

} } ```

3、每個 changelog 檔案中的 changeSet 都有一個 author 屬性,用來標註是誰建立的 changelog,目前我的做法是執行終端命令來獲取 git 的 userName,如果有更好的實現,望不吝賜教。

```java public class GitUtil {

public static String getGitUserName() { try { String cmd = "git config user.name"; Process p = Runtime.getRuntime().exec(cmd); InputStream is = p.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line = reader.readLine(); p.waitFor(); is.close(); reader.close(); p.destroy(); return line; } catch (IOException | InterruptedException e) { e.printStackTrace(); } return "hresh"; } } ```

4、生成 xml 格式的 changelog

```java @Mojo(name = "generateModelChangeXml", defaultPhase = LifecyclePhase.PACKAGE) public class LiquibaseChangeLogXml extends AbstractMojo implements LiquibaseChangeLog {

// 配置的是本maven外掛的配置,在pom使用configration標籤進行配置 property就是名字, // 在配置裡面的標籤名字。在呼叫該外掛的時候會看到 @Parameter(property = "sourceFolderPath") private String sourceFolderPath;

@Override public void execute() throws MojoExecutionException, MojoFailureException { System.out.println("Create a new empty model changelog in liquibase yaml file."); String userName = GitUtil.getGitUserName();

String changeLogFileName = getChangeLogFileName(sourceFolderPath);
if (StrUtil.isNotBlank(changeLogFileName)) {
  generateXmlChangeLog(changeLogFileName, userName);
}

}

private void generateXmlChangeLog(String changeLogFileName, String userName) { String changeLogFileFullName = changeLogFileName + ".xml"; File file = new File(sourceFolderPath, changeLogFileFullName); String content = "<?xml version=\"1.1\" encoding=\"UTF-8\" standalone=\"no\"?>\n" + "\n" + " \n" + " \n" + ""; try { FileWriter fw = new FileWriter(file.getAbsoluteFile()); BufferedWriter bw = new BufferedWriter(fw); bw.write(content); bw.close(); fw.close(); } catch (IOException e) { e.printStackTrace(); } }

} ```

5、生成 yaml 格式的 changelog

```java @Mojo(name = "generateModelChangeYaml", defaultPhase = LifecyclePhase.PACKAGE) public class LiquibaseChangeLogYaml extends AbstractMojo implements LiquibaseChangeLog {

// 配置的是本maven外掛的配置,在pom使用configration標籤進行配置 property就是名字, // 在配置裡面的標籤名字。在呼叫該外掛的時候會看到 @Parameter(property = "sourceFolderPath") private String sourceFolderPath;

@Override public void execute() throws MojoExecutionException, MojoFailureException { System.out.println("Create a new empty model changelog in liquibase yaml file."); String userName = GitUtil.getGitUserName();

String changeLogFileName = getChangeLogFileName(sourceFolderPath);
if (StrUtil.isNotBlank(changeLogFileName)) {
  generateYamlChangeLog(changeLogFileName, userName);
}

}

private void generateYamlChangeLog(String changeLogFileName, String userName) { String changeLogFileFullName = changeLogFileName + ".yml"; File file = new File(sourceFolderPath, changeLogFileFullName); String content = "databaseChangeLog:\n" + " - changeSet:\n" + " id: " + changeLogFileName + "\n" + " author: " + userName + "\n" + " changes:"; try { FileWriter fw = new FileWriter(file.getAbsoluteFile()); BufferedWriter bw = new BufferedWriter(fw); bw.write(content); bw.close(); fw.close(); } catch (IOException e) { e.printStackTrace(); } }

} ```

6、執行 mvn install 命令,然後會在 maven 的 repository 檔案中生成對應的 jar 包。

專案整體結構如下圖所示:

liquibase 模版生成器專案結構

因為個人感覺 yaml 檔案看起來比較簡潔,所以雖然外掛提供了兩種格式,但後續我選擇 yaml 檔案。

Liquibase專案

本專案只是演示如何通過 Liquibase 新增資料表、修改表字段、初始化資料等功能,並不涉及具體的業務功能,所以程式碼部分會比較少。

1、引入依賴

```xml org.springframework.boot spring-boot-starter-parent 2.6.3

1.8 8.0.19 1.18.20 1.1.18 4.16.1

org.springframework.boot spring-boot-starter-web

mysql mysql-connector-java ${mysql.version} runtime com.alibaba druid-spring-boot-starter ${druid.version} org.liquibase liquibase-core 4.16.1 com.baomidou mybatis-plus-boot-starter 3.5.1 com.baomidou mybatis-plus 3.5.1

org.springframework.boot spring-boot-maven-plugin org.liquibase liquibase-maven-plugin 4.16.1 src/main/resources/application.yml true com.msdn.hresh liquibase-changelog-generate 1.0-SNAPSHOT src/main/resources/liquibase/changelogs/ ```

2、application.yml 配置如下:

```yaml server: port: 8088

spring: application: name: springboot-liquibase datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mysql_db?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false username: root password: root liquibase: enabled: true change-log: classpath:liquibase/master.xml # 記錄版本日誌表 database-change-log-table: databasechangelog # 記錄版本改變lock表 database-change-log-lock-table: databasechangeloglock

mybatis: mapper-locations: classpath:mapper/*Mapper.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl lazy-loading-enabled: true

changeLogFile: src/main/resources/liquibase/master.xml

輸出檔案路徑配置

outputChangeLogFile: src/main/resources/liquibase/out/out.xml

```

3、resources 目錄下建立 Liquibase 相關檔案,主要是 master.xml

```xml

```

還需要建立 liquibase/changelogs 目錄。

4、建立一個啟動類,準備啟動專案

```java @SpringBootApplication public class LiquibaseApplication {

public static void main(String[] args) { SpringApplication.run(LiquibaseApplication.class, args); } } ```

接下來我們就進行測試使用 Liquibase 來進行資料庫變更控制。

建立表

準備通過 Liquibase 來建立資料表,首先點選下面這個命令:

image-20220927212757676

然後在控制檯輸入 create_table_admin,回車,我們可以看到對應的檔案如下:

image-20221124162316987

我們填充上述檔案,將建表字段加進去。

yaml databaseChangeLog: - changeSet: id: 20221124_161016_997__create_table_admin author: hresh changes: - createTable: tableName: admin columns: - column: name: id type: ${id} autoIncrement: true constraints: primaryKey: true nullable: false - column: name: name type: varchar(50) - column: name: password type: varchar(100) - column: name: create_time type: ${time}

關於 Liquibase yaml SQL 格式推薦去官網查詢。

啟動專案後,先來檢視控制檯輸出:

liquibase執行日誌

接著去資料庫中看 databasechangelog 表記錄

databasechangelog 表記錄

以及 admin 表結構

admin表字段

新增表字段

使用我們的模版生成器外掛,輸入 add_column_address_in_admin,回車得到一個模版檔案,比如說我們在 admin 表中新增 address 欄位。

yaml databaseChangeLog: - changeSet: id: 20221124_163754_923__add_column_address_in_admin author: hresh changes: - addColumn: tableName: admin columns: - column: name: address type: varchar(100)

再次重啟專案,這裡我就不貼控制檯輸出日誌了,直接去資料庫中看 admin 表的變化。

admin表字段

建立索引

輸入 create_index_in_admin,回車得到模版檔案,然後填充內容:

yaml databaseChangeLog: - changeSet: id: 20221124_164641_992__create_index_in_admin author: hresh changes: - createIndex: tableName: admin indexName: idx_name columns: - column: name: name

檢視 admin 表變化:

admin表字段

如果要修改索引,一般都是先刪再增,刪除索引可以這樣寫:

yaml databaseChangeLog: - changeSet: id: 20221124_164641_992__create_index_in_admin author: hresh changes: - dropIndex: tableName: admin indexName: idx_name

初始化資料

輸入 init_data_in_admin ,修改模版檔案

yaml databaseChangeLog: - changeSet: id: 20221124_165413_348__init_data_in_admin author: hresh changes: - sql: dbms: mysql sql: "insert into admin(name,password) values('hresh','1234')" stripComments: true

重啟專案後,可以發現數據表中多了一條記錄。

關於 Liquibase 還有很多操作沒介紹,等大家實際應用時再去發掘了,這裡就不一一介紹了。

Liquibase 好用是好用,那麼有沒有視覺化的介面呢?答案當然是有的。

plugin-生成資料庫修改文件

雙擊liquibase plugin面板中的liquibase:dbDoc選項,會生成資料庫修改文件,預設會生成到target目錄中,如下圖所示

liquibase文件

訪問index.html會展示如下頁面,簡直應有盡有

liquibase視覺化介面

關於 liquibase 的更多有意思的命令使用,可以花時間再去挖掘一下,這裡就不過多介紹了。

問題

控制檯輸出 liquibase.changelog Reading resource 讀取了很多沒必要的檔案

控制檯截圖如下所示:

image-20221124105341305

我們查詢一個 AbstractChangeLogHistoryService 檔案所在位置,發現它是 liquibase-core 包下的檔案,如下所示:

liquibase-core檔案展示

為什麼會這樣呢?首先來看下我們關於 liquibase 的配置,如下圖所示:

image-20221124105629800

其中 master.xml 檔案內容如下:

```xml

```

從上面可以看出,resource 目錄下關於 liquibase 的資料夾和 liquibase-core 中的一樣,難道是因為重名導致讀取了那些檔案,我們試著修改一下資料夾名稱,將 changelog 改為 changelogs,順便修改 master.xml。

再次重啟專案,發現控制檯就正常輸出了。

簡單去看了下 Liquibase 的執行流程,看看讀取 changelog 時做了哪些事情,最終定位到 liquibase.integration.spring.SpringResourceAccessor 檔案中的 list()方法,原始碼如下:

```java public SortedSet list(String relativeTo, String path, boolean recursive, boolean includeFiles, boolean includeDirectories) throws IOException { String searchPath = this.getCompletePath(relativeTo, path); if (recursive) { searchPath = searchPath + "/*"; } else { searchPath = searchPath + "/"; }

searchPath = this.finalizeSearchPath(searchPath); Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(this.resourceLoader).getResources(searchPath); SortedSet returnSet = new TreeSet(); Resource[] var9 = resources; int var10 = resources.length;

for(int var11 = 0; var11 < var10; ++var11) { Resource resource = var9[var11]; boolean isFile = this.resourceIsFile(resource); if (isFile && includeFiles) { returnSet.add(this.getResourcePath(resource)); }

if (!isFile && includeDirectories) {
  returnSet.add(this.getResourcePath(resource));
}

}

return returnSet; } ```

其中 searchPath 變數值為 classpath:/liquibase/changelog/*,然後通過 ResourcePatternUtils 讀取檔案時,就把 liquibase-core 包下同路徑的檔案都掃描出來了。如下圖所示:

image-20221124113407281

所以我們的應對措施暫時定為修改 changelog 目錄名為 changelogs。

總結

感興趣的朋友可以去我的 Github 下載相關程式碼,如果對你有所幫助,不妨 Star 一下,謝謝大家支援!