【工作小札】利用动态数据源实现Sass的一种思路(内含完整代码示例)
文章目录
- 1 楔子
- 2 分析
- 3 代码实现
- 4 示例演示
-
- 4.1 下载示例代码
- 4.2 执行resources下初始化数据库脚本init.sql
- 4.3 修改resources下application.yml中数据库配置
- 4.4 通过DynamicApplication启动项目
- 4.5 测试
-
- 4.5.1 添加数据库
- 4.5.2 初始化数据库
- 4.5.2 多租户测试
✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:【工作小札】🍅
✈️本篇内容: 利用动态数据源实现Sass化✈️
🍱本篇收录完整代码地址:https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-dynamicDataSource🍱
1 楔子
针对Sass多租户,业内有许多解决方案。一般来说,如果做的简单一点,直接用一个表字段区分租户,所有db操作都带上这个标识即可。如果做的稍微好一点,我们可以考虑分库,即每个租户都拥有自己的数据库,且可以将数据库部署在本地。
2 分析
基于分库的需求,我们可以做以下技术拆分:
1、需要有一个管理中心,管理所有租户的数据库,这个应该是一个单独的库,租户的库又是其他单独的库。
2、从管理中心页面上,要能够对租户的库进行管理,比如动态建库建表。
3、后台只用一套代码,所以要动态适配数据源。
4、租户登录之后,应该就要适配到适合自己的库。
3 代码实现
以下是关键代码的实现,如果读者不感兴趣,可以直接看第4章。
3.1 管理库关键库表设计
库名随意,我这里取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;
3.2 租户库关键库表设计
租户主要是业务表,我们这里就随便设计一个地区表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;
3.3 新建一个SpringBootWeb项目
3.4 添加maven依赖
为了实现我们的需求,需要添加以下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>
3.5 创建初始化数据库工具类
/* 初始化数据库工具 @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;}
}
3.6 创建动态数据源配置类
关键代码是 DynamicRoutingDataSource 的 api 的使用
/* 动态数据源配置 @author 第七人格* @date 2023/04/13*/
@Component
@Slf4j
public class DynamicDataSourceConfig {/* 缓存*/private final Map<String, String> cache = new HashMap<>();/* 数据源*/@Resourceprivate DynamicRoutingDataSource dataSource;/* 加载所有数据库*/@PostConstructpublic 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, "");}
}
3.7 创建登录代码
关键代码是将前端传入的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 "登录成功!";}
}
3.8 创建数据源元数据服务类
关键代码是使用com.baomidou.dynamic.datasource.annotation.DS注解
@DS(“master”),标明使用的是管理库
/* 数据源元数据服务 @author 第七人格* @date 2023/04/13*/
@Service
@DS("master")
public class DataSourceMetaService {/* 数据源元映射器*/@Resourceprivate 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());}
}
3.9 创建saas服务基础父类
关键代码师使用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 {/* 区域映射器*/@Resourceprivate 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();}
}
4 示例演示
4.1 下载示例代码
https://gitee.com/diqirenge/sheep-web-demo/tree/master/sheep-web-demo-dynamicDataSource
4.2 执行resources下初始化数据库脚本init.sql
4.3 修改resources下application.yml中数据库配置
4.4 通过DynamicApplication启动项目
4.5 测试
测试方法皆可在http-test-api.http文件中查看
4.5.1 添加数据库
4.5.2 初始化数据库
初始化数据库dy_test_1
初始化后,可在本地库中看到新建的数据库
修改area表area_name的数据为重庆测试
初始化数据库dy_test
4.5.2 多租户测试
模拟dy_test登录
模拟业务请求
模拟dy_test_1登录
模拟业务请求