很多时候我们的项目用到的可能不是一个数据库或者一种类型的数据库,根据业务我们分有以下类型:单数据库单数据源,多数据库单数据源,多数据库多数据源。面对这样的情况我们可以采用dynamic-datasource-spring-boot-starter ,这是一个基于springboot的快速集成多数据源的启动器。
_
分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。spring.datasource.dynamic.primary
修改。// orm框架 mybatis-plus
implementation group: 'com.baomidou', name: 'mybatis-plus-boot-starter', version: '3.4.3.1'
// 多数据源核心依赖 dynamic-datasource-spring-boot-starter
implementation group: 'com.baomidou', name: 'dynamic-datasource-spring-boot-starter', version: '3.4.0'
// 各类数据库
// dm
compile group: 'com.dameng', name: 'Dm8JdbcDriver17', version: '8.1.1.49'
// compile('com.dameng:Dm7JdbcDriver17:7.6.0.77')
// 人大金仓
implementation 'com.kingbase8:kingbase8:8.2.0'
// 神通数据库
implementation 'com.shentong:stoscar:1.0'
//mysql
implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.25'
如果使用多数据源模式的话,原来在spring.datasource下的连接池配置需要转移到spring.datasource.dynamic下才生效。
hikari在spring.datasource下的配置和spring.datasource.dynamic下略有不同,这一点需要注意。
# 配置文件
spring:
datasource:
# 多数据源配置
dynamic:
# 链接池配置
hikari:
#最小空闲连接数量
min-idle: 5
#从池返回的连接默认自动提交
is-auto-commit: true
#空闲连接最大时间,10秒
idle-timeout: 10000
#连接池名字
pool-name: CsseHikariCP
#池中连接的最长生命周期
max-lifetime: 1800000
#数据库连接的超时时间
connection-timeout: 30000
#测试SQL
connection-test-query: SELECT 1
primary: dm #设置默认的数据源或者数据源组,默认值即为master
strict: true #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
dm:
url: jdbc:dm://192.168.40.205
username: xxxxx
password: xxxxx
driver-class-name: dm.jdbc.driver.DmDriver
kingbases:
url: jdbc:kingbase8://192.168.17.151:54321/PLATFORM_APP_STORE
username: xxxxx
password: xxxxx
driver-class-name: com.kingbase8.Driver
oscar:
url: jdbc:oscar://192.168.17.40:2003/OSRDB?serverTimezone=UTC&useSSL=FALSE
username: xxxxx
password: xxxxx
driver-class-name: com.oscar.Driver
mysql:
url: jdbc:mysql://192.168.40.206:3306/am?characterEncoding=UTF-8&serverTimezone=UTC
username: xxxxx
password: xxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
若是没有多数据库多模式的支持的场景,直接更改数据源配置即可,如果场景中需要用到多库多模式的情况,又希望通过注解或者自定义配置进行操作的话可以采用如下两种模式
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称 |
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
通过自定义MybatisPlusConfig,我们可以实现对数据库模式的动态拼接,从而更好的实现多租户和多数据源等场景
package com.csse.platform.appstore.api.config.mybatisplus;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.csse.platform.appstore.api.context.ApiContext;
import com.google.common.collect.Lists;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.commons.io.FilenameUtils;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ClassUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* @Author : 写代码的徐师傅
* @Company : 中软信息系统工程有限公司
* @Date : Created in 14:32 2020/4/22
*/
@Configuration
@MapperScan("com.csse.platform.appstore.api.mapper")
public class MybatisPlusConfig {
private static final String SYSTEM_TENANT_ID = "tenant_id";
private static final List IGNORE_TENANT_TABLES = Lists.newArrayList("tenant");
static final String DEFAULT_RESOURCE_PATTERN = "*.class";
@Value("${spring.datasource.dynamic.primary}")
private String primaryDbType;
@Value("${database.databaseSchema}")
private String databaseSchema;
@Autowired
private ApiContext apiContext;
/**
* 自定义MybatisPlusInterceptor
*
* 实现下上文注入租户id 拦截修改sql 多种数据源分页模式支持
*
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 多租户
mybatisPlusInterceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
Long currentTenantId = apiContext.getCurrentTenantId();
if (null == currentTenantId) {
throw new RuntimeException("#1129 getCurrentTenantId error.");
}
return new LongValue(currentTenantId);
}
@Override
public String getTenantIdColumn() {
return SYSTEM_TENANT_ID;
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
// 忽略掉一些表:如租户表(Tenant)本身不需要执行这样的处理。
return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
}
}));
// 动态拼接
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
HashMap map = new HashMap(2) {{
// 操作单张表
// put("platform_app_classify", (sql, tableName) -> { return "PLATFORM_APP_STORE." + tableName; });
// 遍历操作所有表
List tables = new ArrayList<>();
tables.addAll(listByClassAnnotation("com.csse.platform.appstore.api.entity"));
tables.addAll(listByClassAnnotation("com.csse.platform.appstore.api.entity.view"));
tables.forEach(table -> put(table, (sql, tableName) -> {
return databaseSchema + tableName;
}));
}};
dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
mybatisPlusInterceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
// 分页
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
if (primaryDbType.equals(DbType.DM.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.DM));
} else if (primaryDbType.equals(DbType.KINGBASE_ES.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.KINGBASE_ES));
} else if (primaryDbType.equals(DbType.OSCAR.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.OSCAR));
} else if (primaryDbType.equals(DbType.MYSQL.getDb())) {
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
}
return mybatisPlusInterceptor;
}
/**
* 自动填充功能
*
* @return globalConfig
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MetaHandler());
return globalConfig;
}
/**
* 扫描所有包,通过反射获取到类中@TableName标注的表名称
*
* @param packageName 包名称
* @return List listByClassAnnotation
*/
public static List listByClassAnnotation(String packageName) {
List tableNameList = new ArrayList<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(packageName) + File.separator + DEFAULT_RESOURCE_PATTERN;
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
for (Resource resource : resources) {
URL url = resource.getURL();
File file = new File(url.getPath());
String cname = packageName + "." + FilenameUtils.getBaseName(file.getName());
Class> aClass = Class.forName(cname);
if (aClass.isAnnotationPresent(TableName.class)) {
TableName declaredAnnotation = aClass.getDeclaredAnnotation(TableName.class);
tableNameList.add(declaredAnnotation.value());
}
}
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return tableNameList;
}
public static String resolveBasePackage(String basePackage) {
return ClassUtils.convertClassNameToResourcePath(new StandardEnvironment().resolveRequiredPlaceholders(basePackage));
}
}