1. 简介
最近我打算开发一个数据库适配器,核心目的是实现数据库表结构语句的动态执行,比如常见的创建/删除操作——涉及表、视图、索引、序列等各类数据库对象。当然,这个组件的理想状态是能自动适配主流数据库,用户无需纠结不同数据库的语法差异,只用专注于对应操作的数据结构定义就行。
之前写过一篇关于liquibase集成使用的文章,其在脚本支持和主流数据库适配(目前已支持超过60种数据库)方面有着不错的表现并且还能自动比较、追踪数据库的结构变化,用它来做核心组件,无疑能省下大量开发工作量,所以决定采用liquibase作为实现该功能的核心组件。
我们平时就常把liquibase用作项目的ddl版本管理工具,大多配置为项目启动阶段自动执行,把DDL相关信息持久化到数据库中。既然启动时能执行,那项目运行过程中想必也能通过编程方式触发执行——思路明确,话不多说,开整!
2. 环境信息
liquibase: 4.33.0
java: 8
3. Maven
<properties><logback.version>1.3.15</logback.version><lombok.version>1.18.38</lombok.version><junit.version>5.10.0</junit.version>
</properties><dependencies><dependency><groupId>org.liquibase</groupId><artifactId>liquibase-core</artifactId><version>4.33.0</version></dependency><dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId><version>3.4.5</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.4.0</version><scope>provided</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.38</version><scope>provided</scope></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>${logback.version}</version><optional>true</optional></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.9</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>${junit.version}</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>${junit.version}</version><scope>test</scope></dependency></dependencies>
4. Quick Start
liquibase数据结构分析
参考一下liquibase xml 描述信息如下
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-4.6.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd"><changeSet author="ludangxin" id="1662615627445-2"><createTable tableName="role" remarks="角色信息表"><column name="name" remarks="角色名称" type="VARCHAR(255)"/><column name="role_key" remarks="角色key" type="VARCHAR(255)"/></createTable><dropTable tableName="order"/></changeSet><changeSet author="ludangxin" id="1662615627445-3"><createTable tableName="user" remarks="用户信息表"><column name="id" type="INT" remarks="主键"><constraints nullable="false" primaryKey="true"/></column><column name="username" type="VARCHAR(255)" remarks="用户名称"/><column name="password" type="VARCHAR(255)" remarks="密码"/><column name="age" type="INT" remarks="性别"/><column name="sex" type="VARCHAR(255)" remarks="性别"/><column name="role" type="VARCHAR(255)" remarks="角色"/><column name="create_time" type="DATETIME" defaultValueComputed="NOW()" remarks="创建时间"/></createTable></changeSet>
</databaseChangeLog>
可以从上述的xml中看到 最外层到里依次是DatabaseChangeLog → ChangeSet → Change(createTable、dropTable....) , 且都是一对多的关系 基本的结构理清楚之后, 我们使用这些对象实现一个创建表的功能
@Test
public void test() throws SQLException, LiquibaseException {DatabaseChangeLog changeLog = new DatabaseChangeLog();changeLog.setLogicalFilePath("testPath");final CreateTableChange createTableChange = buildCreateTableChange();ChangeSet changeSet = new ChangeSet("123", "sysadmin", false, false, "system", null, null, changeLog);changeSet.addChange(createTableChange);changeLog.addChangeSet(changeSet);final Database database = buildDatabase("jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true", "root", "123456");Liquibase liquibase = new Liquibase(changeLog, new ClassLoaderResourceAccessor(), database);liquibase.update(new Contexts(), new LabelExpression());
}public static CreateTableChange buildCreateTableChange() {CreateTableChange createTableChange = new CreateTableChange();createTableChange.setSchemaName("test");createTableChange.setTableName("test_table");createTableChange.setRemarks("测试表");final ColumnConfig columnConfig = new ColumnConfig();columnConfig.setName("name");columnConfig.setType("varchar(255)");columnConfig.setRemarks("姓名");createTableChange.addColumn(columnConfig);return createTableChange;
}public static Database buildDatabase(String url, String username, String password) throws DatabaseException, SQLException {Connection connection = DriverManager.getConnection(url, username, password);return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
}
测试执行结果如下
不仅创建了目标表, 还生成了liquibase元信息存储和悲观锁表(防止并发执行导致表锁)

