【工作小札】利用动态数据源实现Sass的一种思路(内含完整代码示例)

✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:【工作小札】🍅
✈️本篇内容: 利用动态数据源实现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<>();

    /**
     * 数据源
     */
    @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}<{@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 {
    /**
     * 区域映射器
     */
    @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();
    }
}

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登录
在这里插入图片描述

模拟业务请求
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

第七人格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值