背景
之前因项目客户要求,需要适配国产数据库-达梦(这里适配的是达梦大型通用数据库管理系统,简称DM7),对代码中的sql 改造,同时因为项目中的数据库的建表脚本、字典等都是使用 liquibase 管理的,所以也需要扩展 liquibase 。
在我们的使用中,达梦因为开启了 Oracle 的兼容模式,所以 sql 基本无需改造,但 liquibase 本身不支持达梦数据库,仍需要写代码适配。
注:如果需要搭建本地环境测试或者是体验,可以直接使用 docker 搭建,快速验证下效果,这里使用的是镜像是 flobit/dm7 ,启动命令为 docker run -d --name dm7 -p5236:5236 flobit/dm7,账号密码为 sysdba/strongP@ssW0rd 。
适配实现
感谢 liquibase 良好的扩展机制,我们一般只需要扩展两点即可:
- AbstractJdbcDatabase 声明适配的数据库的元数据、连接的创建等
- LiquibaseDataType 声明 java 类型 和数据库字段类型的转换
liquibase版本为 3.5.3.
达梦数据库的依赖如下
<dependency>
<groupId>com.dameng</groupId>
<artifactId>Dm7JdbcDriver18</artifactId>
<version>7.6.0.165</version>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmDialect-for-hibernate2.0</artifactId>
<version>8.1.1.49</version>
</dependency>
实现 AbstractJdbcDatabase
笔者一开始为了省事,直接继承 OracleDatabase 然后再复写其中方法,这样会导致的 liquibase 自身的表无法创建成功,也尝试继承 MySqlDatabase ,这样则是导致 liquibase 重复创建 DATABASECHANGELOG 从而失败。(这里挺有意思,后面再展开说说)
正确的做法,应该是直接复制 OracleDatabase 的代码,声明为新类,然后修改其中相关的方法。
实现如下
public class DaMengDatabase extends AbstractJdbcDatabase {
public DaMengDatabase() {
super.unquotedObjectsAreUppercased=true;
super.setCurrentDateTimeFunction("SYSTIMESTAMP");
// Setting list of Oracle's native functions
dateFunctions.add(new DatabaseFunction("SYSDATE"));
dateFunctions.add(new DatabaseFunction("SYSTIMESTAMP"));
dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
super.sequenceNextValueFunction = "%s.nextval";
super.sequenceCurrentValueFunction = "%s.currval";
}
@Override
public String getShortName() {
return "dm";
}
@Override
protected String getDefaultDatabaseProductName() {
return "dm";
}
@Override
public Integer getDefaultPort() {
return 5236;
}
@Override
public int getPriority() {
return 6;
}
@Override
public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
return "DM DBMS".equalsIgnoreCase(conn.getDatabaseProductName());
}
@Override
public String getDefaultDriver(String url) {
return "dm.jdbc.driver.DmDriver";
}
// 其他方法和 OracleDatabase 一样即可
}
其中 getPriority 是关键,liquibase 的组件都是基于优先级的大小加载的,这里必须设置大于5 才会优先加载,其他不变即可。
实现 LiquibaseDataType
这里需要满足liquibase 的启动,至少需要支持其数据库锁表 DATABASECHANGELOGLOCK 和日志表 DATABASECHANGELOG 的创建才行。按其中的表字段类型,此处需要实现 boolean 、datetime、int、varchar 的转换代码。
(此处查看 CreateDatabaseChangeLogLockTableGenerator.generateSql 和CreateDatabaseChangeLogTableGenerator.generateSql获悉)
和实现 AbstractJdbcDatabase 类似,也是参考 liquibase 中oracle 相关的实现。
以 boolean 为例子,实际需要修改的是注解和 supports、toDatabaseDataType 方法。
@DataTypeInfo的优先级属性 priority 设为5(必须大于1才能优先加载)supports方法只校验通过达梦数据库。toDatabaseDataType直接返回 Oracle 相关的类型。
具体实现如下
@DataTypeInfo(name = "boolean", aliases = {"java.sql.Types.BOOLEAN", "java.lang.Boolean", "bit", "bool"}, minParameters = 0, maxParameters = 0, priority = DaMengConstatns.PRIORITY_DEFAULT)
public class DmBooleanType extends LiquibaseDataType {
@Override
public boolean supports(Database database) {// 只适配达梦数据库dm7
return database instanceof DaMengDatabase;
}
@Override
public DatabaseDataType toDatabaseDataType(Database database) {
// 直接兼容 OracleDatabase
return new DatabaseDataType("NUMBER", 1);
}
@Override
public String objectToSql(Object value, Database database) {
if (value == null || value.toString().equalsIgnoreCase("null")) {
return null;
}
String returnValue;
if (value instanceof String) {
if (((String) value).equalsIgnoreCase("true") || value.equals("1") || ((String) value).equalsIgnoreCase("b'1'") || value.equals("t") || ((String) value).equalsIgnoreCase(this.getTrueBooleanValue(database))) {
returnValue = this.getTrueBooleanValue(database);
} else if (((String) value).equalsIgnoreCase("false") || value.equals("0") || ((String) value).equalsIgnoreCase("b'0'") || value.equals("f") || ((String) value).equalsIgnoreCase(this.getFalseBooleanValue(database))) {
returnValue = this.getFalseBooleanValue(database);
} else {
throw new UnexpectedLiquibaseException("Unknown boolean value: " + value);
}
} else if (value instanceof Long) {
if (Long.valueOf(1).equals(value)) {
returnValue = this.getTrueBooleanValue(database);
} else {
returnValue = this.getFalseBooleanValue(database);
}
} else if (value instanceof Number) {
if (value.equals(1) || value.toString().equals("1") || value.toString().equals("1.0")) {
returnValue = this.getTrueBooleanValue(database);
} else {
returnValue = this.getFalseBooleanValue(database);
}
} else if (value instanceof DatabaseFunction) {
return value.toString();
} else if (value instanceof Boolean) {
if (((Boolean) value)) {
returnValue = this.getTrueBooleanValue(database);
} else {
returnValue = this.getFalseBooleanValue(database);
}
} else {
throw new UnexpectedLiquibaseException("Cannot convert type "+value.getClass()+" to a boolean value");
}
return returnValue;
}
protected boolean isNumericBoolean(Database database) {
return true;
}
/**
* The database-specific value to use for "false" "boolean" columns.
*/
public String getFalseBooleanValue(Database database) {
return "0";
}
/**
* The database-specific value to use for "true" "boolean" columns.
*/
public String getTrueBooleanValue(Database database) {
return "1";
}
}
关于如何找 toDatabaseDataType 中 Oracle 对应的实现,我们直接所搜 liquibase 原有的实现类即可,这些都在liquibse-core 的liquibase.datatype.core 包下,如 boolean 的实现类 BooleanType
其他的 datetime、int 、varchar 同样的道理。(可以再加上常用的 decimal、clob)
@DataTypeInfo(name = "datetime", aliases = {"java.sql.Types.DATETIME", "java.util.Date", "smalldatetime", "datetime2"}, minParameters = 0, maxParameters = 1, priority = DaMengConstatns.PRIORITY_DEFAULT)
public class DmDateTimeType extends LiquibaseDataType {
@Override
public boolean supports(Database database) {// 只适配达梦数据库dm7
return database instanceof DaMengDatabase;
}
@Override
public DatabaseDataType toDatabaseDataType(Database database) {
// 直接兼容 OracleDatabase
return new DatabaseDataType("TIMESTAMP", getParameters());
}
// 其他方法和原来的一样即可
}
@DataTypeInfo(name = "int", aliases = { "integer", "java.sql.Types.INTEGER", "java.lang.Integer", "serial", "int4", "serial4" }, minParameters = 0, maxParameters = 1, priority = DaMengConstatns.PRIORITY_DEFAULT)
public class DmIntType extends LiquibaseDataType {
@Override
public boolean supports(Database database) {// 只适配达梦数据库dm7
return database instanceof DaMengDatabase;
}
@Override
public DatabaseDataType toDatabaseDataType(Database database) {
// 直接兼容 OracleDatabase
return new DatabaseDataType("INTEGER");
}
// 其他方法和原来的一样即可
}
总结
liquibase 的适配扩展,涉及到其数据库锁表 DATABASECHANGELOGLOCK 和日志表 DATABASECHANGELOG 的创建,以及数据库字段类型的转换,这里可以根据 liquibase 本身对 oracle 的支持做修改即可。