1. 项目背景与整体思路拆解
1.1 为什么需要给 Liquibase 做达梦扩展
先聊两句背景。Liquibase 是目前 Java 生态里用得最多的数据库版本管理工具之一,它把数据库结构变更做成 changelog,用 XML、YAML、JSON 或者 SQL 文件统一管理,配合团队协作时能清楚知道哪个环境跑到哪个版本了。平时我们接 PostgreSQL、MySQL、Oracle、SQL Server 都是开箱即用,社区插件也基本覆盖了主流数据库。但真到了国产化改造、信创项目落地的时候,问题就来了——达梦数据库(DM Database)基本不在 Liquibase 的开箱支持列表里,直接用默认驱动去连,往往在databaseChangeLogLock表读写时就翻车。
我最早遇到这个坑是在一个政务系统的国产化迁移项目里,应用层用的 Spring Boot 3 + MyBatis Plus,数据库从 Oracle 切成达梦 V8。代码改动其实不大,但版本管理工具跑不起来,dev、test、prod 三套环境全靠人工比对库表结构,痛苦程度不用我多说。后来下定决心自己写适配,才把这套流程彻底理顺。
简单说,这个项目的核心目标就是:让 Liquibase 能像支持 Oracle 一样支持达梦数据库,包括自动建 changelog 表、正确的数据类型映射、SQL 语法兼容、主键自增处理、以及事务和锁表的可靠性。听起来不复杂,但真正做起来细节非常多,尤其是达梦的 Oracle 兼容模式和原生模式之间的差异,稍不注意就埋雷。
1.2 方案选型:扩展 Database 实现还是改 JDBC 驱动
Liquibase 扩展一个数据库,标准做法是写一个Database接口的实现类,通常继承AbstractJdbcDatabase或者直接仿照现有数据库实现。比如官方支持 Oracle 就是OracleDatabase,支持 PostgreSQL 就是PostgresDatabase。每个实现类负责告诉 Liquibase 这个数据库的方言特征:默认 schema、标识符引用规则、数据类型映射、是否支持序列、是否支持自动增量等。
这里有个关键决策点:到底是实现一个新的 Database 类,还是只修改 JDBC URL 和驱动的加载方式?我的结论是必须老老实实写 Database 实现类。原因有三:
第一,达梦虽然提供了兼容 Oracle 的语法模式,但本质还是有自己的方言。如果不告诉 Liquibase 当前连接的是达梦,很多内部逻辑会走 Oracle 分支,但达梦的某些行为又和 Oracle 不一致,容易出现边界问题。
第二,Liquibase 在启动时需要通过 JDBC URL 或驱动元数据判断数据库类型。达梦的驱动类是dm.jdbc.driver.DmDriver,URL 是jdbc:dm://host:5236,这些信息默认情况下 Liquibase 根本不认识,会直接报 "Unsupported database"。
第三,后续要支持generateChangelog(从已有数据库反向生成变更日志),没有独立的 Database 类型,Liquibase 无法用正确的元数据查询逻辑去读取表结构。
所以整体方案就是写一个DmDatabase类,继承AbstractJdbcDatabase,注册进 Liquibase 的数据库类型映射表,同时配套编写 JDBC 驱动识别逻辑和数据类型转换器。
1.3 现有社区方案为什么不直接可用
做之前我也在 GitHub、Gitee 上搜过现成方案,确实有零星几个 liquibase-dameng 或 liquibase-dm 的仓库。但实测下来问题不少:
- 多数仓库只适配了 Liquibase 4.x 早期版本,对 Spring Boot 3 集成用的 Liquibase 4.20+ 支持不好。
- 数据类型映射很粗糙,比如达梦的
VARCHAR2、NUMBER和CLOB在反向生成时经常变成不兼容的类型。 - 对达梦的
IDENTITY自增列支持不完善,导致CREATE TABLE语句生成时语法错误。 - 锁表逻辑有明显问题,
DATABASECHANGELOGLOCK表在并发执行时偶尔死锁,这对多实例部署是致命的。
所以最后我决定自己动手,基于 Liquibase 4.24 版本做适配,并保持向前兼容。这个项目不只是写一个类那么简单,而是要把 Liquibase 和达梦之间的所有交互点全部捋一遍。
2. 核心适配内容与实现细节
2.1 识别达梦数据库类型
Liquibase 识别数据库类型的入口在DatabaseFactory,它根据 JDBC 连接的DatabaseMetaData信息(主要是getDatabaseProductName())来决定实例化哪个Database实现。
达梦驱动的getDatabaseProductName()返回的是DM DBMS,看起来简单,但实测在不同的达梦驱动版本下返回值并不完全一致,有的版本返回DM,有的返回DM DBMS,还有的返回Dameng。所以判断逻辑不能只做精确字符串匹配,得同时兼容多种返回值。
我这里还用了一个更稳妥的方法:不单纯依赖 product name,而是加上 JDBC URL 前缀判断。达梦的 URL 标准格式是jdbc:dm://host:port,在DmDatabase的构造函数里可以拿到连接信息,通过connection.getMetaData().getURL()再确认一次,双保险。这样即使未来达梦改了 product name 返回,只要 URL 格式不变,照样能正确识别。
注册 Database 类时,核心代码逻辑大致是这样:
public class DmDatabase extends AbstractJdbcDatabase { public DmDatabase() { super(); this.setDefaultDatabasePrefix("dm"); this.setDefaultSchemaName("SYSDBA"); } @Override protected String getDefaultDatabaseProductName() { return "DM DBMS"; } @Override public String getShortName() { return "dm"; } @Override public Integer getDefaultPort() { return 5236; } @Override public String getJdbcUrl(String host, int port, String database) { return "jdbc:dm://" + host + ":" + port; } @Override public boolean supportsInitiallyDeferrableColumns() { return false; } @Override public boolean supportsSequences() { return true; } @Override public boolean supportsAutoIncrement() { return true; } @Override public String getAutoIncrementClause() { return "IDENTITY(1, 1)"; } }supportsSequences()返回 true 是因为达梦确实支持序列对象,supportsAutoIncrement()返回 true 是因为达梦支持IDENTITY自增列,但注意自增语法不是 MySQL 的AUTO_INCREMENT,也不是 Oracle 传统的SEQUENCE + TRIGGER,而是IDENTITY(1,1)。这个差异必须在getAutoIncrementClause()里明确返回,否则生成的建表 SQL 直接语法报错。
2.2 DATABASECHANGELOG 锁表机制适配
这是 Liquibase 适配里最容易被忽视却又最致命的一个点。Liquibase 在执行变更集前,会先在DATABASECHANGELOGLOCK表上获取一把锁,确保同一时刻只有一个实例在修改数据库结构。这个锁的实现就是往表里插入一条ID=1的记录,通过数据库的行锁来保证互斥。
达梦默认的隔离级别和 Oracle 类似,读不阻塞写、写不阻塞读,但在并发插入同一主键时,不同兼容模式下的行为表现不一样。如果达梦跑在 Oracle 兼容模式下,Liquibase 走的是和 Oracle 类似的锁逻辑,一般没问题;但如果达梦用的是原生模式,某些版本下INSERT遇到主键冲突会直接抛异常而不是等待锁释放,导致多实例启动时报Duplicate key错误。
解决思路是:不让 Liquibase 自动建锁表,而是自己预创建 Lock 表并做初始化。更优雅的做法是重写getLiquibaseLockTable()相关的初始化逻辑,在checkDatabaseChangeLogLockTable阶段特殊处理。
实际项目中我采用了以下方式:在DmDatabase里覆写getConnection()返回一个自定义的DmConnection,专门拦截和锁表相关的 SQL。锁表的执行逻辑简化为两步:
- 先尝试
UPDATE DATABASECHANGELOGLOCK SET LOCKED = 1 WHERE ID = 1 AND LOCKED = 0。 - 如果更新影响行数为 0,说明锁被占用,此时再去查
LOCKED和LOCKGRANTED字段判断锁是否过期。
这种方式避开了达梦在并发INSERT时的不确定性,把锁获取做成幂等的更新操作,实测在 4 个实例同时启动的极端情况下也能稳定只有一个实例获得变更权限。
2.3 数据类型映射与反向生成
Liquibase 的数据类型映射由LiquibaseDataType体系管理,达梦扩展必须告诉 Liquibase:哪些类型是标准的,哪些类型需要转换成达梦方言。
达梦 V8 兼容 Oracle 数据类型,但也有一些自己的扩展类型。我做得最多的映射如下:
| Liquibase 标准类型 | 达梦实际生成类型 | 说明 |
|---|---|---|
| VARCHAR | VARCHAR2(长度) | 达梦默认 VARCHAR 语义和 Oracle 类似 |
| VARCHAR2 | VARCHAR2(长度) | 直接映射 |
| NUMBER | NUMBER(精度, 标度) | 支持 NUMBER 全参数 |
| INTEGER | INT | 简化为 INT |
| BIGINT | BIGINT | 原生支持 |
| CLOB | CLOB | 达梦 CLOB 最大支持 2G,够用 |
| BLOB | BLOB | 正常映射 |
| DATE | DATE | 达梦 DATE 包含时间部分,和 Oracle 一致 |
| TIMESTAMP | TIMESTAMP | 支持 |
| BOOLEAN | BIT | 达梦没有原生 BOOLEAN,建议用 BIT 或 TINYINT |
| DECIMAL | DECIMAL(精度, 标度) | 原生支持 |
最需要注意的是BOOLEAN。如果你在 changelog 里写了<column name="enabled" type="BOOLEAN"/>,Liquibase 默认会生成BOOLEAN类型,但达梦原生模式并不直接支持,建表会报错。一定要在类型转换器里把它映射成BIT或TINYINT。
我写了一个DmBooleanType继承BooleanType,覆写toDatabaseDataType方法:
public class DmBooleanType extends BooleanType { @Override public String toDatabaseDataType(Database database) { return "BIT"; } @Override public int getPriority() { return PRIORITY_DATABASE; } }这样配置后,changelog 里写BOOLEAN也完全没问题,生成 SQL 时会自动变成BIT。对于从旧库反向生成变更日志(generateChangelog)的场景,BIT字段也能正确转回BOOLEAN,双向互通。
3. 实操过程与完整集成步骤
3.1 环境准备:Liquibase 版本选择与依赖引入
我用的是 Liquibase 4.24.0 + 达梦 V8 客户端驱动。达梦的 JDBC 驱动可以从达梦官网下载,或者从已安装的数据库目录里找到,通常叫DmJdbcDriver18.jar,支持 JDK 8 及以上。
Maven 项目引入依赖时,由于达梦驱动不在中央仓库,需要先手动安装到本地仓库:
mvn install:install-file \ -Dfile=DmJdbcDriver18.jar \ -DgroupId=com.dameng \ -DartifactId=DmJdbcDriver \ -Dversion=8.1.3.62 \ -Dpackaging=jar然后正常声明依赖:
<dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>4.24.0</version> </dependency> <dependency> <groupId>com.dameng</groupId> <artifactId>DmJdbcDriver</artifactId> <version>8.1.3.62</version> </dependency>如果你的项目用的是 Gradle,方式类似,重点是把驱动 jar 推进本地或私服的 Maven 仓库。这一步搞不定,后面全是空谈。
3.2 Spring Boot 集成配置示例
Spring Boot 集成 Liquibase 特别简单,严格来说只要配置了spring.liquibase.*相关属性,启动时会自动执行。配合达梦,在application.yml里做如下配置:
spring: datasource: url: jdbc:dm://192.168.10.10:5236?schema=SYSDBA username: SYSDBA password: your_password driver-class-name: dm.jdbc.driver.DmDriver liquibase: enabled: true change-log: classpath:db/changelog/db.changelog-master.xml default-schema: SYSDBA liquibase-schema: SYSDBA drop-first: false注意到url里的?schema=SYSDBA是达梦特有写法,用于指定默认 schema,这个参数在 Liquibase 解析元数据时非常关键,否则可能找不到表。liquibase-schema也建议指定成SYSDBA,因为默认的PUBLICschema 在权限管理下不一定有建表权限。
启动时如果看到日志输出Running Changeset: classpath:db/changelog/xxx.xml,并且数据库里出现了DATABASECHANGELOG和DATABASECHANGELOGLOCK两张表,说明整体链路已经通了。
3.3 从零搭建扩展项目的文件结构
如果你要维护一个独立的扩展模块,我推荐以下目录结构:
liquibase-dm-extension/ ├── pom.xml └── src/main/java/com/example/dm/ ├── DmDatabase.java └── type/ ├── DmBooleanType.java └── DmClobType.java核心的pom.xml里要配置liquibase-core为provided作用域,避免打入最终包时冲突:
<dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> <version>4.24.0</version> <scope>provided</scope> </dependency>这样这个扩展模块既能独立测试,又能作为其他项目的依赖正常使用。实际接入时,在业务项目里引入这个扩展模块的坐标即可。
3.4 自动注册扩展的两种方式
Liquibase 发现自定义 Database 类有两种方式:
方式一:在META-INF/services/liquibase.database.Database文件里声明实现类。
com.example.dm.DmDatabase这是标准 Java SPI 机制,Liquibase 在启动时会自动扫描这个文件。推荐这种方式,最省事。
方式二:在代码里手动注册。
DatabaseFactory.getInstance().register(new DmDatabase());这种方式适合你没法改 SPI 文件,或者要动态决定是否启用达梦扩展的场景。但要注意注册时机,必须在 Liquibase 初始化前执行,通常在 Spring 的ApplicationRunner里做就已经晚了,最好的位置是DataSource初始化之后立刻注册。
我实际项目里两个都用了,SPI 作为默认加载方式,代码注册作为保险。因为某些部署环境会把 jar 包重新打包,SPI 文件偶尔会丢。
3.5 参数调优:DmDatabase 里必须覆写的方法清单
我把DmDatabase里必须覆写的方法列成一个清单,方便照抄:
| 方法 | 覆写返回值/逻辑 | 原因 |
|---|---|---|
getDefaultDatabaseProductName() | 返回DM DBMS | 确保类型识别 |
getShortName() | 返回dm | 日志和内部标识 |
getDefaultPort() | 返回5236 | 达梦默认端口 |
getDefaultSchemaName() | 返回SYSDBA | 默认 schema |
supportsSequences() | 返回true | 达梦支持序列 |
supportsAutoIncrement() | 返回true | 达梦支持 IDENTITY |
getAutoIncrementClause() | 返回IDENTITY(1, 1) | 自增语法 |
getDateLiteral() | 按达梦格式化日期 | 避免TO_DATE兼容问题 |
getCurrentDateTimeFunction() | 返回SYSTIMESTAMP | 当前时间函数 |
supportsTablespaces() | 返回true | 达梦支持表空间 |
其中getDateLiteral()特别关键。Liquibase 里如果你写<column name="create_time" defaultValueComputed="CURRENT_TIMESTAMP"/>,生成的 SQL 会变成CURRENT_TIMESTAMP,达梦虽然兼容这个写法,但某些驱动版本下返回的是一个字符串,可能导致后续比较出错。稳妥起见,我建议统一覆写为SYSTIMESTAMP,语义更明确。
4. 常见问题与排查技巧实录
4.1 启动时报 Unsupported database: DM DBMS
这个错误是没识别出达梦类型导致的。出现概率最高的原因是 SPI 文件没生效,或者getDefaultDatabaseProductName()返回值和实际连接不匹配。
排查步骤:
- 先确认驱动能正常返回 product name,写个 JDBC 测试代码打印
connection.getMetaData().getDatabaseProductName(),看看实际返回值。 - 确认
META-INF/services/liquibase.database.Database文件确实被打进 jar 包,解压后检查。 - 如果你的项目用的不是标准
@SpringBootApplication启动,而是自定义了 Liquibase 的 Spring 配置,手动注册DmDatabase也不失为一种快速验证办法。
还有一个坑:达梦有些驱动版本返回的 product name 是DM,不是DM DBMS。所以我在isCorrectDatabaseImplementation里做了模糊匹配:
@Override public boolean isCorrectDatabaseImplementation(DatabaseMetaData databaseMetaData) throws DatabaseException { String productName = databaseMetaData.getDatabaseProductName(); return productName != null && (productName.toUpperCase().contains("DM")); }这样不管返回的是DM还是DM DBMS,都能正确命中。
4.2 DATABASECHANGELOGLOCK 表无法自动创建或锁死
锁表创建失败的常见原因是 schema 权限不足。达梦里如果 Liquibase 配置的liquibase-schema用户没有 CREATE TABLE 权限,启动时就报权限错误。
处理方案:提前在达梦 SQL 客户端里手动执行初始化脚本,直接建好两张表:
CREATE TABLE SYSDBA.DATABASECHANGELOG ( ID VARCHAR(255) NOT NULL, AUTHOR VARCHAR(255) NOT NULL, FILENAME VARCHAR(255) NOT NULL, DATEEXECUTED TIMESTAMP NOT NULL, ORDEREXECUTED INT NOT NULL, EXECTYPE VARCHAR(10) NOT NULL, MD5SUM VARCHAR(35), DESCRIPTION VARCHAR(255), COMMENTS VARCHAR(255), TAG VARCHAR(255), LIQUIBASE VARCHAR(20), CONTEXTS VARCHAR(255), LABELS VARCHAR(255), DEPLOYMENT_ID VARCHAR(10) ); CREATE TABLE SYSDBA.DATABASECHANGELOGLOCK ( ID INT NOT NULL, LOCKED BOOLEAN NOT NULL, LOCKGRANTED TIMESTAMP, LOCKEDBY VARCHAR(255), CONSTRAINT PK_DATABASECHANGELOGLOCK PRIMARY KEY (ID) );注意这里的LOCKED字段类型,建议直接建BOOLEAN,因为达梦在兼容模式下能自动映射,之后 Liquibase 的读写逻辑也稳定。
如果已经出现锁死,比如LOCKED = TRUE但实际没有实例在跑,处理方式很简单:
UPDATE SYSDBA.DATABASECHANGELOGLOCK SET LOCKED = FALSE WHERE ID = 1;这个操作在测试环境很常用,生产环境要谨慎,先确认没有其他实例正在执行变更。
4.3 达梦生僻数据类型导致生成 SQL 报错
反向生成(generateChangelog)时,达梦有些类型如IMAGE、TEXT、NTEXT、UNIQUEIDENTIFIER,Liquibase 不认识,会直接报 unsupported type 或者生成成错误类型。
我的经验是绕开这些非标准类型,在达梦里建表时尽量用 Oracle 兼容类型。如果是从别的地方迁移来的库,反向生成前先用 SQL 查询一下字段类型分布:
SELECT DATA_TYPE, COUNT(*) FROM ALL_TAB_COLUMNS WHERE OWNER = 'SYSDBA' GROUP BY DATA_TYPE;看到异常类型,在扩展模块里补一个DmCustomType:
public class DmCustomType extends LiquibaseDataType { public DmCustomType() { super("IMAGE", 0, 0); } @Override public String toDatabaseDataType(Database database) { return "BLOB"; } }这样遇到IMAGE字段时会自动转成BLOB,不会报错。
4.4 Navicat 或 DBeaver 连接达梦正常,但 Liquibase 连不上
这个问题的根源通常不是 Liquibase,而是 JDBC URL 里的参数问题。达梦对 URL 参数比较敏感,schema和currentSchema写法在不同驱动版本不一样。
我踩过最典型的坑是:URL 里写jdbc:dm://ip:5236?currentSchema=SYSDBA,在达梦旧版本驱动下直接启动失败,新驱动却能识别。为了兼容,最好的办法是不要依赖 URL 参数指定 schema,而是用 Liquibase 的配置项:
spring: liquibase: default-schema: SYSDBA liquibase-schema: SYSDBA实测这样配置后,不需要动 URL,各种驱动版本都能跑通。
另外,如果你用 DBeaver 或 Navicat 工具连接达梦没问题,说明网络和用户没问题,那么问题一定出在驱动或 URL 上。先去确认驱动版本,用和图形工具一致的驱动包,能减少大量排查时间。
4.5 多实例部署时变更重复执行
默认情况下,Liquibase 通过DATABASECHANGELOG表判断变更集是否已执行,记录粒度是ID + AUTHOR + FILENAME三元组。如果两个实例用的 changelog 文件路径不一致(比如一个叫db/changelog/db.changelog-master.xml,另一个叫classpath:db/changelog/db.changelog-master.xml),会被认为是两条不同记录,导致重复执行。
解决办法是统一 changelog 文件路径,并在配置里加上:
spring: liquibase: change-log: classpath:db/changelog/db.changelog-master.xml确保所有实例配置一致。另外,如果修改了已有变更集的内容但没改 ID,也会导致 MD5SUM 校验错误,启动报Validation Failed。正确的做法是永远不要修改已经执行过的 changeset,新增变更集才改。
4.6 达梦数据库到期或连接数不足报错
排查环境时发现达梦有评估版到期或 license 限制连接数的问题。如果测试库里连接数打满,Liquibase 启动会报连接池无法获取连接的异常。这种问题不是 Liquibase 本身故障,而是数据库资源管理层面的。
检查办法:查询V$SESSIONS或达梦管理工具里的会话数,看是否超过 license 限制。若是连接泄漏,重点排查应用里是否每次操作完都释放了连接。Liquibase 本身只用一条连接做变更,不会造成连接数暴涨,问题多半出在业务代码。
4.7 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 启动报 Unsupported database | SPI 未生效或识别失败 | 检查 SPI 文件、手动注册 DmDatabase |
| 锁表并发异常 | 默认锁实现不兼容 | 覆写锁逻辑为 UPDATE 方式 |
| BOOLEAN 生成报错 | 达梦不支持 BOOLEAN | 配置类型转换为 BIT |
| 日期字段比较异常 | CURRENT_TIMESTAMP 语义差异 | 覆写为 SYSTIMESTAMP |
| 反向生成缺表 | schema 配置错误 | 设置 default-schema 和 liquibase-schema 一致 |
| 变更重复执行 | changelog 路径不一致 | 统一路径和配置 |
| 连接不上达梦 | URL 参数或驱动版本问题 | 确认驱动版本,避免 URL 带 schema 参数 |
| 达梦评估版到期 | license 过期 | 联系 DBA 更新授权 |
5. 反向生成 Changlog 与日常使用技巧
5.1 如何从已有达梦库生成变更日志
我的扩展模块还额外实现了LiquibaseSnapshotGenerator和LiquibaseTableSnapshotGenerator,支持从已有库生成 changelog。这对存量系统迁移特别有用,不用手写最初的基础表结构。
在 Spring Boot 工程里配置一个临时 profile,间断执行:
spring: liquibase: enabled: false然后通过命令行工具反向生成:
liquibase \ --driver=dm.jdbc.driver.DmDriver \ --url="jdbc:dm://192.168.10.10:5236" \ --username=SYSDBA \ --password=your_password \ --default-schema-name=SYSDBA \ --changeLogFile=output.xml \ generateChangeLog如果DmDatabase实现正确,这条命令能直接输出一个完整的 XML 变更文件。首次跑的时候大概率会有个别字段类型映射不对,这是正常的,手改一版就能稳定。
5.2 写 changelog 时达梦专属注意事项
日常使用中,我用得最多的几条经验:
一是尽量用<sql>标签写复杂变更,而不是全靠 Change Type。比如要建存储过程、函数、视图,直接写原生 SQL 最稳,Liquibase 的 XML 化 Change Type 对复杂数据库对象支持有限。
<changeSet id="create-function" author="tester" dbms="dm"> <sql> CREATE OR REPLACE FUNCTION GENERATE_PINYIN_CODE(...) RETURN VARCHAR2 IS ... </sql> </changeSet>dbms="dm"表示这段变更只在达梦上执行,多数据库部署时其他库会跳过。
二是达梦支持CREATE TABLE IF NOT EXISTS吗?实测 V8 在兼容模式下不支持,必须靠 Liquibase 的DATABASECHANGELOG来控制幂等,不要在 changelog 里写IF NOT EXISTS这种不跨库的 SQL。
三是DROP TABLE时有外键约束容易报错,提前DROP依赖表或先禁用约束。
5.3 多环境自动切换:dev 用 H2、prod 用达梦
一套 changelog 跑多个数据库是 Liquibase 最拿手的。实践里我的做法是:让开发环境用 H2 的 MySQL 兼容模式,测试和生产用达梦。这样开发时本地秒级启动,一键跑所有迁移,CI 汇总跑一次达梦,避免到了生产才发现 SQL 不兼容。
关键在于 changelog 里不要写死数据库方言,复杂 SQL 用<sql dbms="dm">和<sql dbms="h2">各写一份。虽然增加了维护成本,但能保证两边都能跑通。我自己做过的项目里,最长的一个 changelog 文件包含 200 多个 changeset,混合跑了 H2、MySQL、达梦三种环境,只要每条变更集都标注了dbms或经过充分测试,基本不会出幺蛾子。
5.4 编码和字符集:最容易忽略的坑
达梦数据库安装时如果不指定字符集,默认可能是 GBK,也可能是 UTF-8,取决于初始化参数。如果达梦用 GBK,而应用传的是 UTF-8 字符串,Liquibase 写入DATABASECHANGELOG表后,MD5SUM 计算不会出错,但 changelog 里的中文注解和描述可能乱码。
建库时强烈建议直接指定 UTF-8:
CREATE DATABASE DMDB CHARACTER SET UTF_8;如果库已经建了,改字符集比较麻烦,那就保证 JDBC URL 里设置编码参数,达梦驱动支持?compatibleMode=oracle&characterEncoding=UTF-8,实测对中文场景很有效。还有个小细节,达梦管理工具导出 dmp 文件时字符集也要一致,否则导入导出后中文注释变乱码,排查起来特别费劲。
我自己在处理一个老系统迁移时遇过这样的场景:服务器启动日志全部正常,Liquibase 变更也都执行了,但表里的中文数据全变成问号。最后发现就是数据库字符集和 JDBC 连接字符集不一致导致的,统一成 UTF-8 后问题消失。
6. 个人实操心得与后续扩展思路
6.1 踩过几次坑之后的核心体会
给 Liquibase 写达梦扩展,技术上其实不算难,难的是细节足够多。总结下来,我认为最有价值的几点经验是:
第一,所有类型适配都要回归到"双向"验证。既要从 changelog 变成达梦 SQL,也要从达梦表结构反向生成 changelog。很多开源方案只做了单向,所以一用就翻车。
第二,锁表逻辑必须优先搞定。一个数据库版本管理工具,如果锁表不稳定,在并发环境下立即暴露问题。尤其是 Spring Cloud 微服务多实例部署,每个实例启动都会触发 Liquibase 初始化,这时候锁表逻辑就是生死线。
第三,达梦的兼容模式不是万能钥匙。很多人以为开了 Oracle 兼容模式就能当 Oracle 用,实际达梦在某些细节上和 Oracle 仍有差异,比如USER_TABLES视图的字段、SYSDBA的权限边界、VARCHAR2的最大长度等等,不验证就大胆用,早晚出问题。
第四,反向生成的 changelog 只是起点,不是终点。自动生成的结构没问题,但字段注释、索引命名、约束命名经常不符合规范,我会再加一层手工整理流程,把 changelog 整理成"人话"。
6.2 后人接手时的建议
如果你是在别人已经写好的扩展示例上继续改,建议先跑通最小验证用例,不要直接在生产环境试。最小验证用例就是连一个测试库,跑三个最基本的变更:建表、加字段、加索引,看能否成功生成并执行。
通了以后再做反向生成测试,检查生成结果里的数据类型是否符合预期。这两关过了,再把日常开发里的各种复杂对象(函数、视图、存储过程)逐步加进去验证。每一步都记录下来,形成自己的兼容矩阵,后续遇到问题能快速定位。
6.3 扩展后续可以往哪些方向走
这个项目的扩展空间还挺大的。比如:
- 支持达梦的
DISK表空间和HUGE表,让 Liquibase 能管理达梦特色存储结构。 - 适配达梦的
MPP集群部署模式,在集群环境下验证锁表逻辑和分布式事务行为。 - 增加达梦 binlog 日志相关的解析和回放能力,不过这已经是数据同步工具的范畴了,和 Liquibase 关系不大。
- 接一套达梦在线表结构变更(online DDL)能力,达梦现在 DDL 也支持在线执行,但 Liquibase 默认没利用这个特性,某些大表变更会导致锁表时间过长。
就我个人的日常经验来说,当前这套扩展已经能覆盖绝大多数业务系统的变更管理需求。数据库版本管理这个事,关键不是工具多花哨,而是让团队形成纪律:所有结构变更必须走 changelog、必须走评审、必须能回滚。Liquibase 接到达梦之后,这套纪律就能在国产数据库上继续运转,这比任何单个技术点都重要。
最后再分享一个小技巧:配置里记得把liquibase.enabled的开关留给运维,不同环境可能要临时关闭 Liquibase 启动,尤其是当达梦在做主备切换或者备份恢复的时候,避免一个启动动作把生产库结构改了。用配置中心动态控制这个开关,比硬编码在代码里稳妥得多。