千家信息网

如何使用Mybatis-plus实现多租户架构

发表于:2025-01-19 作者:千家信息网编辑
千家信息网最后更新 2025年01月19日,这篇文章给大家分享的是有关如何使用Mybatis-plus实现多租户架构的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。多租户(Multi-Tenant)是SaaS中的一个重
千家信息网最后更新 2025年01月19日如何使用Mybatis-plus实现多租户架构

这篇文章给大家分享的是有关如何使用Mybatis-plus实现多租户架构的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

多租户(Multi-Tenant)是SaaS中的一个重要概念,它是一种软件架构技术,在多个租户的环境下,共享同一套系统实例,并且租户之间的数据具有隔离性,也就是说一个租户不能去访问其他租户的数据。基于不同的隔离级别,通常具有下面三种实现方案:

1、每个租户使用独立DataBase,隔离级别高,性能好,但成本大

2、租户之间共享DataBase,使用独立的Schema

3、租户之间共享Schema,在表上添加租户字段,共享数据程度最高,隔离级别最低。

Mybatis-plus在第3层隔离级别上,提供了基于分页插件的多租户的解决方案,我们对此来进行介绍。在正式开始前,首先做好准备工作创建两张表,在基础字段后都添加租户字段tenant_id:

CREATE TABLE `user` (  `id` bigint(20) NOT NULL,  `name` varchar(20) DEFAULT NULL,  `phone` varchar(11) DEFAULT NULL,  `address` varchar(64) DEFAULT NULL,  `tenant_id` bigint(20) DEFAULT NULL,  PRIMARY KEY (`id`))CREATE TABLE `dept` (  `id` bigint(20) NOT NULL,  `dept_name` varchar(64) DEFAULT NULL,  `comment` varchar(128) DEFAULT NULL,  `tenant_id` bigint(20) DEFAULT NULL,  PRIMARY KEY (`id`))

在项目中导入需要的依赖:

    com.baomidou    mybatis-plus-boot-starter    3.3.2    com.github.jsqlparser    jsqlparser    3.1

Mybatis-plus 配置类:

@EnableTransactionManagement(proxyTargetClass = true)@Configurationpublic class MybatisPlusConfig {    @Bean    public PaginationInterceptor paginationInterceptor() {        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();        List sqlParserList=new ArrayList<>();        TenantSqlParser tenantSqlParser=new TenantSqlParser();        tenantSqlParser.setTenantHandler(new TenantHandler() {            @Override            public Expression getTenantId(boolean select) {                               String tenantId = "3";                return new StringValue(tenantId);            }            @Override            public String getTenantIdColumn() {                return "tenant_id";            }            @Override            public boolean doTableFilter(String tableName) {                return false;            }        });        sqlParserList.add(tenantSqlParser);        paginationInterceptor.setSqlParserList(sqlParserList);        return paginationInterceptor;    }}

这里主要实现的功能:

  • 创建SQL解析器集合

  • 创建租户SQL解析器

  • 设置租户处理器,具体处理租户逻辑

这里暂时把租户的id固定写成3,来进行测试。测试执行全表语句:

public List getUserList() {    return userMapper.selectList(new LambdaQueryWrapper().isNotNull(User::getId));}

使用插件解析执行的SQL语句,可以看到自动在查询条件后加上了租户过滤条件:

那么在实际的项目中,怎么将租户信息传给租户处理器呢,根据情况我们可以从缓存或者请求头中获取,以从Request请求头获取为例:

@Overridepublic Expression getTenantId(boolean select) {    ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();    HttpServletRequest request = attributes.getRequest();    String tenantId = request.getHeader("tenantId");    return new StringValue(tenantId);}

前端在发起http请求时,在Header中加入tenantId字段,后端在处理器中获取后,设置为当前这次请求的租户过滤条件。

如果是基于请求头携带租户信息的情况,那么在使用中可能会遇到一个坑,如果当使用多线程的时候,新开启的异步线程并不会自动携带当前线程的Request请求。

@Overridepublic List getUserListByFuture() {    Callable getUser=()-> userMapper.selectList(new LambdaQueryWrapper().isNotNull(User::getId));    FutureTask> future=new FutureTask<>(getUser);    new Thread(future).start();    try {        return future.get();    } catch (Exception e) {        e.printStackTrace();    }    return null;}

执行上面的方法,可以看出是获取不到当前的Request请求的,因此无法获得租户id,会导致后续报错空指针异常:

修改的话也非常简单,开启RequestAttributes的子线程共享,修改上面的代码:

