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; @BeforeAll public 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(); } @Test public 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