文章目录
- 1 楔子
- 2 分析
- 3 代码实现
- 3.1 管理库关键库表设计
- 3.2 租户库关键库表设计
- 3.3 新建一个SpringBootWeb项目
- 3.4 添加maven依赖
- 3.5 创建初始化数据库工具类
- 3.6 创建动态数据源配置类
- 3.7 创建登录代码
- 3.8 创建数据源元数据服务类
- 3.9 创建saas服务基础父类
- 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=trueusername: rootpassword: 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 */@Slf4jpublic 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@Slf4jpublic 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, ""); }}
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 { /** * 数据源元映射器 */ @Resource private DataSourceMetaMapper datasourceMetaMapper; /** * 选择可用数据 * * @param dataSourceMeta 数据源元 * @return {@link List} */ 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 */@Servicepublic class AreaServiceImpl extends SaasService { /** * 区域映射器 */ @Resource private AreaMapper areaMapper; /** * 选择所有 * * @param area 区域 * @return {@link List} */ 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登录
模拟业务请求