liquibase 扩展适配达梦数据库(dm7)

背景

之前因项目客户要求,需要适配国产数据库-达梦(这里适配的是达梦大型通用数据库管理系统,简称DM7),对代码中的sql 改造,同时因为项目中的数据库的建表脚本、字典等都是使用 liquibase 管理的,所以也需要扩展 liquibase 。

在我们的使用中,达梦因为开启了 Oracle 的兼容模式,所以 sql 基本无需改造,但 liquibase 本身不支持达梦数据库,仍需要写代码适配。

注:如果需要搭建本地环境测试或者是体验,可以直接使用 docker 搭建,快速验证下效果,这里使用的是镜像是 flobit/dm7 ,启动命令为 docker run -d --name dm7 -p5236:5236 flobit/dm7,账号密码为 sysdba/strongP@ssW0rd

适配实现

感谢 liquibase 良好的扩展机制,我们一般只需要扩展两点即可:

  1. AbstractJdbcDatabase 声明适配的数据库的元数据、连接的创建等
  2. 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.generateSqlCreateDatabaseChangeLogTableGenerator.generateSql获悉)

和实现 AbstractJdbcDatabase 类似,也是参考 liquibase 中oracle 相关的实现。

以 boolean 为例子,实际需要修改的是注解和 supports、toDatabaseDataType 方法。

  1. @DataTypeInfo 的优先级属性 priority 设为5(必须大于1才能优先加载)
  2. supports 方法只校验通过达梦数据库。
  3. 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 的支持做修改即可。


版权声明:本文为Cceking原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。