MyBatis Plus重写多租户拦截器,实现基于租户列的模糊查询

文章介绍了如何在MyBatisplus中实现多租户数据隔离,包括在构造方法中添加处理逻辑,自定义TenantLineHandler以支持模糊查询。作者还详细描述了如何重写buildTableExpression方法以满足特殊需求,如数据同时存在于多个租户中。
摘要由CSDN通过智能技术生成

注意:代码有更新,在MyTenantLineInnerInterceptor类的构造方法中加入一行 super.setTenantLineHandler(tenantLineHandler); 否则更新会报错!!!

一、多租户实现

基于MyBatis plus官方提供的多租户插件可以很容易的实现多租户数据隔离功能,只需要在需要数据隔离的表结构中加上相应的租户字段即可,简单实现如下文:

使用@Configuration定义一个MybatisPlusConfig配置类,随后定义MybatisPlusInterceptor bean由Spring管理,在bean中实现分页插件以及多租户插件,需要注意需要先定义多租户拦截器,随后定义分页插件,这样不会出现只有select多租户隔离生效,分页不生效的问题

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                return new LongValue("租户ID");
            }


            @Override
            public String getTenantIdColumn() {
                return "industry_chain_id";
            }

            @Override
            public boolean ignoreTable(String tableName) {
                List<String> list = new ArrayList<>();
                list.add("ods_whs_zsj_wh_corp_enterp_indi_bsc_info");
                list.add("t_talent");
                return list.stream().noneMatch(s -> s.equals(tableName));
            }
        }));
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

上述代码为简单实现多租户数据隔离功能,在getTenantId方法中获取当前请求的租户ID,如果是使用Spring Security的可以定义的Context中,如果是其他的可以在filter中编写代码存储到ThreadLocal中,在请求结束后清除,做法很多都可以实现获取当前请求的租户ID;getTenantIdColumn是获取租户列名的,需要在数据库需要隔离的表中加上租户字段,我这里是industry_chain_id;ignoreTable用于判定是否忽略多租户查询条件,比如一些系统表不需要多租户过滤,只给超管用,那么在这边可以进行表名的判断,返回为true即表示忽略多租户查询条件。

二、多租户的特殊需求

当我在用上述方法实现了多租户数据隔离后,某一天客户突然找到我,说需要支持某条数据同时存在于多个租户中,也就是说A用户既可能在A企业中也可能在B企业中,所以要求当租户ID为A的时候或者为B的时候,A用户都可以被查出来,而表中实现的方式是基于逗号分割,比如A用户的industry_chain_id存储的数据为A,B,这样该如何实现呢?
尝试

首先看到TenantLineHandler类中getTenantId方法是获取租户ID的方法,随后在打印的SQL中看到SQL为select * from t_user where industry_chain_id = ?,当时理解为我写的new LongValue拼接字段后面是=相连,于是想,结合我表中存的租户字段,使用like不就可以了吗,既然getTenantId是定义多租户值的,那是不是可以在这边下功夫呢?于是我点进Expression类,尝试找出符合需求的实现类,于是找到了LikeExpression
Expression接口类
点进LikeExpression类后发现能够set值的字段不多,LikeExpression实现类
看了半天也就看懂一个withLeftExpression与withRightExpression可能是我们需要用到的,于是我改掉getTenantId方法为
@Override public Expression getTenantId() { // 使用 LikeExpression 构建租户条件 LikeExpression likeExpression = new LikeExpression(); likeExpression.withRightExpression(new LongValue(SecurityUser.getIndustryChainId())); return likeExpression; }
尝试在右操作数设置租户值实现like查询,随后打印SQL发现执行语句为select * from t_user where industry_chain_id = '' like ? 为什么=始终存在呢,随后我又尝试了很多方法比如设置左操作数、给右操作数拼接%%,但是都没有作用

无法实现,分析源码

首先我们看到,MyBatis plus的多租户功能是add一个TenantLineInnerInterceptor到MybatisPlusInterceptor,我们点进TenantLineInnerInterceptor中发现类里面自己维护了一个TenantLineHandler,而我们实现的时候也是给TenantLineInnerInterceptor类一个自己重写后的TenantLineHandler类,但是很明显只操作TenantLineHandler类无法实现我们的需求,于是阅读代码,发现一个方法TenantLineInnerInterceptor类
很明显是在这个类中实现的一些SQL执行前的拼接,而从源码中我们看到buildTableExpression方法,这个方法只有一行,而且调用了我们重写的tenantLineHandler类中的方法,那我想这个一定是拼接SQL的关键,阅读代码我们发现,判断当前SQL的表名是否存在于ignoreTable的忽略清单中,如果忽略,则不构建表达式,返回NULL,否则返回new EqualsTo类,从名称中我们可以看出这个类一定跟相等有关系,观察它的两个参数,第一个参数为获取字段别名并拼接的一些方法,里面也获取到了我们的getTenantIdColumn字段,目前来看只是设置SQL的字段名的,跟我们的需求没有关系;看第二个参数,第二个参数是获取当前请求的租户值,所以大概可以推测出这个表达式构建的是"字段名=值"的SQL,点进EqualsTo类后发现果然是实现的Expression类,那么如何解决就知道了,在buildTableExpression方法中下功夫

最终实现

首先我们可以看到TenantLineInnerInterceptor的buildTableExpression方法是允许被重写的,于是我们定义一个类,继承TenantLineInnerInterceptor,我们只需要重写buildTableExpression方法,其他的仍然由MyBatis plus进行操作,而我们仍然是需要获取字段名的,所以copy一份getAliasColumn方法到我们自定义的拦截器类中,而buildTableExpression方法全部重写,

@Override
    public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
        if (this.tenantLineHandler.ignoreTable(table.getName())) {
            return null;
        }
        Expression expression = this.tenantLineHandler.getTenantId();
        LikeExpression likeExpression = null;
        if (expression instanceof LikeExpression) {
            likeExpression = (LikeExpression) expression;
            likeExpression.withLeftExpression(this.getAliasColumn(table));
        }
        return likeExpression;
    }

