一、背景介绍
公司主要做toG 的项目,经常服务于各个地方政府。之前某省会城市下属区县项目,需要将已交付的项目中所使用到的各类中间件全部替换为国产,其中就包括了数据库的替换。项目中一直使用的是mysql数据库,后经商务调研及选型,决定替换为国产数据库 kingBase (人大金仓数据库),并由我负责所有应用的代码迁移工作。
我们的项目采用的是springCloud微服务体系,数据访问层用的是mybatisPlus, 微服务的个数比较多,我需要一个一个迁移并验证,这其实确实是一个比较繁琐且漫长的工作,并且由于只搭建了一套环境的数据库,我没法本地连接调试,每次都只能发包后通过观察错误日志的方式进行排查,给我的迁移工作增加了更大的困难。
二、适配过程
这里先强调一下,我的主要工作是把当前使用的mysql数据库,切换到人大金仓数据库上,这是一个迁移的过程,而不是说一个新项目从头开始就使用人大金仓,所以我所关注的主要就是mysql和人大金仓的兼容性上,把一些人大金仓无法成功的执行的mysql 中的语句改成能让他执行的语句。
而适配的第一步,其实主要是人大金仓的技术人员和我方的运维人员的工作。 首先由人大金仓的技术人员在我方指定的服务器上,帮助我们安装人大金仓的数据库,并且提供人大金仓(下面都使用kingbase替代)的数据库连接工具和相关的技术文档。并由我方运维人员根据他们提供的数据库迁移手册,将mysql上的数据迁移到kingbase中。完成迁移后,会给我一个kingbase数据库的连接相关信息(url,username,password等)和一份迁移文档。
接下来我就要根据文档进行应用上数据库迁移了,这里所说的迁移,其实就是做一个替换,将代码里的mysql连接替换成kingbase连接。替换步骤如下:
- 更换驱动 对方技术人员提供了一个驱动jar包,kingbase8-x.x.x.jar ,首先第一步我先将这个jar包上传到了公司私服上,然后用坐标的方式将其配置到了项目的pom文件中,同时注释掉里面的mysql驱动。
- 修改数据库连接配置信息 由于我们使用的是nacos配置中心,直接在nacos里进行修改:
spring:
datasource:
driver-class-name: com.kingbase8.Driver # 配置MySQL8的驱动程序类
url: jdbc:kingbase8:/host:port/database?para1=val1...
username: root # 数据库用户名
password: root # 数据库连接密码
3. 重新启动服务,测试,解决报错。
上面这几步就是迁移的主要步骤了,如果运气比较好,完成上面的改动后,服务应该会正常启动,但是也有可能会出现一些问题,接下来就是要具体问题具体分析了,报的错误基本上都是sql不兼容的一些问题。然后就要根据具体的报错情况进行sql的适配。同时人大金仓的技术人员也会组建一个答疑的群,遇到一些报错可以发到答疑群中,他们也会有专业的技术人员进行解答。
三、常见问题解决
问题一: Error updateting database. Cause: com.kingbase8.util.KSQLException: ERROR: syntax error at or near "`"
这个问题是由于kingbase数据库不支持反引号造成的,有一些框架在生成一些增删改查语句的时候,可能习惯将字段名称使用反引号包裹起来,我们需要将反引号去掉或者改为双引号引号重新启动项目后解决。
类似于:
问题二: DATE_ADD函数不支持
sql中有使用DATE_ADD函数的地方报错,原因是默认的kingbase中是没有DATE_ADD函数的,需要自己创建(非系统函数,每个库都需要创建): 创建代码如下:
代码语言:javascript复制create or replace function date_add(v_date text , v_interval interval)
returns text as $$
declare
v_rt text;
begin
select to_char(v_date::timestamp(0) v_interval,'yyyy-mm-dd hh24:mi:ss' ) into v_rt;
if length(v_date) = 10 and v_rt like '% 00:00:00' then
select substr(v_rt,0,10) into v_rt;
end if;
return v_rt;
end;
$$
LANGUAGE plpgsql;
问题三: DATE_FORMAT 函数不支持
解决方案: 需要自己创建(非系统函数,每个库都需要创建),创建语句如下:
代码语言:javascript复制create or replace function date_format(para1 timestamp,para2 text) returns text
as $$
declare
form1 text;
begin
--form1=replace(para2,'%M','Month');
form1=replace(para2,'%W','Day');
--form1=replace(form1,'%D','DDth');
form1=replace(form1,'%Y','YYYY');
--form1=replace(form1,'%y','yy');
form1=replace(form1,'%a','Dy');
form1=replace(form1,'%d','DD');
form1=replace(form1,'%e','DD');
form1=replace(form1,'%m','MM');
form1=replace(form1,'%c','MM');
form1=replace(form1,'%b','Mon');
form1=replace(form1,'%j','DDD');
form1=replace(form1,'%H','HH24');
form1=replace(form1,'%k','HH24');
--form1=replace(form1,'%h','HH');
--form1=replace(form1,'%I','HH');
form1=replace(form1,'%l','HH');
form1=replace(form1,'%i','MI');
form1=replace(form1,'%r','HH:MI:SS');
form1=replace(form1,'%T','HH24:MI:SS');
--form1=replace(form1,'%S','SS');
form1=replace(form1,'%s','SS');
form1=replace(form1,'%%','%');
return to_char(para1,form1);
end;
$$ LANGUAGE plpgsql;
问题四: Cause: com.github.pagehelper.PageException: 无法自动获取数据库类型,请通过 helperDialect 参数指定!
解决方案: 在配置文件中手动指定:
代码语言:javascript复制pagehelper:
helperDialect: postgresql
auto-runtime-dialect: false #默认参数,由于我的项目本地写死了true,我这里需要强制覆盖一下,如果配置成true,上面的配置不生效
问题五: ORDER BY isnull(kpi_seq), kpi_seq ASC ,create_time desc 报错
解决方案: kingbase不支持 isnull(字段名)的写法,需要改为一下写法:
代码语言:javascript复制ORDER BY kpi_seq isnull, kpi_seq ASC ,create_time desc
问题六: ERROR: CONNECT BY clause required in this query block
解决方案: 这个问题,主要是我在sql中使用了类似于name, level这样的关键字,kingbase中对于关键字的要求还是比较多的,有一些关键字是不允许我们在sql中使用的。解决方案有两种,一是把这些关键字的字段名修改一下。二是设置关键字的忽略。具体关键字有哪些,可以参考kingbase提供的文档,里面有详细参数。关键字忽略的方式,执行如下sql:
代码语言:javascript复制alter system set exclude_reserved_words = 'level';
select sys_reload_conf();
问题七: druid连接池在连接kingbase时会产生dbType not support, 或者 无法识别数据库类型的错误。
解决方案: 由于druild中一般会自动根据jdbc-url自动检查数据库的类型,但是它里面只有检查url中是否包含kingbase, 但是却不认识kingbase8,就会出现这个问题,我们需要自己设置driverClassName,同时druild的filter中要把wall去掉,因为kingbase不支持这个参数。
代码语言:javascript复制druidDataSource.setFilters("stat,slf4j");
druidDataSource.setDriverClassName("com.kingbase8.Driver")
问题八: com.kingbase8.util.KSQLException: Cannot convert the column of type TINYINT to requested type boolean.
解决方案: 这个问题主要是由于mysql中没有boolean类型,所以一般都使用tinyint来存储标志位,然后在实体中使用boolean类型来接收,类似mybatis或者mybatisPlus框架都会自动把tinyint类型转换为boolean类型。但是在kingbase里就不行了,解决方案: 这里只针对mybatisPlus,
首先实体类上的TableName注解中加入一个 autoResultMap = true
代码语言:javascript复制@TableName(value = “d_lc_res”, autoResultMap = true)
然后在boolean类型的字段注解上加上一个TypeHandler
代码语言:javascript复制@TableField(typeHandler = TinyIntToBooleanHandler.class, jdbcType= JdbcType.TINYINT)
private Boolean warningFlag;
TinyIntToBooleanHandler:
代码语言:javascript复制package com.xxx.global.config.mp;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @className: TinyIntToBooleanHandler
* @description:
* @author liushuai
* @date 2022/9/19 3:21 PM
*/
@MappedTypes({Boolean.class})
@MappedJdbcTypes({JdbcType.TINYINT})
public class TinyIntToBooleanHandler extends BaseTypeHandler<Boolean> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i,Boolean.TRUE.equals(parameter) ? 1 : 0 );
}
@Override
public Boolean getNullableResult(ResultSet rs, String columnName) throws SQLException {
int anInt = rs.getInt(columnName);
return Integer.valueOf("1").equals(anInt);
}
@Override
public Boolean getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int anInt = rs.getInt(columnIndex);
return Integer.valueOf("1").equals(anInt);
}
@Override
public Boolean getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int anInt = cs.getInt(columnIndex);
return Integer.valueOf("1").equals(anInt);
}
}
这样就可以了,这里要注意,这种方式只对mybatis已经封装好的方法有效,比如list, getOne,等,如果是我们自己写的sql, 还需要做进一步的处理:
对于自己写的sql, 我们需要使用resultMap来接收, 同时在定义resultMap的时候,给相应字段添加typeHandler属性:
代码语言:javascript复制<resultMap id="kpiDO" type="com.xxx.entity.KpiDO">
<result column="warningFlag" property="warningFlag" typeHandler="com.cestc.global.config.mp.TinyIntToBooleanHandler" />
// 省略其他字段
</resultMap>
四、总结
上面就是最近一段时间在迁移国产数据库过程中的一些步骤和解决问题的方式,希望能够给大家带来帮助。整体来说迁移的过程还是比较痛苦的,上面只列出了常规应用的一些问题,还有一些使用动态数据源的应用,迁移起来的问题其实更大,像一些查询所有库,查询所有表,查询所有字段的sql写法和mysql都是完全不一样的。
虽然过程很艰辛,但是我想说的是,在大形势不断紧张的情况下,还是支持有更多企业、机构甚至个人,可以更多的支持国产软件的发展,有朝一日让我们的国产软件也能够站在世界的顶端,我们软件人不在遭受其他人的制裁与威胁。希望这一天早日到来,这也是全体中国软件人真正站起来的时候。