@Overridepublic List getUserListByFuture() {    ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();    Callable getUser=()-> {        RequestContextHolder.setRequestAttributes(sra, true);        return userMapper.selectList(new LambdaQueryWrapper().isNotNull(User::getId));    };    FutureTask> future=new FutureTask<>(getUser);    new Thread(future).start();    try {        return future.get();    } catch (Exception e) {        e.printStackTrace();    }    return null;}

这样修改后,在异步线程中也能正常的获取租户信息了。

那么,有的小伙伴可能要问了,在业务中并不是所有的查询都需要过滤租户条件啊,针对这种情况,有两种方式来进行处理。

1、如果整张表的所有SQL操作都不需要针对租户进行操作,那么就对表进行过滤,修改doTableFilter方法,添加表的名称:

@Overridepublic boolean doTableFilter(String tableName) {    List IGNORE_TENANT_TABLES= Arrays.asList("dept");    return IGNORE_TENANT_TABLES.stream().anyMatch(e->e.equalsIgnoreCase(tableName));}

这样,在dept表的所有查询都不进行过滤:

2、如果有一些特定的SQL语句不想被执行租户过滤,可以通过@SqlParser注解的形式开启,注意注解只能加在Mapper接口的方法上:

@SqlParser(filter = true)@Select("select * from user where name =#{name}")User selectUserByName(@Param(value="name") String name);

或在分页拦截器中指定需要过滤的方法:

@Beanpublic PaginationInterceptor paginationInterceptor() {    PaginationInterceptor paginationInterceptor = new PaginationInterceptor();    paginationInterceptor.setSqlParserFilter(metaObject->{        MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);        // 对应Mapper、dao中的方法        if("com.cn.tenant.dao.UserMapper.selectUserByPhone".equals(ms.getId())){            return true;        }        return false;    });    ...}

上面这两种方式实现的功能相同,但是如果需要过滤的SQL语句很多,那么第二种方式配置起来会比较麻烦,因此建议通过注解的方式进行过滤。

除此之外,还有一个比较容易踩的坑就是在复制Bean时,不要复制租户id字段,否则会导致SQL语句报错:

public void createSnapshot(Long userId){    User user = userMapper.selectOne(new LambdaQueryWrapper().eq(User::getId, userId));    UserSnapshot userSnapshot=new UserSnapshot();    BeanUtil.copyProperties(user,userSnapshot);    userSnapshotMapper.insert(userSnapshot);}

查看报错可以看出,本身Bean的租户字段不为空的情况下,SQL又自动添加一次租户查询条件,因此导致了报错:

我们可以修改复制Bean语句,手动忽略租户id字段,这里使用的是hutool的BeanUtil工具类,可以添加忽略字段。

BeanUtil.copyProperties(user,userSnapshot,"tenantId");

在忽略了租户id的拷贝后,查询可以正常执行。

最后,再来看一下对联表查询的支持,首先看一下包含子查询的SQL:

@Select("select * from user where id in (select id from user_snapshot)")List selectSnapshot();

查看执行结果,可以看见,在子查询的内部也自动添加的租户查询条件:

再来看一下使用Join进行联表查询:

@Select("select u.* from user u left join user_snapshot us on u.id=us.id")List selectSnapshot();

同样,会在左右两张表上都添加租户的过滤条件:

再看一下不使用Join的普通联表查询:

@Select("select u.* from user u ,user_snapshot us,dept d where u.id=us.id and d.id is not null")List selectSnapshot();

查看执行结果,可以看见在这种情况下,只在FROM关键字后面的第一张表上添加了租户的过滤条件,因此如果使用这种查询方式,需要额外注意,用户需要手动在SQL语句中添加租户过滤。

感谢各位的阅读!关于"如何使用Mybatis-plus实现多租户架构"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!

租户 查询 字段 条件 语句 情况 方式 方法 线程 处理 隔离 级别 架构 之间 信息 处理器 数据 注解 内容 功能 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 笔记本代理服务器自动打开 腾讯云服务器更改账号密码错误 软件开发团队一般多少人 科技公司数据服务器 如何把网页复制到另一个服务器上 阿里云服务器地址显示青岛 抓包软件怎么能看到服务器跳转 工程中网络技术原理 重庆hp服务器阵列卡驱动服务器 wifi无法连接到服务器原因 软件开发工具实践课怎么考 进口实时数据库软件 怎么设置数据库时间 网络安全属于哪个法律范畴 三大中文全文数据库的对比 mssql数据库太大怎么办 2019网络安全周 第几界 射阳服务器和滨海服务区哪个大 管理工具中服务器有哪些功能 网络安全七层防护 自愈合网络技术 前置数据库设计 苏州手机软件开发费用 苏州多美初会网络技术有限公司 中山市网络安全作业 河北省网络安全专业大专排名 java连接数据库获取表名 迅雷网络技术 颜文俊 excel表个数据库 软件开发工程师和售前工程师
0