✨这里是第七人格的博客✨小七,欢迎您的到来~✨
系列专栏:【工作小札】
✈️本篇内容: 利用动态数据源实现Sass化✈️
本篇收录完整代码地址:https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-dynamicDataSource
针对Sass多租户,业内有许多解决方案。一般来说,如果做的简单一点,直接用一个表字段区分租户,所有db操作都带上这个标识即可。如果做的稍微好一点,我们可以考虑分库,即每个租户都拥有自己的数据库,且可以将数据库部署在本地。
基于分库的需求,我们可以做以下技术拆分:
1、需要有一个管理中心,管理所有租户的数据库,这个应该是一个单独的库,租户的库又是其他单独的库。
2、从管理中心页面上,要能够对租户的库进行管理,比如动态建库建表。
3、后台只用一套代码,所以要动态适配数据源。
4、租户登录之后,应该就要适配到适合自己的库。
以下是关键代码的实现,如果读者不感兴趣,可以直接看第4章。
库名随意,我这里取dynamic
CREATE DATABASE `dynamic` ;
作为管理库,肯定要管理其他库的数据库元数据,那么抽象出哪些元数据比较合适呢?观察以下配置
url: jdbc:mysql://localhost:3306/dynamic?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: 123456
我们发现连接Mysql时,需要配置url、username以及password,为了方便切库我们多抽象设计一个schema(即问号前面的dynamic部分)。这里给出一个简单的参考表如下:
CREATE TABLE `data_source_meta` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',
`url` varchar(127) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql地址',
`mysql_schema` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql库名',
`user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql用户名',
`user_password` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'mysql密码',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
租户主要是业务表,我们这里就随便设计一个地区表area
CREATE TABLE `area` (
`area_id` int(0) NOT NULL AUTO_INCREMENT,
`area_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`area_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
为了实现我们的需求,需要添加以下2个关键依赖
<!--动态数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<!--加入数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
/**
* 初始化数据库工具
*
* @author 第七人格
* @date 2023/04/13
*/
@Slf4j
public class InitDBUtil {
/**
* jdbc url模板
*/
private static final String jdbcUrlTemplate = "jdbc:mysql://#{mysqlUrl}/#{schema}?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true";
/**
* 驱动程序类
*/
private static final String driverClass = "com.mysql.cj.jdbc.Driver";
/**
* 删除sql模板
*/
private static final String dropSchemaSqlTemplate = "DROP DATABASE IF EXISTS #{schema}";
/**
* 创建sql模板
*/
private static final String createSchemaSqlTemplate = "CREATE DATABASE `#{schema}` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'; ";
/**
* 使用sql模板
*/
private static final String useSchemaSqlTemplate = "use `#{schema}`;";
/**
* 初始化数据库
*
* @param mysqlUrl mysql url
* @param schema 模式
* @param username 用户名
* @param password 密码
* @return boolean
*/
public static boolean initDB(String mysqlUrl,String schema,String username,String password){
Connection connection = null;
try{
Class.forName(driverClass);
connection = DriverManager.getConnection(jdbcUrlTemplate.replace("#{mysqlUrl}",mysqlUrl).replace("#{schema}","mysql"), username, password);
Statement statement = connection.createStatement();
statement.execute(dropSchemaSqlTemplate.replace("#{schema}",schema));
statement.execute(createSchemaSqlTemplate.replace("#{schema}",schema));
statement.execute(useSchemaSqlTemplate.replace("#{schema}",schema));
ScriptRunner scriptRunner = new ScriptRunner(connection);
scriptRunner.setStopOnError(true);
ClassPathResource classPathResource = new ClassPathResource("sqlTemplate.sql");
InputStream inputStream = classPathResource.getInputStream();
InputStreamReader isr = new InputStreamReader(inputStream);
scriptRunner.runScript(isr);
return true;
}catch(Exception e){
log.error("初始化数据库失败,{}",e.getMessage());
return false;
}finally {
if(null != connection){
try {
connection.commit();
connection.close();
} catch (SQLException ignored) {
}
}
}
}
public static boolean tryConnectDB(String mysqlUrl,String schema,String username,String password){
Connection connection = null;
try{
Class.forName(driverClass);
connection = DriverManager.getConnection(jdbcUrlTemplate.replace("#{mysqlUrl}",mysqlUrl).replace("#{schema}",schema), username, password);
return true;
}catch(Exception e){
log.error("尝试连接数据库失败,{}",e.getMessage());
return false;
}finally {
if(null != connection){
try {
connection.commit();
connection.close();
} catch (SQLException ignored) {
}
}
}
}
/**
* 得到初始化数据库配置
*
* @return {@link DruidDataSource}
*/
public static DruidDataSource getInitDBConfig(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setInitialSize(5);
dataSource.setMinIdle(5);
dataSource.setMaxActive(20);
dataSource.setMaxWait(60000);
dataSource.setTimeBetweenEvictionRunsMillis(60000);
dataSource.setMinEvictableIdleTimeMillis(300000);
dataSource.setValidationQuery("select 1 from dual");
dataSource.setTestWhileIdle(true);
dataSource.setTestOnBorrow(false);
dataSource.setTestOnReturn(false);
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
return dataSource;
}
}
关键代码是 DynamicRoutingDataSource 的 api 的使用
/**
* 动态数据源配置
*
* @author 第七人格
* @date 2023/04/13
*/
@Component
@Slf4j
public class DynamicDataSourceConfig {
/**
* 缓存
*/
private final Map<String, String> cache = new HashMap<>();
/**
* 数据源
*/
@Resource
private DynamicRoutingDataSource dataSource;
/**
* 加载所有数据库
*/
@PostConstruct
public void loadAllDB(){
cache.put("master","管理中心");
// todo 这里可以做成,项目一启动就去读取管理库的数据库元数据,加载到缓存之中
}
/**
* 动态添加数据库
*
* @param datasourceMeta 数据源元
*/
public void addDB(DataSourceMeta datasourceMeta){
DruidDataSource tmpdb = InitDBUtil.getInitDBConfig();
tmpdb.setUsername(datasourceMeta.getUsername());
tmpdb.setPassword(datasourceMeta.getPassword());
tmpdb.setUrl("jdbc:mysql://"+ datasourceMeta.getUrl()+"/"+ datasourceMeta.getMysqlSchema()+"?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true");
dataSource.addDataSource(datasourceMeta.getMysqlSchema(),tmpdb);
log.info("======动态添加数据库完成:mysqlSchema={}",datasourceMeta.getMysqlSchema());
cache.put(datasourceMeta.getMysqlSchema(), datasourceMeta.getName());
}
/**
* 动态删除数据库
*
* @param datasourceMeta 数据源元
*/
public void deleteDB(DataSourceMeta datasourceMeta){
dataSource.removeDataSource(datasourceMeta.getMysqlSchema());
log.info("======动态删除数据库完成:mysqlSchema={}",datasourceMeta.getMysqlSchema());
cache.remove(datasourceMeta.getMysqlSchema());
}
/**
* 通过schema获取在元数据中的名称
*
* @param schema 模式
* @return {@link String}
*/
public String getNameBySchema(String schema){
return cache.getOrDefault(schema, "");
}
}
关键代码是将前端传入的schema,放到浏览器session中
/**
* 登录控制器
*
* @author 第七人格
* @date 2023/04/13
*/
@RestController
@RequestMapping(value = "/admin")
public class LoginController {
/**
* 登录
*
* @param schema 模式
* @param request 请求
* @return {@link String}
*/
@GetMapping("/login/{schema}")
public String login(@PathVariable String schema, HttpServletRequest request) {
// 存入session,用于切库
request.getSession().setAttribute("schema",schema);
return "登录成功!";
}
}
关键代码是使用com.baomidou.dynamic.datasource.annotation.DS注解
@DS(“master”),标明使用的是管理库
/**
* 数据源元数据服务
*
* @author 第七人格
* @date 2023/04/13
*/
@Service
@DS("master")
public class DataSourceMetaService {
/**
* 数据源元映射器
*/
@Resource
private DataSourceMetaMapper datasourceMetaMapper;
/**
* 选择可用数据
*
* @param dataSourceMeta 数据源元
* @return {@link List}<{@link DataSourceMeta}>
*/
public List<DataSourceMeta> selectAvailable(DataSourceMeta dataSourceMeta) {
return new LambdaQueryChainWrapper<>(datasourceMetaMapper)
.eq(DataSourceMeta::getId, dataSourceMeta.getId())
.eq(DataSourceMeta::getUrl, dataSourceMeta.getUrl())
.list();
}
public void add(DataSourceMeta dataSourceMeta) {
datasourceMetaMapper.insert(dataSourceMeta);
}
public void update(DataSourceMeta dataSourceMeta) {
datasourceMetaMapper.updateById(dataSourceMeta);
}
public void delete(int dataSourceMetaId) {
datasourceMetaMapper.deleteById(dataSourceMetaId);
}
public boolean initDB(DataSourceMeta dataSourceMeta) {
return InitDBUtil.initDB(dataSourceMeta.getUrl(),dataSourceMeta.getMysqlSchema(),dataSourceMeta.getUsername(),dataSourceMeta.getPassword());
}
public boolean tryConnectDB(DataSourceMeta dataSourceMeta) {
return InitDBUtil.tryConnectDB(dataSourceMeta.getUrl(),dataSourceMeta.getMysqlSchema(),dataSourceMeta.getUsername(),dataSourceMeta.getPassword());
}
}
关键代码师使用com.baomidou.dynamic.datasource.annotation.DS注解
@DS(“#session.schema”), 该接口下的所有数据操作默认根据session中的schema进行路由,其他业务服务类都要继承他
/**
* saas服务
* 该接口下的所有数据操作默认根据session中的schema进行路由。
*
* @author 第七人格
* @date 2023/04/13
*/
@DS("#session.schema")
public class SaasService {
}
业务实现类例子
/**
* 区域服务impl
*
* @author 第七人格
* @date 2023/04/13
*/
@Service
public class AreaServiceImpl extends SaasService {
/**
* 区域映射器
*/
@Resource
private AreaMapper areaMapper;
/**
* 选择所有
*
* @param area 区域
* @return {@link List}<{@link Area}>
*/
public List<Area> selectAll(Area area) {
return new LambdaQueryChainWrapper<>(areaMapper)
.eq(Area::getAreaId, area.getAreaId())
.list();
}
}
https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-dynamicDataSource
测试方法皆可在http-test-api.http文件中查看