背景

基于mybatis-plus的多数据库兼容功能,实现同时支持Mysql和人大金仓数据库

根据jdk规定,各个数据库厂商必须实现方法java.sql.DatabaseMetaData#getDatabaseProductName,这个方法会返回数据库产品的名称。mybatis就是根据这一原理,通过DatabaseIdProviderdatabaseId通过识别产品的productName,切换到对应数据库的处理,从而解决xml中SQL对于不同数据库的适配问题。

注意点:

人大金仓不支持转义字符` 表名,表字段定义不能使用系统关键字

解决问题

解决不同数据库分页不同

解决通过参数控制执行不同sql

没有特殊处理时执行默认sql

主要实现

1.设置不同数据库枚举类 DBIdEnum

import com.baomidou.mybatisplus.annotation.DbType;
import com.zhikuntech.base.execption.BusinessException;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
/**
 * mybatis使用databaseid时需要适配的厂商名称和自定义的对应值。 厂商名称可以通过dataSource.getConnection().getMetaData().getDatabaseProductName()获取
 **/
@AllArgsConstructor
@Getter
public enum DBIdEnum {

    MYSQL("MySQL", "mysql", "com.mysql.jdbc.Driver", DbType.MYSQL),

    KINGBASE("KingbaseES", "kingbase", "com.kingbase8.Driver", DbType.KINGBASE_ES),
    ;
    /**
     * 数据库驱动唯一标识id,从驱动jar中获取的数据库唯一标志id
     */
    private String name;
    /**
     * 数据库 别名,用于mapper中指定datasourceId
     */
    private String value;
    /**
     * 驱动
     */
    private String driver;
    /**
     * 指定分页插件
     */
    private DbType dbType;


    public static DBIdEnum getEnumer(String databaseId) {
        DBIdEnum[] values = DBIdEnum.values();
        for (DBIdEnum v : values) {
            if (databaseId.equals(v.getValue())) {
                return v;
            }
        }
        throw new BusinessException("DBIdEnum中找不到给定的databaseId:" + databaseId);
    }

    /**
     * 通过数据名获取对应的分页插件
     *
     * @param driver
     * @return
     */
    public static DbType getDbTypeByDriver(String driver) {
        for (DBIdEnum databaseIdEnum : DBIdEnum.values()) {
            if (Objects.equals(databaseIdEnum.getDriver(), driver)) {
                return databaseIdEnum.dbType;
            }
        }
        return null;
    }

2.注册mybatis的DatabaseIdProvider,设置分页插件.配置多个数据源,使用了自定义sqlSessionFactory配置

我们项目是在主数据源文件PrimaryDataSourceConfig中配置,创建了自定义的工厂类

import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.zhikuntech.config.enums.DBIdEnum;
import com.zhikuntech.config.mp.AutoFillMetaObjectHandler;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.util.Properties;

/**
 * basePackages:接口文件的包路径
 */
@Configuration
@ConditionalOnProperty(prefix = "component.scan", name = "ds-master", havingValue = "true")
@MapperScan(basePackages = "com.zhikuntech.*.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {

    /**
     * 表示这个数据源是默认数据源
     * 通过@Primary 确定主数据源
     * 通过 @ConfigurationProperties 配置我们配置文件中的前缀
     */
    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.system")
    public DataSource getPrimaryDateSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "primarySqlSessionFactory")
    @DependsOn("primaryDataSource")
    public SqlSessionFactory primarySqlSessionFactory(DatabaseIdProvider databaseIdProvider)
            throws Exception {
        // 使用 mybatis plus 配置
        MybatisSqlSessionFactoryBean b1 = new MybatisSqlSessionFactoryBean();
        b1.setDataSource(getPrimaryDateSource());
        b1.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml"));

        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setMetaObjectHandler(new AutoFillMetaObjectHandler());
        b1.setGlobalConfig(globalConfig);


        // 分页插件,根据数据源获取对应的分页插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        String databaseId = this.databaseIdProvider().getDatabaseId(getPrimaryDateSource());
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DBIdEnum.getDbTypeByDriver(databaseId)));
        b1.setPlugins(interceptor);

        //注册数据源识别分配器到工厂中
        b1.setDatabaseIdProvider(databaseIdProvider);
        return b1.getObject();
    }


    /**
     * 模板
     *
     * @param sessionFactory sessionFactory
     * @return SqlSessionTemplate
     */
    @Bean("primarySqlSessionTemplate")
    @Primary
    public SqlSessionTemplate primarySqlSessionTemplate(
            @Qualifier("primarySqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }

    /**
     * 事务
     *
     * @param dataSource dataSource
     * @return DataSourceTransactionManager
     */
    @Primary
    @Bean(name = "primaryTransactionManager")
    @DependsOn("primaryDataSource")
    public DataSourceTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * 设置数据库id分配器的配置
     * @return
     */
    @Bean
    public DatabaseIdProvider databaseIdProvider() {
        VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        Properties props = new Properties();
        DBIdEnum[] values = DBIdEnum.values();
        for (DBIdEnum v : values) {
            props.put(v.getName(), v.getValue());
        }
        databaseIdProvider.setProperties(props);
        return databaseIdProvider;
    }

}

如果有多个sqlSesionFactory,配置文件也加上,如



import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.zhikuntech.config.enums.DBIdEnum;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;

/**
 * basePackages:接口文件的包路径
 *
 */
@Configuration
@MapperScan(basePackages = "com.zhikuntech.alarm.mapper.point", sqlSessionFactoryRef = "pointSqlSessionFactory")
public class PointDataSourceConfig {

    /**
     * 表示这个数据源是默认数据源
     * 通过@Primary 确定主数据源
     * 通过 @ConfigurationProperties 配置我们配置文件中的前缀
     */
    @Bean(name = "pointDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.point-model")
    public DataSource getPrimaryDateSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "pointSqlSessionFactory")
    @DependsOn("pointDataSource")
    public SqlSessionFactory primarySqlSessionFactory(DatabaseIdProvider databaseIdProvider)
            throws Exception {
        // 使用 mybatis plus 配置
        MybatisSqlSessionFactoryBean b1 = new MybatisSqlSessionFactoryBean();
        b1.setDataSource(getPrimaryDateSource());
        b1.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/point/*.xml"));

        // 分页插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        String databaseId = databaseIdProvider.getDatabaseId(getPrimaryDateSource());
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DBIdEnum.getDbTypeByDriver(databaseId)));
        b1.setPlugins(interceptor);


        b1.setDatabaseIdProvider(databaseIdProvider);
        return b1.getObject();
    }


    /**
     * 模板
     *
     * @param sessionFactory sessionFactory
     * @return SqlSessionTemplate
     */
    @Bean("pointSqlSessionTemplate")
    public SqlSessionTemplate primarySqlSessionTemplate(
            @Qualifier("pointSqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }

    /**
     * 事务
     *
     * @param dataSource dataSource
     * @return DataSourceTransactionManager
     */
    @Bean(name = "pointTransactionManager")
    @DependsOn("pointDataSource")
    public DataSourceTransactionManager primaryTransactionManager(@Qualifier("pointDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }


}

3.建立通用DBUtils类,专门处理数据库兼容的问题。类初始化的时候指定了DBtype



import com.zhikuntech.base.execption.BusinessException;
import com.zhikuntech.config.enums.DBIdEnum;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.SQLException;


/**
 * 数据库操作工具类
 * 此工具类目的是为了兼容多个数据库,目前兼容的数据库是mysql和 人大金仓
 */
@Component
public class DBUtils {

    @Resource
    private DatabaseIdProvider databaseIdProvider;

    @Resource
    private DataSource dataSource;

    public static DBIdEnum DBType;

    @PostConstruct
    public void init() throws SQLException {
        // 初始化
        String databaseId = databaseIdProvider.getDatabaseId(dataSource);
        DBType = DBIdEnum.getEnumer(databaseId);
    }

    /**
     * 不同原生数据库适配的正则表达式匹配符号
     *
     * @return
     */
    public static String regexp() {
        switch (DBType) {
            case MYSQL:
                return "REGEXP";
            case KINGBASE:
                return "~";
            default:
                defaultOperation();
        }
        return "";
    }

    private static void defaultOperation() {
        throw new BusinessException("不支持的数据库类型");
    }
}

4.兼容使用

1.在XML文件中使用工具类获取方法,同一方法适配不同数据库

<select id="getByRegexp" resultMap="BaseResultMap">
 select * from alarm_config_record where  main.alarm_type_id_json ${@com.zhikuntech.config.utils.DBUtils@regexp()}  '(^|\s|\W)1801524545212($|\s|\W)'
</select>

2.无法在同一sql中适配的。在XML中指定databaseId,分开写sql,databaseId为枚举DBIdEnum中的别名value。没有指定databaseId为无需特殊处理的sql

  <select id="select" resultType="java.lang.String" databaseId="mysql">
        select * from test where 'mysql' = 'mysql'
    </select>
    
    <select id="select" resultType="java.lang.String" databaseId="kingbase">
        select * from test where 'kingbase' = 'kingbase'
    </select>

5.需要兼容的方法总结(待扩展)

  1. mysql中使用 REGEXP 人大金仓使用 ~

  2. 操作符不支持 人大金仓不支持mysql的转义字符 `

  3. 人大金仓插入主键不能使用null值填充,需要插入时去掉id字段

  4. 时间处理函数不一样

mysql:
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d') AS now_date;

kingbase:
to_char(timestamp, format);
  1. groupby 操作

groupby 需要 select列表中的列与group by 一致 或者使用聚合函数 否则人大金仓会报错

目前存在问题

目前方案的DBUtils只获取了唯一的DataSource,如果多个数据源属于不同类型数据库,需要改造DBUtils

如果需要动态切换不同类型的数据源:

不能使用DynamicDataSource组件,因为组件只有一个sqlSessionFactory,切换的时候sqlSessionFactory的Configuration的databaseId唯一,线程共享。无法做到切换不同的databaseId

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