美文网首页
Springboot 3 + Druid + MybatisPl

Springboot 3 + Druid + MybatisPl

作者: 齐格Insight | 来源:发表于2024-09-12 22:46 被阅读0次

    1 背景

    最近在做一款名为星舰的产品,需要用到多租户的能力。多租户的实现无非两种方式,一种方式是采用的是分库分表的模式,每个租户单独一套数据库表;另一种方式是共享数据库表的方式。这两种方式各有各的优缺点。本文以共享数据库表为例,实现多租户。每个表都添加一个tenant_id字段,在做表CRUD时,希望能做到和无多租户的 SQL一样。这里,我采用了 MybatisPlus的多租户插件。

    2 准备工作

    2.1 增加依赖

    在项目中添加mybatis-plusdruid的依赖,如下(注意和 springboot 2.x是有区别的):

     <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                <version>3.5.7</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-3-starter</artifactId>
                <version>1.2.23</version>
    </dependency>
    

    2.2 添加数据库表

    先建两张表,一张为用户表 sys_user,另一张为用户地址表 user_addr,这两张表通过user_id字段进行关联。插入对应数据,如下:

    sys_user 表 user_addr 表

    我们的租户的字段为 tenant_id 这个字段,这里我们只对sys_user增加租户 ID字段。建表和插入数据,对应的 SQL语句如下:

    USE `mtenant`;
    
    drop table if exists `sys_user`;
    CREATE TABLE `sys_user`
    (
        `user_id`   bigint      NOT NULL AUTO_INCREMENT COMMENT '用户ID',
        `user_name` varchar(30) NOT NULL COMMENT '用户名',
        `tenant_id` varchar(64) DEFAULT 'default' COMMENT '租户ID',
        PRIMARY KEY (`user_id`) USING BTREE,
        KEY         `idx_tenant_id` (`tenant_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=Dynamic COMMENT='用户信息表';
    
    INSERT INTO `sys_user` (user_id, user_name, tenant_id)
    values (1, 'admin', 'default'),
           (2, 'zhonghui', 'zhc'),
           (3, 'ceshi', 'zhc'),
           (4, 'aborn', 'zhc');
    
    drop table if exists `user_addr`;
    CREATE TABLE `user_addr`
    (
        `id`        bigint       NOT NULL AUTO_INCREMENT COMMENT 'ID',
        `user_id`   bigint       NOT NULL COMMENT '用户ID',
        `addr`      varchar(500) NOT NULL COMMENT '地址信息',
        PRIMARY KEY (`id`) USING BTREE,
        KEY         `idx_user_id` (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=Dynamic COMMENT='用户信息表';
    
    INSERT INTO `user_addr` (id, user_id, addr)
    values (1, 2, '中慧物联苏州'),
           (2, 2, '中慧物联上海地址'),
           (3, 2, '中慧物联重庆地址'),
           (4, 2, '中慧物联广州地址'),
           (5, 2, '中慧广州地址'),
           (6, 2, '中慧物联广州地址'),
           (7, 2, '中慧物联广州地址'),
           (8, 1, '中慧物联广州地址'),
           (9, 1, '中慧物联广州地址');
    

    3. MybatisPlus多租户的配置

    MybatisPlus 有一个扩展TenantLineInnerInterceptor已经实现了多租户的能力,它在数据查询的时候默认帮忙加上 tenant_id = 'tn' 租户的限制,以达到按租户过滤的功能。在数据插入时默认将当前用户的 tenant_id插入相应字段。这个过程对代码无侵入性,也不需要过多改造原有 SQL语句。

    3.1 配置 MybatisPlusConfig

    package com.zhonghuitech.multitenant.config;
    
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
    import net.sf.jsqlparser.expression.Expression;
    import net.sf.jsqlparser.expression.StringValue;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author aborn
     * @since 2018-08-10
     */
    @Configuration
    @MapperScan("com.zhonghuitech.**.mapper")
    public class MybatisPlusConfig {
        public static final String tenantId = "zhc";
    
        /**
         * 多租户插件配置
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
                @Override
                public Expression getTenantId() {
                    return new StringValue(tenantId);
                }
    
                @Override
                public boolean ignoreTable(String tableName) {
                   return "user_addr".equals(tableName);
                }
            }));
            // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return interceptor;
        }
    }
    

    注意点:

    1. 这里的 getTenantId 实现是个关键,依据你实际项目来,一般是传入的是登录用户的租户 ID字段,我这里为了测试方便直接写死为zhc
    2. ignoreTable 对于一些表,可以跳过多租户模型(表里本身无tenant_id字段),在这里可以配置。我这里配置了user_addr这张表不做多租户的过滤。
    3. 这里有个坑,如果代码里配置了SqlSessionFactory的注入,需要将上面改成如下配置,否则多租户插件不生效:
      image.png

    3.2 特殊SQL语句忽略拦截

    在一些场景下,无需多租户拦截,或者对于一些超级管理员使用的接口,希望跨租户查询、免数据鉴权时,可以通过下面几种方式实现忽略拦截:

    1. 使用MybatisPlus框架自带的@InterceptorIgnore注解,以用在Mapper类上,也可以用在Mapper类的方法上
    2. 添加超级用户账号白名单,可以在ignoreTable里针对特定账户,直接返回 true,跳过拦截
    3. 添加数据表白名单,可以在ignoreTable里针对特定数据库表,直接返回 true,跳过拦截

    4 测试多租户

    4.1 编写测试用例

    package com.zhonghuitech.multitenant;
    
    import com.baomidou.mybatisplus.core.toolkit.Wrappers;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.zhonghuitech.multitenant.config.MybatisPlusConfig;
    import com.zhonghuitech.multitenant.entity.User;
    import com.zhonghuitech.multitenant.mapper.UserMapper;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.util.CollectionUtils;
    
    import java.util.List;
    
    import static org.assertj.core.api.Assertions.assertThat;
    
    /**
     * @author aborn (jiangguobao)
     * @date 2024/09/13 11:03
     */
    @SpringBootTest
    public class TenantTest {
        @Autowired
        private UserMapper mapper;
    
        @Test
        void contextLoads() {
        }
    
        @Test
        void dSelect() {
            List<User> userList = mapper.selectList(null);
            userList.forEach(u -> Assertions.assertTrue(MybatisPlusConfig.tenantId.equals(u.getTenantId())));
        }
    
        // 联表Join的测试
        @Test
        void dSelectJoin() {
            List<User> userList = mapper.getAddrAndUser(null);
            if (!CollectionUtils.isEmpty(userList)) {
                userList.forEach(u -> Assertions.assertTrue(MybatisPlusConfig.tenantId.equals(u.getTenantId())));
            }
        }
    
        // 分页查询的测试
        @Test
        void lambdaPagination() {
            Page<User> page = new Page<>(1, 2);
            Page<User> result = mapper.selectPage(page, Wrappers.<User>lambdaQuery().orderByAsc(User::getUserId));
            assertThat(result.getTotal()).isGreaterThan(2);
            assertThat(result.getRecords().size()).isEqualTo(2);
        }
    }
    

    4.2 运行测试用例

    1. 第一个测试用例 dSelect,普通查询语句
      运行日志如下:
    2024-09-16T18:50:05.515+08:00 DEBUG 47298 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.selectList       : ==>  Preparing: SELECT user_id, tenant_id, user_name, nick_name FROM sys_user WHERE tenant_id = 'zhc'
    2024-09-16T18:50:05.600+08:00 DEBUG 47298 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.selectList       : ==> Parameters: 
    2024-09-16T18:50:05.614+08:00 DEBUG 47298 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.selectList       : <==      Total: 5
    

    测试结果:

    image.png

    真实运行的sql如下:

    SELECT user_id, user_name, tenant_id FROM sys_user WHERE tenant_id = 'zhc'
    

    看打印出的 SQL 语句,自动加上了 tenant_id='zhc'的限制条件。

    1. 第二个测试用例 dSelectJoin,联表查询语句
      getAddrAndUser SQL语句如下:
      image.png

    运行日志如下:

    2024-09-16T19:10:19.547+08:00 DEBUG 85079 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.getAddrAndUser   : ==>  Preparing: SELECT u.user_id, u.user_name, u.tenant_id, a.addr FROM sys_user u LEFT JOIN user_addr a ON u.user_id = a.user_id WHERE u.tenant_id = 'zhc'
    2024-09-16T19:10:19.616+08:00 DEBUG 85079 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.getAddrAndUser   : ==> Parameters: 
    2024-09-16T19:10:19.631+08:00 DEBUG 85079 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.getAddrAndUser   : <==      Total: 8
    

    真实运行的sql如下:

    SELECT u.user_id, u.user_name, u.tenant_id, a.addr FROM sys_user u LEFT JOIN user_addr a ON u.user_id = a.user_id WHERE u.tenant_id = 'zhc'
    
    1. 第三个测试用例lambdaPagination,分页查询
      运行日志如下:
    2024-09-16T19:17:18.460+08:00 DEBUG 82705 --- [multi-tenant] [           main] c.z.m.m.UserMapper.selectList_mpCount    : ==>  Preparing: SELECT COUNT(*) AS total FROM sys_user WHERE tenant_id = 'zhc'
    2024-09-16T19:17:18.535+08:00 DEBUG 82705 --- [multi-tenant] [           main] c.z.m.m.UserMapper.selectList_mpCount    : ==> Parameters: 
    2024-09-16T19:17:18.545+08:00 DEBUG 82705 --- [multi-tenant] [           main] c.z.m.m.UserMapper.selectList_mpCount    : <==      Total: 1
    2024-09-16T19:17:18.548+08:00 DEBUG 82705 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.selectList       : ==>  Preparing: SELECT user_id, user_name, tenant_id FROM sys_user WHERE tenant_id = 'zhc' ORDER BY user_id ASC LIMIT ?
    2024-09-16T19:17:18.550+08:00 DEBUG 82705 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.selectList       : ==> Parameters: 2(Long)
    2024-09-16T19:17:18.552+08:00 DEBUG 82705 --- [multi-tenant] [           main] c.z.m.mapper.UserMapper.selectList       : <==      Total: 2
    
    分页查询结果

    5. 源码地址

    https://github.com/zhonghuitech/multi-tenant

    相关文章

      网友评论

          本文标题:Springboot 3 + Druid + MybatisPl

          本文链接:https://www.haomeiwen.com/subject/lagmljtx.html