刚刚尝试创建了一张新表, 那除了创建表, 它还能干嘛呢? 看一下liquibase源码

可以看到, 对库/表的操作基本上都覆盖了, 那其实就好办了 对这些change对象做简单的封装, 方便客户端调用即可
5. 封装工具类
封装思路, 将这些change对应的功能封装一个builder类 用于方便build, 比如
这里只展示核心类, 其余代码请参考最后的源码
5.1 CreateTableChangeBuilder
创建表change builder
import com.ldx.meta.db.engine.entity.Column;
import com.ldx.meta.db.engine.entity.Table;
import liquibase.change.ColumnConfig;
import liquibase.change.ConstraintsConfig;
import liquibase.change.core.CreateTableChange;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.commons.collections4.CollectionUtils;import java.util.List;/*** 建表 change builder** @author ludangxin* @since 2025/11/6*/
@Data
@Accessors(fluent = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CreateTableChangeBuilder {private String schemaName;private Table table;private boolean ifNotExists = true;public static CreateTableChangeBuilder getInstance() {return new CreateTableChangeBuilder();}public CreateTableChange build() {final String tableName = table.getName();final String tableComment = table.getComment();CreateTableChange createTableChange = new CreateTableChange();createTableChange.setSchemaName(schemaName);createTableChange.setTableName(tableName);createTableChange.setIfNotExists(ifNotExists);createTableChange.setRemarks(tableComment);final List<Column> columns = table.getColumns();if (CollectionUtils.isNotEmpty(columns)) {for (Column column : columns) {final ColumnConfig columnConfig = buildColumnConfig(column);createTableChange.addColumn(columnConfig);}}return createTableChange;}private static ColumnConfig buildColumnConfig(Column column) {ColumnConfig columnConfig = new ColumnConfig();columnConfig.setName(column.getName());columnConfig.setType(column.getType());columnConfig.setRemarks(column.getComment());columnConfig.setDefaultValue(column.getDefaultValue());final ConstraintsConfig constraintsConfig = new ConstraintsConfig();constraintsConfig.setNullable((!column.isNotNull()));constraintsConfig.setPrimaryKey(column.isPrimaryKey());columnConfig.setConstraints(constraintsConfig);return columnConfig;}
}
5.2 ChangeSetBuilder
change 集合 builder: 用于快速收集change
import com.ldx.meta.db.engine.configuration.MetaDbProperties;
import liquibase.change.Change;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** change set** @author ludangxin* @since 2025/11/6*/
@Setter
@Accessors(fluent = true)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChangeSetBuilder {private String id;private String comments;private String author;private boolean alwaysRun = false;private boolean runOnChange = false;private String filePath = "none";private String contextFilter;private String dbmsList;private DatabaseChangeLog changeLog;private final List<Change> changes = new ArrayList<>();private MetaDbProperties properties = MetaDbProperties.DEFAULT();public static ChangeSetBuilder getInstance() {return new ChangeSetBuilder();}public ChangeSetBuilder change(Change... change) {changes.addAll(new ArrayList<>(Arrays.asList(change)));return this;}public ChangeSet build() {if (changeLog == null) {changeLog = ChangeLogBuilder.getInstance().build();}if (properties != null) {if (id == null || id.isEmpty()) {id = properties.getIdProvider().getNextId();}if (author == null || author.isEmpty()) {author = properties.getAuthor();}}changeLog.setLogicalFilePath(filePath);ChangeSet changeSet = new ChangeSet(id, author, alwaysRun, runOnChange, filePath, contextFilter, dbmsList, changeLog);changeSet.setComments(comments);changeSet.setLogicalFilePath(filePath);for (Change change : changes) {changeSet.addChange(change);}return changeSet;}
}
5.3 ChangeLogBuilder
changeLog: 用于快速收集changeSet
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** change log** @author ludangxin* @since 2025/11/6*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ChangeLogBuilder {public static ChangeLogBuilder getInstance() {return new ChangeLogBuilder();}private final List<ChangeSet> changeSets = new ArrayList<>();public ChangeLogBuilder changeSet(ChangeSet... changeSet) {changeSets.addAll(new ArrayList<>(Arrays.asList(changeSet)));return this;}public DatabaseChangeLog build() {DatabaseChangeLog changeLog = new DatabaseChangeLog();if (!changeSets.isEmpty()) {final ChangeSet anyChangeSet = changeSets.get(0);final String logicalFilePath = anyChangeSet.getLogicalFilePath();changeLog.setLogicalFilePath(logicalFilePath);}else {changeLog.setLogicalFilePath("default");}for (ChangeSet changeSet : changeSets) {changeLog.addChangeSet(changeSet);}return changeLog;}
}
5.4 MetaDbEngineService
适配器执行入口 支持快速execute & lambda调用
import com.ldx.meta.db.engine.configuration.MetaDbDataSource;
import com.ldx.meta.db.engine.configuration.MetaDbProperties;
import com.ldx.meta.db.engine.exception.MetaDbDataSourceException;
import com.ldx.meta.db.engine.exception.MetaDbEngineException;
import com.ldx.meta.db.engine.builder.ChangeLogBuilder;
import com.ldx.meta.db.engine.builder.ChangeSetBuilder;
import com.ldx.meta.db.engine.builder.SqlBuilder;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.change.Change;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.database.Database;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;/*** 适配器入口** @author ludangxin* @since 2025/11/6*/
@Slf4j
public class MetaDbEngineService {private final MetaDbDataSource dataSource;private final MetaDbProperties metaDbProperties;public MetaDbEngineService(MetaDbDataSource dataSource) {this.dataSource = Objects.requireNonNull(dataSource, "dataSource can not be null");this.metaDbProperties = MetaDbProperties.DEFAULT();}public MetaDbEngineService(MetaDbDataSource dataSource, MetaDbProperties metaDbProperties) {this.dataSource = Objects.requireNonNull(dataSource, "dataSource can not be null");this.metaDbProperties = Objects.requireNonNull(metaDbProperties, "metaDbProperties can not be null");}public void execute(Change... changes) {this.execute(false, changes);}public void execute(boolean printSql, Change... changes) {if (changes == null || changes.length == 0) {log.warn("execute called with no changes -> no-op");return;}final ChangeSet changeSet = ChangeSetBuilder.getInstance().properties(metaDbProperties).change(changes).build();this.execute(printSql, changeSet);}public void execute(ChangeSet... changeSets) {this.execute(false, changeSets);}public void execute(boolean printSql, ChangeSet... changeSets) {if (changeSets == null || changeSets.length == 0) {log.warn("execute called with no changeSets -> no-op");return;}final DatabaseChangeLog changeLog = ChangeLogBuilder.getInstance().changeSet(changeSets).build();this.execute(printSql, changeLog);}public void execute(DatabaseChangeLog changeLog) {this.execute(false, changeLog);}public void execute(boolean printSql, DatabaseChangeLog changeLog) {if (changeLog == null || CollectionUtils.isEmpty(changeLog.getChangeSets())) {log.warn("execute called with empty changeLog -> no-op");return;}this.executeChangeLog(changeLog, printSql);}public List<String> getSqlList(Change... changes) {if (changes == null || changes.length == 0) {return Collections.emptyList();}final ChangeSet changeSet = ChangeSetBuilder.getInstance().properties(metaDbProperties).change(changes).build();return this.getSqlList(changeSet);}public List<String> getSqlList(ChangeSet... changeSets) {if (changeSets == null || changeSets.length == 0) {return Collections.emptyList();}final DatabaseChangeLog changeLog = ChangeLogBuilder.getInstance().changeSet(changeSets).build();return this.getSqlList(changeLog);}public List<String> getSqlList(DatabaseChangeLog changeLog) {if (changeLog == null) {return Collections.emptyList();}return this.generateSqlList(changeLog);}public String getSql(Change... changes) {List<String> list = getSqlList(changes);return list.isEmpty() ? "" : String.join(";\n", list) + ";";}public String getSql(ChangeSet... changeSets) {List<String> list = getSqlList(changeSets);return list.isEmpty() ? "" : String.join(";\n", list) + ";";}public String getSql(DatabaseChangeLog changeLog) {List<String> list = getSqlList(changeLog);return list.isEmpty() ? "" : String.join(";\n", list) + ";";}private void executeChangeLog(DatabaseChangeLog changeLog, boolean printSql) {try (final Database database = dataSource.openDatabase()) {if (printSql) {this.printSql(database, changeLog);}String url = safeGetDbUrl(database);Liquibase liquibase = new Liquibase(changeLog, new ClassLoaderResourceAccessor(), database);liquibase.update(new Contexts(), new LabelExpression());int executed = changeLog.getChangeSets() == null ? 0 : changeLog.getChangeSets().size();log.info("liquibase execute success, URL: {}, changeSets: {}", url, executed);}catch (LiquibaseException e) {throw new MetaDbEngineException("liquibase execute failed: " + e.getMessage(), e, safeGenerateAllSql(changeLog));}catch (RuntimeException e) {throw new MetaDbEngineException("meta-db-engine execute changelog failed: " + e.getMessage(), e, safeGenerateAllSql(changeLog));}}private void printSql(DatabaseChangeLog changeLog) {try (final Database database = dataSource.openDatabase()) {printSql(database, changeLog);}catch (DatabaseException e) {throw new MetaDbDataSourceException("print sql failed", e);}}public void printSql(Database database, DatabaseChangeLog changeLog) {final String dbUrl = safeGetDbUrl(database);generateSqlList(changeLog).forEach(sql -> log.info("execute sql url:{} statement: {}", dbUrl, sql));}private List<String> generateSqlList(DatabaseChangeLog changeLog) {try (final Database database = dataSource.openDatabase()) {if (changeLog == null || CollectionUtils.isEmpty(changeLog.getChangeSets())) {return Collections.emptyList();}return changeLog.getChangeSets().stream().filter(Objects::nonNull).map(ChangeSet::getChanges).filter(CollectionUtils::isNotEmpty).flatMap(Collection::stream).map(change -> SqlBuilder.getInstance().database(database).change(change).build()).collect(Collectors.toList());}catch (Exception e) {throw new MetaDbEngineException("generate sql failed", e, null);}}private List<String> safeGenerateAllSql(DatabaseChangeLog changeLog) {try {return this.generateSqlList(changeLog);}catch (Exception e) {return Collections.emptyList();}}private String safeGetDbUrl(Database db) {try {return db != null && db.getConnection() != null ? db.getConnection().getURL() : "unknown";}catch (Exception e) {return "unknown";}}public TransientEngine lambda() {return new TransientEngine(this);}@Setter@Accessors(fluent = true)public static final class TransientEngine {private final MetaDbEngineService engine;private boolean printSql = false;private DatabaseChangeLog changeLog = ChangeLogBuilder.getInstance().build();private TransientEngine(MetaDbEngineService service) {this.engine = service;}public TransientEngine change(Change... changes) {final ChangeSet changeSet = ChangeSetBuilder.getInstance().properties(engine.metaDbProperties).change(changes).build();return this.changeSet(changeSet);}public TransientEngine changeSet(ChangeSet... changeSets) {for (ChangeSet changeSet : changeSets) {changeLog.addChangeSet(changeSet);}return this;}public TransientEngine changeLog(DatabaseChangeLog changeLog) {Objects.requireNonNull(changeLog, "changeLog can not be null");this.changeLog = changeLog;return this;}public void execute() {engine.execute(printSql, changeLog);}public void printSql() {engine.printSql(changeLog);}public List<String> getSqlList() {return engine.getSqlList(changeLog);}}
}
6. 测试工具类
import com.ldx.meta.db.engine.builder.ChangeSetBuilder;
import com.ldx.meta.db.engine.builder.CreateTableIndexChangeBuilder;
import com.ldx.meta.db.engine.builder.DropTableIndexChangeBuilder;
import com.ldx.meta.db.engine.builder.DropViewChangeBuilder;
import com.ldx.meta.db.engine.configuration.MetaDbDataSource;
import com.ldx.meta.db.engine.configuration.MetaDbProperties;
import com.ldx.meta.db.engine.entity.Column;
import com.ldx.meta.db.engine.entity.Table;
import com.ldx.meta.db.engine.builder.CreateTableChangeBuilder;
import com.ldx.meta.db.engine.builder.CreateViewChangeBuilder;
import com.ldx.meta.db.engine.builder.DropTableChangeBuilder;
import com.ldx.meta.db.engine.builder.SqlBuilder;
import com.ldx.meta.db.engine.entity.TableIndex;
import com.ldx.meta.db.engine.enums.IdProviderType;
import liquibase.change.core.CreateIndexChange;
import liquibase.change.core.CreateTableChange;
import liquibase.change.core.CreateViewChange;
import liquibase.change.core.DropTableChange;
import liquibase.change.core.DropViewChange;
import liquibase.change.core.RawSQLChange;
import liquibase.changelog.ChangeSet;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;import java.util.Arrays;
import java.util.Collections;/*** test** @author ludangxin* @since 2025/11/6*/
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class MetaDbEnginesTest {private static MetaDbEngineService META_DB_ENGINE_SERVICE;@BeforeAllpublic static void init() {final MetaDbDataSource metaDbDataSource = MetaDbDataSource.fromJdbc("jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true", "root", "123456");final MetaDbProperties metaDbProperties = new MetaDbProperties();metaDbProperties.setIdProvider(IdProviderType.TIMESTAMP);metaDbProperties.setAuthor("zhang-tie-niu");META_DB_ENGINE_SERVICE = new MetaDbEngineService(metaDbDataSource, metaDbProperties);}@Test@Order(1)public void given_table_info_when_create_table_then_complete() {final Column idColumn = Column.builder().name("id").type("varchar(32)").comment("主键").build();final Table table = Table.builder().name("test123").columns(Collections.singletonList(idColumn)).build();final CreateTableChange createTableChange = CreateTableChangeBuilder.getInstance().table(table).build();META_DB_ENGINE_SERVICE.execute(createTableChange);}@Test@Order(2)public void given_view_info_when_create_view_then_complete() {final CreateViewChange createView = CreateViewChangeBuilder.getInstance().viewName("v_test123").sql("select * from test123").replaceIfExists(true).build();META_DB_ENGINE_SERVICE.lambda().printSql(true).change(createView).execute();}@Test@Order(3)public void given_sql_info_when_update_table_then_complete() {final RawSQLChange build = SqlBuilder.build("insert into test123 (id) values (1)");META_DB_ENGINE_SERVICE.lambda().printSql(true).change(build).execute();}@Test@Order(4)public void given_view_name_when_drop_view_then_complete() {final DropViewChange dropView = DropViewChangeBuilder.getInstance().viewName("v_test123").build();META_DB_ENGINE_SERVICE.lambda().printSql(true).change(dropView).execute();}@Test@Order(5)public void given_table_name_when_drop_table_then_complete() {final DropTableChange dropTable = DropTableChangeBuilder.getInstance().tableName("test123").build();META_DB_ENGINE_SERVICE.lambda().printSql(true).change(dropTable).execute();}@Testpublic void given_ddl_info_when_execute_then_complete() {final Column idColumn = Column.builder().name("id").type("varchar(32)").primaryKey(true).comment("主键").build();final Column nameColumn = Column.builder().name("name").type("varchar(32)").comment("姓名").build();final Column idCardColumn = Column.builder().name("id_card").type("varchar(32)").comment("身份证").build();final Table table = Table.builder().name("test666").columns(Arrays.asList(idColumn, nameColumn, idCardColumn)).build();final CreateTableChange createTableChange = CreateTableChangeBuilder.getInstance().table(table).build();final TableIndex tableIdCardIndex = TableIndex.builder().indexName("id_card_unique_index").tableName("test666").columnNames(Collections.singletonList("id_card")).unique(true).build();final CreateIndexChange createIndexChange = CreateTableIndexChangeBuilder.getInstance().tableIndex(tableIdCardIndex).build();final ChangeSet changeSet = ChangeSetBuilder.getInstance().change(createTableChange).change(createIndexChange).build();META_DB_ENGINE_SERVICE.execute(changeSet);}}
测试执行结果如下

7. 小结
本章节尝试使用liquibase作为数据库适配器底座, 通过简单封装其内置的DatabaseChangeLog → ChangeSet → Change(createTable、dropTable....)等对象, 实现方便快速的执行ddl操作, 最主要的是其默认就支持多达60+数据库的自动适配, 可以轻松的实现数据库兼容.
8. 源码
测试过程中的代码已全部上传至github, 欢迎点赞收藏 仓库地址: https://github.com/ludangxin/meta-db-engine