一套代码内多类型数据库适配方案-人大金仓+mysql示例
我们项目是在主数据源文件PrimaryDataSourceConfig中配置,创建了自定义的工厂类/*** basePackages:接口文件的包路径*//*** 表示这个数据源是默认数据源* 通过@Primary 确定主数据源* 通过 @ConfigurationProperties 配置我们配置文件中的前缀*/@Primary@Primary// 使用 mybatis plus 配置。
背景
基于mybatis-plus的多数据库兼容功能,实现同时支持Mysql和人大金仓数据库
根据jdk规定,各个数据库厂商必须实现方法java.sql.DatabaseMetaData#getDatabaseProductName,这个方法会返回数据库产品的名称。mybatis就是根据这一原理,通过DatabaseIdProvider和databaseId通过识别产品的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.需要兼容的方法总结(待扩展)
-
mysql中使用 REGEXP 人大金仓使用 ~
-
操作符不支持 人大金仓不支持mysql的转义字符 `
-
人大金仓插入主键不能使用null值填充,需要插入时去掉id字段
-
时间处理函数不一样
mysql:
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d') AS now_date;
kingbase:
to_char(timestamp, format);
-
groupby 操作
groupby 需要 select列表中的列与group by 一致 或者使用聚合函数 否则人大金仓会报错
目前存在问题
目前方案的DBUtils只获取了唯一的DataSource,如果多个数据源属于不同类型数据库,需要改造DBUtils
如果需要动态切换不同类型的数据源:
不能使用DynamicDataSource组件,因为组件只有一个sqlSessionFactory,切换的时候sqlSessionFactory的Configuration的databaseId唯一,线程共享。无法做到切换不同的databaseId
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)