# 数据库版本控制 - liqiubase
# 简介
LiquiBase 是一个用于数据库重构和迁移的开源工具,通过日志文件的形式记录数据库的变更,然后执行日志文件中的修改,将数据库更新或回滚到一致的状态。
- LiquiBase 的主要特点有:
支持几乎所有主流的数据库,如 MySQL, PostgreSQL, Oracle, Sql Server, DB2 等 - 支持多开发者的协作维护
- 日志文件支持多种格式,如 XML, YAML, JSON, SQL 等
- 支持多种运行方式,如命令行、Spring 集成、Maven 插件、Gradle 插件等
# 同类型其他产品
liquibase 主要特性
- 灵活的 Schema 变更:支持 SQL, XML, JSON, and YAML 语言
- 自动生成可 review 的 SQL 脚本
- 支持可重复和不可重复执行的变更
- 集成和扩展能力
- 自动或者通过自定义回滚 SQL 撤销数据库的变更
- 使用上下文和前提条件来微调脚本的执行
- Liquibase hub 实时监控和可观测能力
Flyway 主要特性
- 基于 SQL/JAVA 变更
- 重复变更
- 撤销变更
- Callback
- 批量变更
- ....
这里为什么使用 liquibase 是因为我在用集成 flyway 对旧版本的 sqlserver 需要社区版才支持,而 liquibase 对 sql 变更更灵活
# 说明
- changelog 是 Liquibase 版本控制的核心,Liquibase 通过有序的 changelog 罗列你对数据库的更改,你可以把它想象成为一个账本,包含你对数据库所有操作的文件
- LiquiBase 在执行 changelog 时,会在数据库中插入 DATABASECHANGELOG 和 DATABASECHANGELOGLOCK 两张表。在执行 changelog 中的 changeSet 时,会首先查看 DATABASECHANGELOG 表,如果已经执行过,则会跳过,如果没有执行过,则执行并记录 changelog 日志。
- DATABASECHANGELOG 表:数据表,用于记录已经应用的变更集的信息,包括变更集 ID、应用时间、应用用户等信息。这个表格是 Liquibase 用来判断数据库的变更状态,避免重复应用变更集的关键
- DATABASECHANGELOGLOCK 表:数据表,用于控制 Liquibase 在应用变更时的并发访问。当 Liquibase 应用变更时,它会自动在表中创建一行记录,防止其他进程或线程同时修改数据库。当变更应用完成后,Liquibase 会删除这个表格中的记录,以允许其他进程或线程修改数据库。
# 整合 spring boot
本章 liquibase 整合 springboot 是以组件的方式封装的,通过自定义配置项目是否需要启用数据库版本控制。
本项目是一个用 mybatis 实现可动态添加多数据源的项目,所以 liquibase 也需要动态的为多数据源添加版本控制
要实现多数据源的版本控制需要先实现单个的,下面是单个数据源的整合步骤:
# 单数据源整合
pox.xml
文件<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
配置 application.yml
spring:
liquibase:
change-log: classpath:liquibase/master.xml #指定 master.xml 文件的位置
新建 master.xml(用于配置你的增量脚本)
master.xml 配置如下(示例)
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<!-- id:唯一 id author:修改人 -->
<changeSet id="1.0.0-20240318-001" author="Tz">
<!-- 使用 sql 脚本 一些建表语句和插入语句 -->
<sqlFile path="classpath:db/mysql/master/scaffold_init.sql"/>
</changeSet>
</databaseChangeLog>
注册 liquibase bean
@Configuration
public class LiquibaseConfiguration() {
@Bean
@ConfigurationProperties(prefix = "spring.liquibase")
public LiquibaseProperties getLiquibaseProperties() {
return new LiquibaseProperties();
}
@Bean
public SpringLiquibase liquibase(DataSource dataSource) {
LiquibaseProperties liquibaseProperties = getLiquibaseProperties();
SpringLiquibase liquibase = new SpringLiquibase();
liquibase.setChangeLog(liquibaseProperties.getChangeLog());
liquibase.setDataSource(dataSource);
liquibase.setShouldRun(true);
return liquibase;
}
}
到此就实现了单数据源的数据库版本控制了,思考两个问题
一般项目基本会有多个数据源,而且是不同的数据库,这个时候怎么办
# 多数据源整合
需要达到的效果:
开始:
项目结构
pom.xml
<dependencies>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<!-- <optional>true</optional>-->
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
</dependency>
</dependencies>
application.yml 这里不在是用原来 liquibase 的配置,而是自己定义的
liquibase:
changelog: changelog-master.xml
enabled: true
spring:
# 数据源配置项
autoconfigure:
exclude:
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 1 # 初始连接数
min-idle: 1 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
name: scaffold4
url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&createDatabaseIfNotExist=true # MySQL Connector/J 8.X 连接的示例
username: root
password: 123456
slave: # 模拟从库,可根据自己需要修改
name: scaffold4
lazy: true # 开启懒加载,保证启动速度
url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name};createTablesIfNotExist=true;encrypt=false;trustServerCertificate=true; # SQLServer 连接的示例
username: sa
password: 123456
代码
/**
* <p> Project: scaffold - LiquibaseProperties </p>
*
* 数据库版本控制 liquibase 配置类
* @author Tz
* @version 1.0.0
* @date 2024/02/29 19:13
* @since 1.0.0
*/
@ConfigurationProperties("liquibase")
@Data
@Validated
public class LiquibaseProperties {
/**
* Change log configuration path.
*/
private String changeLog = "changelog-master.yaml";
/**
* Whether to clear all checksums in the current changelog, so they will be
* recalculated upon the next update.
*/
private boolean clearChecksums;
/**
* Comma-separated list of runtime contexts to use.
*/
private String contexts;
/**
* Default database schema.
*/
private String defaultSchema;
/**
* Schema to use for Liquibase objects.
*/
private String liquibaseSchema;
/**
* Tablespace to use for Liquibase objects.
*/
private String liquibaseTablespace;
/**
* Name of table to use for tracking change history.
*/
private String databaseChangeLogTable = "DATABASECHANGELOG";
/**
* Name of table to use for tracking concurrent Liquibase usage.
*/
private String databaseChangeLogLockTable = "DATABASECHANGELOGLOCK";
/**
* Whether to first drop the database schema.
*/
private boolean dropFirst;
/**
* Whether to enable Liquibase support.
*/
private boolean enabled = true;
/**
* Login user of the database to migrate.
*/
private String user;
/**
* Login password of the database to migrate.
*/
private String password;
/**
* Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.
*/
private String driverClassName;
/**
* JDBC URL of the database to migrate. If not set, the primary configured data source
* is used.
*/
private String url;
/**
* Comma-separated list of runtime labels to use.
*/
private String labels;
/**
* Change log parameters.
*/
private Map<String, String> parameters;
/**
* File to which rollback SQL is written when an update is performed.
*/
private File rollbackFile;
/**
* Whether rollback should be tested before update is performed.
*/
private boolean testRollbackOnUpdate;
/**
* Tag name to use when applying database changes. Can also be used with
* "rollbackFile" to generate a rollback script for all existing changes associated
* with that tag.
*/
private String tag;
}
// 数据库版本控制启动类
/**
* <p> Project: scaffold - ScaffoldLiquibaseAutoConfiguration </p>
*
* Liquibase 数据库版本控制启动类
* @author Tz
* @version 1.0.0
* @date 2024/02/23 18:56
* @since 1.0.0
*/
@AutoConfiguration(
after = {DataSourceAutoConfiguration.class}
)
@ConditionalOnProperty(prefix = "liquibase", value = "enabled", matchIfMissing = true)
@EnableConfigurationProperties({LiquibaseProperties.class})
public class ScaffoldLiquibaseAutoConfiguration {
@Bean
public SpringLiquibase liquibase(LiquibaseProperties liquibaseProperties, DynamicRoutingDataSource dataSource, DynamicDataSourceProperties dynamicDataSourceProperties) throws SQLException, LiquibaseException {
// 本来想弄成返回一个 list 集合,或者用 beanfactory 但是不行
// 投机做法 因为这里会被 liquibase 读取到 会在动态数据源初始化完毕执行,所有在 DynamicLiquibaseSource 中动态注册多个
return new DynamicLiquibaseSource(liquibaseProperties, dataSource, dynamicDataSourceProperties).initDynamicLiquibase();
}
}
DynamicLiquibaseSource.java
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import com.tz.scaffold.framework.liquibase.config.LiquibaseProperties;
import com.tz.scaffold.framework.liquibase.core.util.JdbcUrlMetaDataUtil;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.integration.spring.SpringLiquibase;
import liquibase.resource.ClassLoaderResourceAccessor;
import lombok.AllArgsConstructor;
import lombok.Data;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Map;
/**
* <p> Project: scaffold - DynamicLiquibaseSource </p>
*
* liquibase 动态数据源数据库版本控制初始化器
* @author Tz
* @version 1.0.0
* @date 2024/03/02 19:49
* @since 1.0.0
*/
@Data
@AllArgsConstructor
public class DynamicLiquibaseSource {
private LiquibaseProperties liquibaseProperties;
private DynamicRoutingDataSource dataSource;
private DynamicDataSourceProperties dynamicDataSourceProperties;
/**
* 初始化动态数据库版本控制
* @return liquibase 版本控制器
* @throws SQLException
* @throws LiquibaseException
*/
public SpringLiquibase initDynamicLiquibase() throws SQLException, LiquibaseException {
// 读取到多个数据源的配置
Map<String, DataSourceProperty> dataSourcePropertyMap = dynamicDataSourceProperties.getDatasource();
for (DataSource tempDataSource : dataSource.getDataSources().values()) {
String name = ((ItemDataSource) tempDataSource).getName();
// 根据配置替换数据类型、数据源的名称(master、slav 等)
String changeLogPath = "classpath:db/{JdbcType}/{relation}/" + liquibaseProperties.getChangeLog();
DataSourceProperty dataSourceProperty = dataSourcePropertyMap.get(name);
// 如果不存在数据库则自动创建
JdbcUrlMetaDataUtil.createDataBasesIfNotExists(dataSourceProperty.getUrl(), dataSourceProperty.getUsername(), dataSourceProperty.getPassword());
DatabaseMetaData metaData = tempDataSource.getConnection().getMetaData();
DbType dbType = JdbcUtils.getDbType(metaData.getURL());
if (dbType.getDb().contains("sqlserver")) {
changeLogPath = changeLogPath.replace("{JdbcType}", "sqlserver").replace("{relation}", name);
} else {
changeLogPath = changeLogPath.replace("{JdbcType}", dbType.getDb()).replace("{relation}", name);
}
// 手动注册 liquibase
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(tempDataSource.getConnection()));
Liquibase liquibase = new Liquibase(changeLogPath, new ClassLoaderResourceAccessor(), database);
liquibase.update(new Contexts(), new LabelExpression());
}
// 随便返回一个
SpringLiquibase liquibase = new SpringLiquibase();
String changeLogPath = "classpath:db/{JdbcType}/{relation}/" + liquibaseProperties.getChangeLog();
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
DbType dbType = JdbcUtils.getDbType(metaData.getURL());
changeLogPath = changeLogPath.replace("{JdbcType}", dbType.getDb()).replace("{relation}", "master");
liquibase.setChangeLog(changeLogPath);
liquibase.setDataSource(dataSource);
liquibase.setShouldRun(true);
return liquibase;
}
}
JdbcInfoEnum.java
import lombok.AllArgsConstructor; | |
import lombok.Getter; | |
/** | |
* <p> Project: scaffold - JdbcInfoEnum </p> | |
* | |
* jdbc 连接的信息 | |
* @author Tz | |
* @version 1.0.0 | |
* @date 2024/03/16 19:25 | |
* @since 1.0.0 | |
*/ | |
@Getter | |
@AllArgsConstructor | |
public enum JdbcInfoEnum { | |
/** | |
* 数据库名称 | |
*/ | |
DATABASES_NAME("databases_name", "数据库名称"), | |
/** | |
* 连接参数值 | |
*/ | |
PARAM_VALUES("param_values", "连接参数值"), | |
/** | |
* 原始数据库连接 | |
*/ | |
ORIGINAL_URL("original_url", "原始数据库连接"), | |
/** | |
* url 中是否存在连接的数据库 | |
*/ | |
EXISTS_DATABASES_FOR_URL("exists_databases_for_url", "url中是否存在连接的数据库"); | |
private final String infoName; | |
private final String explain; | |
} |
JdbcUrlMetaDataUtil.java
package com.tz.scaffold.framework.liquibase.core.util; | |
import com.baomidou.mybatisplus.core.toolkit.Assert; | |
import com.baomidou.mybatisplus.core.toolkit.StringUtils; | |
import com.tz.scaffold.framework.liquibase.core.enums.JdbcInfoEnum; | |
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.jdbc.datasource.DriverManagerDataSource; | |
import java.sql.Connection; | |
import java.sql.ResultSet; | |
import java.sql.SQLException; | |
import java.sql.Statement; | |
import java.util.HashMap; | |
import java.util.Map; | |
import java.util.regex.Matcher; | |
import java.util.regex.Pattern; | |
import static com.tz.scaffold.framework.liquibase.core.enums.JdbcInfoEnum.*; | |
/** | |
* <p> Project: scaffold - JdbcUrlMetaDataUtil </p> | |
* | |
* 获取 jdbc 中相关信息的工具类 | |
* @author Tz | |
* @version 1.0.0 | |
* @date 2024/03/16 19:47 | |
* @since 1.0.0 | |
*/ | |
@Slf4j | |
public class JdbcUrlMetaDataUtil { | |
/** | |
* 如果 jdbcUrl 中的连接的数据库不存在则创建 | |
* @param jdbcUrl 数据库连接 | |
* @param username 用户名 | |
* @param password 密码 | |
* @throws SQLException 连接失败异常 | |
*/ | |
public static void createDataBasesIfNotExists(String jdbcUrl, String username, String password) throws SQLException { | |
Map<String, String> jdbcUrlInfo = getJdbcUrlInfo(jdbcUrl); | |
String existsDatabasesForUrlFlag = jdbcUrlInfo.get(EXISTS_DATABASES_FOR_URL.getInfoName()); | |
String databaseName = jdbcUrlInfo.get(DATABASES_NAME.getInfoName()); | |
String existsDatabaseSql = getExistsDatabasesSql(jdbcUrl, databaseName); | |
// 如果如果是连接默认数据库 则不执行自动创建数据库 | |
if ("false".equalsIgnoreCase(existsDatabasesForUrlFlag)) { | |
return; | |
} | |
DriverManagerDataSource dataSource = new DriverManagerDataSource(); | |
dataSource.setDriverClassName(getDriverNameFromUrl(jdbcUrl)); | |
// 设置去除数据库参数后的 jdbcUrl | |
dataSource.setUrl(jdbcUrlInfo.get(ORIGINAL_URL.getInfoName()) + jdbcUrlInfo.get(PARAM_VALUES.getInfoName())); | |
dataSource.setUsername(username); | |
dataSource.setPassword(password); | |
// 检查数据库是否存在,如果不存在则创建数据库 | |
try (Connection connection = dataSource.getConnection(); | |
Statement statement = connection.createStatement(); | |
ResultSet resultSet = statement.executeQuery(existsDatabaseSql)) { | |
// 如果不存在则新增数据库 | |
if (!resultSet.next()) { | |
statement.executeUpdate("CREATE DATABASE " + databaseName); | |
} | |
} catch (SQLException e) { | |
e.printStackTrace(); | |
throw e; | |
} | |
} | |
/** | |
* 通过 jdbcUrl 和数据库名称 返回对应数据库是否存在的查询语句 | |
* @param url jdbc 连接 | |
* @param databaseName 数据库名称 | |
* @return 对应数据库是否存在的查询语句 | |
*/ | |
public static String getExistsDatabasesSql(String url, String databaseName) { | |
String existsDatabaseSql = ""; | |
if (url.startsWith("jdbc:mysql")) { | |
existsDatabaseSql = "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:cobar")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:mariadb")) { | |
existsDatabaseSql = "SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:oracle")) { | |
existsDatabaseSql = "SELECT 1 FROM dual"; | |
} else if (url.startsWith("jdbc:sqlserver")) { | |
existsDatabaseSql = "SELECT 1 FROM sys.databases WHERE name = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:postgresql")) { | |
existsDatabaseSql = "SELECT 1 FROM pg_database WHERE datname = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:hsqldb")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_SCHEMAS WHERE SCHEMA_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:db2")) { | |
existsDatabaseSql = "SELECT 1 FROM syscat.databases WHERE name = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:sqlite")) { | |
existsDatabaseSql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name='" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:h2")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:lealone")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:dm")) { | |
existsDatabaseSql = "select 1 table_schema from all_objects where object_type='SCH' and object_name = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:xugu")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:kingbase")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:phoenix")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:zenith")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} else if (url.startsWith("jdbc:gbase")) { | |
existsDatabaseSql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = '" + databaseName + "';"; | |
} | |
else { | |
// 处理未知数据库类型的逻辑 | |
System.out.println("Unknown database type: " + url); | |
} | |
return existsDatabaseSql; | |
} | |
/** | |
* 根据 jdbc url 获取驱动名称 | |
* @param url jdbc 连接 | |
* @return jdbc 连接对应的驱动名称 | |
*/ | |
public static String getDriverNameFromUrl(String url) { | |
String driverName = null; | |
if (url.startsWith("jdbc:mysql")) { | |
driverName = "com.mysql.cj.jdbc.Driver"; | |
} else if (url.startsWith("jdbc:cobar")) { | |
driverName = "cobar.jdbc.driver.CobarDriver"; | |
} else if (url.startsWith("jdbc:mariadb")) { | |
driverName = "org.mariadb.jdbc.Driver"; | |
} else if (url.startsWith("jdbc:oracle")) { | |
driverName = "oracle.jdbc.driver.OracleDriver"; | |
} else if (url.startsWith("jdbc:sqlserver")) { | |
driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; | |
} else if (url.startsWith("jdbc:sqlserver2012")) { | |
driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; | |
} else if (url.startsWith("jdbc:postgresql")) { | |
driverName = "org.postgresql.Driver"; | |
} else if (url.startsWith("jdbc:hsqldb")) { | |
driverName = "org.hsqldb.jdbc.JDBCDriver"; | |
} else if (url.startsWith("jdbc:db2")) { | |
driverName = "com.ibm.db2.jcc.DB2Driver"; | |
} else if (url.startsWith("jdbc:sqlite")) { | |
driverName = "org.sqlite.JDBC"; | |
} else if (url.startsWith("jdbc:h2")) { | |
driverName = "org.h2.Driver"; | |
} else if (url.startsWith("jdbc:lealone")) { | |
driverName = "org.lealone.driver.LealoneDriver"; | |
} else if (url.startsWith("jdbc:dm")) { | |
driverName = "dm.jdbc.driver.DmDriver"; | |
} else if (url.startsWith("jdbc:xugu")) { | |
driverName = "xugu.jdbc.XuguDriver"; | |
} else if (url.startsWith("jdbc:kingbase")) { | |
driverName = "kingbase.Driver"; | |
} else if (url.startsWith("jdbc:phoenix")) { | |
driverName = "org.apache.phoenix.jdbc.PhoenixDriver"; | |
} else if (url.startsWith("jdbc:zenith")) { | |
driverName = "com.huawei.gauss.jdbc.ZenithDriver"; | |
} else if (url.startsWith("jdbc:gbase")) { | |
driverName = "com.gbase.jdbc.Driver"; | |
} else if (url.startsWith("jdbc:gbaseDBT-SQLI")) { | |
driverName = "com.gbasedbt.jdbc.IfxDriver"; | |
} else if (url.startsWith("jdbc:informix-sqli")) { | |
driverName = "com.informix.jdbc.IfxDriver"; | |
} else { | |
driverName = "Unknown"; | |
} | |
return driverName; | |
} | |
/** | |
* 通过 jdbc 连接获取相关的信息的集合 | |
* @param jdbcUrl jdbc 连接 | |
* @return 返回相关信息的集合 具体值:{@link JdbcInfoEnum} | |
*/ | |
public static Map<String, String> getJdbcUrlInfo(String jdbcUrl) { | |
Assert.isFalse(StringUtils.isBlank(jdbcUrl), "Error: The jdbcUrl is Null, Cannot read database type"); | |
String url = jdbcUrl.toLowerCase(); | |
Map<String, String> jdbcInfo = new HashMap(); | |
if (url.contains(":mysql:") || url.contains(":cobar:")) { | |
Pattern pattern = Pattern.compile("jdbc:(.*?):\\/\\/([^:/]+)(:[0-9]+)?\\/([^?]+)\\??(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(5)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:" + matcher.group(1) + "://" + matcher.group(2) + matcher.group(3) + "?"); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":mariadb:")) { | |
Pattern pattern = Pattern.compile("jdbc:mariadb://([^:/]+)(:[0-9]+)?/([^?]+)\\??(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:mariadb://" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":oracle:")) { | |
Pattern pattern = Pattern.compile("jdbc:oracle:thin:@([^:/]+)(:[0-9]+)?/([^;]+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:oracle:thin:@" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":sqlserver:") || url.contains(":microsoft:") || url.contains(":sqlserver2012:")) { | |
Pattern pattern = Pattern.compile("jdbc:sqlserver://([^:/]+)(:[0-9]+)?;DatabaseName=([^;]+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:sqlserver://" + matcher.group(1) + matcher.group(2) + ";"); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":postgresql:")) { | |
Pattern pattern = Pattern.compile("jdbc:postgresql://([^:/]+)(:[0-9]+)?/([^?]+)\\??(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:postgresql://" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":hsqldb:")) { | |
Pattern pattern = Pattern.compile("jdbc:hsqldb:(file:|mem:|res:)([^;]+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(2)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:hsqldb:" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":db2:")) { | |
Pattern pattern = Pattern.compile("jdbc:db2://([^:/]+)(:[0-9]+)?/([^:]+):([^;]+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(5)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:db2://" + matcher.group(1) + matcher.group(2) + "/" + matcher.group(3)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":sqlite:")) { | |
Pattern pattern = Pattern.compile("jdbc:sqlite:(.+)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(1)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), ""); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:sqlite:" + matcher.group(1)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":h2:")) { | |
} else if (url.contains(":lealone:")) { | |
} else if (regexFind(":dm\\d*:", url)) { | |
Pattern pattern = Pattern.compile("jdbc:dm://([^:/]+)(:[0-9]+)?/([^;]+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:dm://" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":xugu:")) { | |
Pattern pattern = Pattern.compile("jdbc:xugu://([^:/]+)(:[0-9]+)?/([^?]+)\\??(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:dm://" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (regexFind(":kingbase\\d*:", url)) { | |
Pattern pattern = Pattern.compile("jdbc:kingbase://([^:/]+)(:[0-9]+)?/([^?]+)\\??(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:dm://" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":phoenix:")) { | |
Pattern pattern = Pattern.compile("jdbc:kingbase://([^:/]+)(:[0-9]+)?/([^?]+)\\??(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:dm://" + matcher.group(1) + matcher.group(2)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":zenith:")) { | |
} else if (url.contains(":gbase:")) { | |
} else if (url.contains(":gbasedbt-sqli:") || url.contains(":informix-sqli:")) { | |
Pattern pattern = Pattern.compile("jdbc:informix-sqli://([^:/]+)(:[0-9]+)?/([^:]+):informixServer=([^;]+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(5)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:informix-sqli://" + matcher.group(1) + matcher.group(2) + "/" + matcher.group(3) + ":informixServer=" + matcher.group(4)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":ch:") || url.contains(":clickhouse:")) { | |
} else if (url.contains(":oscar:")) { | |
} else if (url.contains(":sybase:")) { | |
Pattern pattern = Pattern.compile("jdbc:sybase:Tds:(.+):(\\d+)/(\\w+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(4)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:sybase:Tds:" + matcher.group(1) + ":" + matcher.group(2) + "/" + matcher.group(3)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":oceanbase:")) { | |
} else if (url.contains(":highgo:")) { | |
} else if (url.contains(":cubrid:")) { | |
Pattern pattern = Pattern.compile("jdbc:cubrid:([^:]+):(\\d+):(\\w+):(\\w+);?(.*)"); | |
Matcher matcher = pattern.matcher(jdbcUrl); | |
if (matcher.find()) { | |
jdbcInfo.put(DATABASES_NAME.getInfoName(), matcher.group(3)); | |
jdbcInfo.put(PARAM_VALUES.getInfoName(), matcher.group(5)); | |
jdbcInfo.put(ORIGINAL_URL.getInfoName(), "jdbc:cubrid:" + matcher.group(1) + ":" + matcher.group(2) + ":" + matcher.group(3) + ":" + matcher.group(4)); | |
} else { | |
jdbcInfo.put(EXISTS_DATABASES_FOR_URL.getInfoName(), "false"); | |
} | |
} else if (url.contains(":goldilocks:")) { | |
} else if (url.contains(":csiidb:")) { | |
} else if (url.contains(":sap:")) { | |
} else if (url.contains(":impala:")) { | |
} else if (url.contains(":vertica:")) { | |
} else if (url.contains(":xcloud:")) { | |
} else if (url.contains(":firebirdsql:")) { | |
} else if (url.contains(":redshift:")) { | |
} else if (url.contains(":opengauss:")) { | |
} else if (url.contains(":taos:") || url.contains(":taos-rs:")) { | |
} else if (url.contains(":informix")) { | |
} else if (url.contains(":sinodb")) { | |
} else if (url.contains(":uxdb:")) { | |
} else { | |
log.warn("The jdbcUrl is " + jdbcUrl + ", Mybatis Plus Cannot Read Database type or The Database's Not Supported!"); | |
} | |
return jdbcInfo; | |
} | |
/** | |
* 正则匹配 | |
* | |
* @param regex 正则 | |
* @param input 字符串 | |
* @return 验证成功返回 true,验证失败返回 false | |
*/ | |
public static boolean regexFind(String regex, CharSequence input) { | |
if (null == input) { | |
return false; | |
} | |
return Pattern.compile(regex).matcher(input).find(); | |
} | |
} |
以上就是实现的全部代码
# 使用
从上面的 application.yml 配置文件中可以看到 数据源连接的数据库
mysql -> scaffold4
sqlserver -> scaffold3
mysql | |
SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = 'scaffold4' | |
null | |
sqlserver | |
SELECT 1 FROM sys.databases WHERE name = 'scaffold3' | |
null |
得到的效果是自动创建数据库,并初始化数据库执行 sql 语句建表插入数据
启动的日志:
2024-04-23 19:57:31.338 | INFO 34392 | main [TID: N/A] liquibase.database | Set default schema name to dbo
2024-04-23 19:57:31.501 | INFO 34392 | main [TID: N/A] liquibase.lockservice | Successfully acquired change log lock
2024-04-23 19:57:33.257 | INFO 34392 | main [TID: N/A] liquibase.changelog | Creating database history table with name: DATABASECHANGELOG
2024-04-23 19:57:33.261 | INFO 34392 | main [TID: N/A] liquibase.changelog | Reading from DATABASECHANGELOG
Running Changeset: db/sqlserver/slave/changelog-master.xml::1.0.1-20240318-001::Tz
2024-04-23 19:57:36.718 | INFO 34392 | main [TID: N/A] liquibase.changelog | SQL in file classpath:db/sqlserver/slave/scaffold_init.sql executed
2024-04-23 19:57:36.721 | INFO 34392 | main [TID: N/A] liquibase.changelog | ChangeSet db/sqlserver/slave/changelog-master.xml::1.0.1-20240318-001::Tz ran successfully in 3400ms
2024-04-23 19:57:36.729 | INFO 34392 | main [TID: N/A] liquibase.lockservice | Successfully released change log lock
2024-04-23 19:57:36.774 | INFO 34392 | main [TID: N/A] liquibase.lockservice | Successfully acquired change log lock
2024-04-23 19:57:37.565 | INFO 34392 | main [TID: N/A] liquibase.changelog | Creating database history table with name: scaffold4.DATABASECHANGELOG
2024-04-23 19:57:37.580 | INFO 34392 | main [TID: N/A] liquibase.changelog | Reading from scaffold4.DATABASECHANGELOG
Running Changeset: db/mysql/master/changelog-master.xml::1.0.0-20240318-001::Tz
2024-04-23 19:57:40.119 | INFO 34392 | main [TID: N/A] liquibase.changelog | SQL in file classpath:db/mysql/master/scaffold_init.sql executed
2024-04-23 19:57:40.119 | INFO 34392 | main [TID: N/A] liquibase.changelog | ChangeSet db/mysql/master/changelog-master.xml::1.0.0-20240318-001::Tz ran successfully in 2494ms
Running Changeset: db/mysql/master/changelog-master.xml::1.0.0-20240318-002::Tz
2024-04-23 19:57:40.291 | INFO 34392 | main [TID: N/A] liquibase.changelog | SQL in file classpath:db/mysql/master/pay_wallet.sql executed
2024-04-23 19:57:40.291 | INFO 34392 | main [TID: N/A] liquibase.changelog | ChangeSet db/mysql/master/changelog-master.xml::1.0.0-20240318-002::Tz ran successfully in 169ms
2024-04-23 19:57:40.295 | INFO 34392 | main [TID: N/A] liquibase.lockservice | Successfully released change log lock
2024-04-23 19:57:40.330 | INFO 34392 | main [TID: N/A] liquibase.lockservice | Successfully acquired change log lock
2024-04-23 19:57:40.394 | INFO 34392 | main [TID: N/A] liquibase.changelog | Reading from scaffold4.DATABASECHANGELOG
2024-04-23 19:57:40.423 | INFO 34392 | main [TID: N/A] liquibase.lockservice | Successfully released change log lock
2024-04-23 19:57:44.034 | INFO 34392 | main [TID: N/A] org.redisson.Version | Redisson 3.18.0
2024-04-23 19:57:47.644 | INFO 34392 | redisson-netty-2-5 [TID: N/A] o.r.c.pool.MasterPubSubConnectionPool | 1 connections initialized for 127.0.0.1/127.0.0.1:6379
2024-04-23 19:57:47.674 | INFO 34392 | redisson-netty-2-19 [TID: N/A] o.r.c.pool.MasterConnectionPool | 24 connections initialized for 127.0.0.1/127.0.0.1:6379
可以看到成功执行了初始化sql
# 查看结果
liquibase 更新数据库会有执行更新语句的记录
mysql | |
SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = 'scaffold4' | |
1 | |
select * from DATABASECHANGELOG | |
ID AUTHOR FILENAME DATEEXECUTED ORDEREXECUTED EXECTYPE MD5SUM DESCRIPTION COMMENTS TAG LIQUIBASE CONTEXTS LABELS DEPLOYMENT_ID | |
1.0.0-20240318-001 Tz db/mysql/master/changelog-master.xml 2024-03-18 19:46:15 1 EXECUTED 8:2bbd175ecbb0c52cddaa817632a1d55f sqlFile 4.9.1 0762373118 | |
1.0.0-20240318-002 Tz db/mysql/master/changelog-master.xml 2024-03-18 19:46:16 2 EXECUTED 8:a9390b23c590523c591203a644bf6ed0 sqlFile 4.9.1 0762373118 | |
select * from databasechangeloglock | |
ID LOCKED LOCKGRANTED LOCKEDBY | |
1 0 | |
sqlserver | |
SELECT 1 FROM sys.databases WHERE name = 'scaffold4' | |
1 | |
select * from scaffold4.dbo.DATABASECHANGELOG | |
ID AUTHOR FILENAME DATEEXECUTED ORDEREXECUTED EXECTYPE MD5SUM DESCRIPTION COMMENTS TAG LIQUIBASE CONTEXTS LABELS DEPLOYMENT_ID | |
1.0.1-20240318-001 Tz db/sqlserver/slave/changelog-master.xml 2024-04-23 19:57:36.723 1 EXECUTED 8:b7e4c6627ab3bbb057fc499e75ea8f00 sqlFile NULL 4.9.1 NULL NULL 3873453275 | |
select * from scaffold4.dbo.DATABASECHANGELOGLOCK | |
ID LOCKED LOCKGRANTED LOCKEDBY | |
1 0 NULL NULL |
通过上面的查询结果可以知道执行成功了
# 扩展
如果遇到要创建存储过程或函数的语句改如何呢,比如 mysql 创建存储过程,如果用普通的方式会报如下错误:
Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DELIMITER $$
CREATE PROCEDUREproc_demo
(IN pageNo INT, IN pageSize INT)
BEGI' at line 1表示不支持 DELIMITER;
如果去除 DELIMITER 也无法创建
解决办法:
使用 createProcedure 来表示指定的 sql 文件是存储过程 如下
<changeSet id="1.0.0-20240625-005" author="Tz">
<createProcedure path="classpath:db/mysql/master/jimureport_proc.sql"/>
</changeSet>
然后实际的内容就可以去除 DELIMITER
CREATE PROCEDURE `proc_demo`(IN pageNo INT,IN pageSize INT)
BEGIN
DECLARE begin_no INT;
SET begin_no = (pageNo-1)*pageSize;
-- 分页查询列表
SELECT * FROM rep_demo
WHERE id >= (
SELECT id FROM rep_demo
ORDER BY id ASC
LIMIT begin_no,1
)
ORDER BY id ASC
LIMIT pageSize;
END