优秀的编程知识分享平台

网站首页 > 技术文章 正文

MybatisPlus对租户模式的支持(二)——重写TenantSqlParser

nanyue 2024-07-31 12:01:49 技术文章 7 ℃

前言

上期说到,假设我是系统管理员,想看到租户A数据的同时也能看到租户B的数据,这样就不能给系统管理员的账号设置租户id。

但不设置租户id的话,当系统管理员进行查询时,sql 的 where 条件会加上 ”tenant_id = null”,这个条件会导致管理员连 租户A 和 租户B 的数据都看不到。

我们可以在加上 ”tenant_id = ?” 条件前判断当前用户是否为系统管理员,如果是系统管理员,则直接返回,不处理为 sql 语句添加租户判断条件的代码逻辑。

通过查看 MybatisPlus 源码,得知处理租户条件的 sql 解析器位置如下:

com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser

那么我们可以参考和重写它,让 MybatisPlus 使用我们自定义的租户 sql 解析器。


具体操作

首先,在 MyTenantHandler 加一下 doUserFilter() 接口,用于判断当前用户是否为系统管理员。

package com.rjkj.quickboot.base.tenant;

import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;

public interface MyTenantHandler extends TenantHandler {
    // 判断当前用户是否为系统管理员
    boolean doUserFilter();

    void setTenantId(String tenantId);
}

然后,参考 TenantSqlParser 创建我们自定义的租户 sql 解析器,对 CRUD 的处理方法进行重写,代码可以直接复制,在方法开始前额外加上以下判断:

MyTenantHandler tenantHandler = (MyTenantHandler) getTenantHandler();
// 过滤系统管理员
if (tenantHandler.doUserFilter()){
    return;
}

整体代码如下:

package com.rjkj.quickboot.base.tenant;

import com.baomidou.mybatisplus.core.toolkit.Assert;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;

import javax.annotation.PostConstruct;
import java.util.List;

@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class MyTenantSqlParser extends TenantSqlParser {

    /**
     * select 语句处理
     */
    @Override
    public void processSelectBody(SelectBody selectBody) {
        MyTenantHandler tenantHandler = (MyTenantHandler) getTenantHandler();
        if (tenantHandler.doUserFilter()){
            // 过滤系统管理员
            return;
        }
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            if (withItem.getSelectBody() != null) {
                processSelectBody(withItem.getSelectBody());
            }
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            if (operationList.getSelects() != null && operationList.getSelects().size() > 0) {
                operationList.getSelects().forEach(this::processSelectBody);
            }
        }
    }

    /**
     * insert 语句处理
     */
    @Override
    public void processInsert(Insert insert) {
        MyTenantHandler tenantHandler = (MyTenantHandler) getTenantHandler();
        if (tenantHandler.doTableFilter(insert.getTable().getName())) {
            // 过滤退出执行
            return;
        }
        if (tenantHandler.doUserFilter()){
            // 过滤系统管理员
            return;
        }
        insert.getColumns().add(new Column(tenantHandler.getTenantIdColumn()));
        if (insert.getSelect() != null) {
            processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
        } else if (insert.getItemsList() != null) {
            // fixed github pull/295
            ItemsList itemsList = insert.getItemsList();
            if (itemsList instanceof MultiExpressionList) {
                ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(tenantHandler.getTenantId()));
            } else {
                ((ExpressionList) insert.getItemsList()).getExpressions().add(tenantHandler.getTenantId());
            }
        } else {
            throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
        }
    }

    /**
     * update 语句处理
     */
    @Override
    public void processUpdate(Update update) {
        MyTenantHandler tenantHandler = (MyTenantHandler) getTenantHandler();
        if (tenantHandler.doUserFilter()){
            // 过滤系统管理员
            return;
        }
        List<Table> tableList = update.getTables();
        Assert.isTrue(null != tableList && tableList.size() < 2,
                "Failed to process multiple-table update, please exclude the statementId");
        Table table = tableList.get(0);
        if (tenantHandler.doTableFilter(table.getName())) {
            // 过滤退出执行
            return;
        }
        update.setWhere(this.andExpression(table, update.getWhere()));
    }

    /**
     * delete 语句处理
     */
    @Override
    public void processDelete(Delete delete) {
        MyTenantHandler tenantHandler = (MyTenantHandler) getTenantHandler();
        if (tenantHandler.doUserFilter()){
            // 过滤系统管理员
            return;
        }
        if (tenantHandler.doTableFilter(delete.getTable().getName())) {
            // 过滤退出执行
            return;
        }
        delete.setWhere(this.andExpression(delete.getTable(), delete.getWhere()));
    }

}

同时对上期文章中的部分代码进行修改,主要是对处理类进行类型转换。(为了简单显示,只打印需要改动的代码部分)

MybatisPlusConfig.java

@Configuration
public class MybatisPlusConfig {

    ...

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 自定义租户sql解析器
        MyTenantSqlParser tenantSqlParser = new MyTenantSqlParser();
        // 自定义租户sql处理类
        tenantSqlParser.setTenantHandler(new MyTenantHandler() {
            @Override
            public boolean doUserFilter() {
                // 如果是系统管理员,它的租户id为null
                return ObjectUtil.isNull(getTenantId());
            }
            ...
        }
    }
    ...

TenantInterceptor.java

@Component
public class TenantInterceptor implements HandlerInterceptor {

    @Autowired
    private PaginationInterceptor pi;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        // 自定义租户sql解析器
        MyTenantSqlParser tenantSqlParser = (MyTenantSqlParser) pi.getSqlParserList().get(0);
        MyTenantHandler myTenantHandler = (MyTenantHandler) tenantSqlParser.getTenantHandler();

        //获取当前用户
        LoginUser currentUser = getCurrentUser();
        myTenantHandler.setTenantId(currentUser.getTenantId());
        return true;
    }
    ...
}


相比 Mybatis,Mybatis-Plus 的支持更加丰富,除了本文描述的对租户支持,还内置了各种插件,如性能分析、分页插件等,并且使用强大的条件构造器,足以替代手写 sql,满足大部分的查询需求。

下次有机会再继续研究 Mybatis-Plus 的其他的特性,希望本文对大家有所帮助。


以上就是本期分享,如果大家对此感兴趣,欢迎各位关注、留言,大家的支持就是我的动力!

最近发表
标签列表