首先我们需要仍然按照原先方法中的来,第一步忽略不需要添加多租户过滤条件的表,返回NULL,第二步获取tenantLineHandler传过来的Expression类,也就是getTenantId方法,因为我们只需要实现基于like的模糊查询,所以我判断Expression是否是LikeExpression类?如果是的话,强制转换为LikeExpression,随后设置左操作数为我们的租户列名,而右操作数在原本传过来的Expression方法中存在,所以不需要再设置,随后返回Expression类给MyBatis plus,其他操作不重写,保持原样。随后也重写了tenantLineHandler类,代码如下:

public class MyTenantLineHandler implements TenantLineHandler {
    @Override
    public Expression getTenantId() {
        LikeExpression likeExpression = new LikeExpression();
        likeExpression.withRightExpression(new StringValue("%" + SecurityUser.getIndustryChainId() + "%"));
        return likeExpression;
    }

    @Override
    public String getTenantIdColumn() {
        return "industry_chain_id";
    }

    /**
     * 这个地方需要排除没有这个权限字段的表,如果是嵌套或者连表,都会添加上这个多租户
     *
     * @param tableName
     * @return
     */
    @Override
    public boolean ignoreTable(String tableName) {
        List<String> list = new ArrayList<>();
        list.add("ods_whs_zsj_wh_corp_enterp_indi_bsc_info");
        list.add("t_talent");
        return list.stream().noneMatch(s -> s.equals(tableName));
    }
    public String getUserName() {
        return SecurityUser.getManager().getUsername();
    }
}

仍然是跟原先MybatisPlusInterceptor类中重写的方法一致,只不过在getTenantId方法中我们构建了右操作数为我们的租户值,在前后加上了%%实现任意模糊查询,在我们自定义的MyTenantLineInnerInterceptor类中会给Expression构建左操作数即列名,MybatisPlusInterceptor类中改为下方代码即可。

        interceptor.addInnerInterceptor(new MyTenantLineInnerInterceptor(new MyTenantLineHandler()));

最后奉上代码:

public class MyTenantLineInnerInterceptor extends TenantLineInnerInterceptor {
    private MyTenantLineHandler tenantLineHandler;

    public TenantLineHandler getTenantLineHandler() {
        return this.tenantLineHandler;
    }

    public void setTenantLineHandler(final MyTenantLineHandler tenantLineHandler) {
        this.tenantLineHandler = tenantLineHandler;
    }

    public MyTenantLineInnerInterceptor() {
    }

    public MyTenantLineInnerInterceptor(final MyTenantLineHandler tenantLineHandler) {
        super.setTenantLineHandler(tenantLineHandler);
        this.tenantLineHandler = tenantLineHandler;
    }

    @Override
    public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
        if (this.tenantLineHandler.ignoreTable(table.getName())) {
            return null;
        }
        Expression expression = this.tenantLineHandler.getTenantId();
        LikeExpression likeExpression = null;
        if (expression instanceof LikeExpression) {
            likeExpression = (LikeExpression) expression;
            likeExpression.withLeftExpression(this.getAliasColumn(table));
        }
        return likeExpression;
    }
    protected Column getAliasColumn(Table table) {
        StringBuilder column = new StringBuilder();
        if (table.getAlias() != null) {
            column.append(table.getAlias().getName()).append(".");
        }

        column.append(this.tenantLineHandler.getTenantIdColumn());
        return new Column(column.toString());
    }
}
public class MyTenantLineHandler implements TenantLineHandler {
    @Override
    public Expression getTenantId() {
        LikeExpression likeExpression = new LikeExpression();
        likeExpression.withRightExpression(new StringValue("%" + SecurityUser.getIndustryChainId() + "%"));
        return likeExpression;
    }

    @Override
    public String getTenantIdColumn() {
        return "industry_chain_id";
    }

    /**
     * 这个地方需要排除没有这个权限字段的表,如果是嵌套或者连表,都会添加上这个多租户
     *
     * @param tableName
     * @return
     */
    @Override
    public boolean ignoreTable(String tableName) {
        List<String> list = new ArrayList<>();
        list.add("ods_whs_zsj_wh_corp_enterp_indi_bsc_info");
        list.add("t_talent");
        return list.stream().noneMatch(s -> s.equals(tableName));
    }
    public String getUserName() {
        return SecurityUser.getManager().getUsername();
    }
}

三、最终结论

从上述过程中可以了解到MyBatis plus多租户插件的工作原理,也实现了重写buildTableExpression来构建我们需要的多租户查询语句,如果想要实现当前租户为超管则不添加多租户过滤条件也是可以的,同样在buildTableExpression方法中,判断当前用户是否为超管即可,为true则返回NULL,如果某些方法需要忽略多租户但是表不需要,则在mapper方法上加@InterceptorIgnore(tenantLine = "true")注解即可。

有任何问题或者优化方案欢迎指正,只是分享一下我个人的实现过程以及源码分析思路

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值
>