当前位置: 首页 > news >正文

mybatis-plus多租户兼容多字段租户标识

默认租户插件处理器的缺陷

在springboot工程中引入mybatis-plus的租户插件TenantLineInnerInterceptor,能简化我们的数据隔离操作,例如各类含租户用户登录权限的rest接口中,不需要再根据登录用户-set租户条件-触发查询,租户插件能帮我们省略手动插入条件的繁琐过程。

mybatis-plus默认仅支持单个字段的租户条件,实际使用场景中,我们的“租户”在系统中,可能是一个多类型的数据概念,例如菜单大类模块一可能一种用户能访问,菜单大类模块二是另一种用户能访问,两种模块用户权限都在同一管理菜单、权限、用户中进行配置,即用户区分类型,不同类型租户id属性来源不同。由于不同大类模块字段可能不一致,即“TenantId”在不同表中,是不同的字段名称,这时候使用原始的租户插件接口,就满足不了需求了。

public interface TenantLineHandler {/*** 获取租户 ID 值表达式,只支持单个 ID 值** @return 租户 ID 值表达式*/Expression getTenantId();/*** 获取租户字段名* 默认字段名叫: tenant_id** @return 租户字段名*/default String getTenantIdColumn() {return "tenant_id";}/*** 根据表名判断是否忽略拼接多租户条件* 默认都要进行解析并拼接多租户条件** @param tableName 表名* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件*/default boolean ignoreTable(String tableName) {return false;}/*** 忽略插入租户字段逻辑** @param columns        插入字段* @param tenantIdColumn 租户 ID 字段* @return*/default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {return columns.stream().map(Column::getColumnName).anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));}
}

上述接口中getTenantId和getTenantIdColumn是唯一的,无法区分不同表不同租户字段。

针对多字段条件租户插件的实现

改造步骤如下:

1、定义一个新的租户数据行处理器

如这里命名FixTenantLineHandler,

仅更新getTenantId和getTenantIdColumn方法,方便根据表名来判断取什么租户字段。

package com.infypower.vpp.security.permission;import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.schema.Column;import java.util.List;/*** 租户处理器( TenantId 行级 )** @author endcy* @see com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler*/
public interface FixTenantLineHandler {/*** 获取租户 ID 值表达式,只支持单个 ID 值* <p>** @return 租户 ID 值表达式*/Expression getTenantId(String tableName);/*** 获取租户字段名* <p>* 默认字段名叫: tenant_id** @return 租户字段名*/default String getTenantIdColumn(String tableName) {return "tenant_id";}/*** 根据表名判断是否忽略拼接多租户条件* <p>* 默认都要进行解析并拼接多租户条件** @param tableName 表名* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件*/default boolean ignoreTable(String tableName) {return false;}/*** 忽略插入租户字段逻辑** @param columns        插入字段* @param tenantIdColumn 租户 ID 字段* @return*/default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {return columns.stream().map(Column::getColumnName).anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));}
}

2、实现自定义TenantLineHandler

下列TenantSecurityUtils是用户登录时注入的requestScope或者ThreadLocal存储的用户信息,包含不同租户类型信息及ID、管理员租户数据授权配置信息等,可自定义实现。

hasProperty方法会取mybatis-plus缓存的表信息,根据表名判断表映射属性是否包含不同租户id字段,这样做的好处是无需额外编码处理不同表对应不同属性名称配置判断,直接使用mybatis-plus原有的TableInfoHelper相关功能。

import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.gitee.coadmin.enums.UserIdentityTypeEnum;
import com.gitee.coadmin.utils.TenantSecurityUtils;
import com.infypower.vpp.common.CesConstant;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import org.springframework.stereotype.Component;import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;/*** ...** @author endcy* @date 2025/9/9*/
@Slf4j
@Component
public class FixTenantLineInnerInterceptor implements FixTenantLineHandler {private static final String TENANT_ID1_COLUMN = "tenant_id1";private static final String TENANT_ID1_NAME = "tenantId1";private static final String TENANT_ID2_COLUMN = "tenant_id2";private static final String TENANT_ID2_NAME = "tenantId2";private static final String TENANT_ID3_COLUMN = "tenant_id3";private static final String TENANT_ID3_NAME = "tenantId3";//存在租户id 字段但忽略租户过滤的数据表 逐行加private final List<String> IGNORE_TABLES = CollUtil.newArrayList("");@Overridepublic Expression getTenantId(String tableName) {UserIdentityTypeEnum identityType = TenantSecurityUtils.getCurrentUserIdentityType();//如果是平台权限用户 可能有需要过滤租户类型1数据if (identityType == UserIdentityTypeEnum.PLATFORM) {Set<Long> tenantIds = TenantSecurityUtils.getCurrentPlatformUserTenantId1List();if (CollUtil.isEmpty(tenantIds)) {//默认返回平台管理权限return new LongValue(0L);}List<Expression> valueList = tenantIds.stream().map(LongValue::new).collect(Collectors.toList());return new InExpression(new Column(getTenantIdColumn(tableName)),new ExpressionList(valueList));}return new LongValue(TenantSecurityUtils.getCurrentUserMajorIdentityId());}@Overridepublic String getTenantIdColumn(String tableName) {String tenantColumn = TENANT_ID1_COLUMN;//平台理员绑定了特定租户权限if (hasProperty(tableName, TENANT_ID1_NAME) && hasProperty(tableName, TENANT_ID2_NAME)) {tenantColumn = TENANT_ID1_COLUMN;if (TenantSecurityUtils.getCurrentUserIdentityType() == UserIdentityTypeEnum.TENANT1) {tenantColumn = TENANT_ID2_COLUMN;}} else if (hasProperty(tableName, TENANT_ID2_NAME)) {tenantColumn = TENANT_ID2_COLUMN;} else if (hasProperty(tableName, TENANT_ID3_NAME)) {tenantColumn = TENANT_ID3_COLUMN;}//默认返回主租户字段log.debug(">>>> table:{} tenantColumn:{}", tableName, tenantColumn);return tenantColumn;}@Overridepublic boolean ignoreTable(String tableName) {boolean ignore;//全数据管理员略过if (TenantSecurityUtils.getCurrentUserMajorIdentityId() == 0L&& CollUtil.isEmpty(TenantSecurityUtils.getCurrentPlatformUserTenantId1List())) {return true;}if (IGNORE_TABLES.contains(tableName)) {return true;}if (!hasProperty(tableName, TENANT_ID1_NAME)&& !hasProperty(tableName, TENANT_ID2_NAME)&& !hasProperty(tableName, TENANT_ID3_NAME)) {ignore = true;log.debug(">>>> ignore tenant data for table:{}", tableName);} else {ignore = false;}if (ignore) {IGNORE_TABLES.add(tableName);return true;}
//        ignore = checkIgnoreTable(tableName);return false;}private boolean checkIgnoreTable(String tableName) {if (IGNORE_TABLES.contains(tableName.toLowerCase())) {return true;}//其他校验逻辑待拓展return false;}public static boolean hasProperty(String tableName, String propertyName) {TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);if (tableInfo == null) {return false;}// 检查字段列表是否包含该属性return tableInfo.getFieldList().stream().anyMatch(field -> field.getProperty().equals(propertyName));}}

3、重写租户sql注入处理器

参考原TenantLineInnerInterceptor实现,使用tenantLineHandler,并替换需要传入表名的调用

package com.infypower.vpp.security.permission;import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.toolkit.PropertyMapper;
import lombok.*;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.*;
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 org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;/*** @author endcy* @since 2029/9/9* @see com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@SuppressWarnings({"rawtypes"})
public class FixTenantLineInnerInterceptor2 extends JsqlParserSupport implements InnerInterceptor {private FixTenantLineHandler tenantLineHandler;@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {……}@Overridepublic void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {……}@Overrideprotected void processSelect(Select select, int index, String sql, Object obj) {……}protected void processSelectBody(SelectBody selectBody) {……}@Overrideprotected void processInsert(Insert insert, int index, String sql, Object obj) {String tableName = insert.getTable().getName();if (tenantLineHandler.ignoreTable(tableName)) {// 过滤退出执行return;}List<Column> columns = insert.getColumns();if (CollectionUtils.isEmpty(columns)) {// 针对不给列名的insert 不处理return;}String tenantIdColumn = tenantLineHandler.getTenantIdColumn(tableName);if (tenantLineHandler.ignoreInsert(columns, tenantIdColumn)) {// 针对已给出租户列的insert 不处理return;}columns.add(new Column(tenantIdColumn));// fixed gitee pulls/141 duplicate updateList<Expression> duplicateUpdateColumns = insert.getDuplicateUpdateExpressionList();if (CollectionUtils.isNotEmpty(duplicateUpdateColumns)) {EqualsTo equalsTo = new EqualsTo();equalsTo.setLeftExpression(new StringValue(tenantIdColumn));equalsTo.setRightExpression(tenantLineHandler.getTenantId(tableName));duplicateUpdateColumns.add(equalsTo);}Select select = insert.getSelect();if (select != null) {this.processInsertSelect(tableName, select.getSelectBody());} else if (insert.getItemsList() != null) {// fixed github pull/295ItemsList itemsList = insert.getItemsList();if (itemsList instanceof MultiExpressionList) {((MultiExpressionList) itemsList).getExpressionLists().forEach(el -> el.getExpressions().add(tenantLineHandler.getTenantId(tableName)));} else {((ExpressionList) itemsList).getExpressions().add(tenantLineHandler.getTenantId(tableName));}} else {throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");}}/*** update 语句处理*/@Overrideprotected void processUpdate(Update update, int index, String sql, Object obj) {final Table table = update.getTable();if (tenantLineHandler.ignoreTable(table.getName())) {// 过滤退出执行return;}update.setWhere(this.andExpression(table, update.getWhere()));}/*** delete 语句处理*/@Overrideprotected void processDelete(Delete delete, int index, String sql, Object obj) {……}/*** delete update 语句 where 处理*/protected BinaryExpression andExpression(Table table, Expression where) {//获得where条件表达式EqualsTo equalsTo = new EqualsTo();equalsTo.setLeftExpression(this.getAliasColumn(table));equalsTo.setRightExpression(tenantLineHandler.getTenantId(table.getName()));if (null != where) {if (where instanceof OrExpression) {return new AndExpression(equalsTo, new Parenthesis(where));} else {return new AndExpression(equalsTo, where);}}return equalsTo;}/*** 处理 insert into select* <p>* 进入这里表示需要 insert 的表启用了多租户,则 select 的表都启动了** @param selectBody SelectBody*/protected void processInsertSelect(String tableName, SelectBody selectBody) {PlainSelect plainSelect = (PlainSelect) selectBody;FromItem fromItem = plainSelect.getFromItem();if (fromItem instanceof Table) {// fixed gitee pulls/141 duplicate updateprocessPlainSelect(plainSelect);appendSelectItem(tableName, plainSelect.getSelectItems());} else if (fromItem instanceof SubSelect) {SubSelect subSelect = (SubSelect) fromItem;appendSelectItem(tableName, plainSelect.getSelectItems());processInsertSelect(tableName, subSelect.getSelectBody());}}/*** 追加 SelectItem** @param selectItems SelectItem*/protected void appendSelectItem(String tableName, List<SelectItem> selectItems) {if (CollectionUtils.isEmpty(selectItems))return;if (selectItems.size() == 1) {SelectItem item = selectItems.get(0);if (item instanceof AllColumns || item instanceof AllTableColumns)return;}selectItems.add(new SelectExpressionItem(new Column(tenantLineHandler.getTenantIdColumn(tableName))));}/*** 处理 PlainSelect*/protected void processPlainSelect(PlainSelect plainSelect) {……}/*** 处理where条件内的子查询** @param where where 条件*/protected void processWhereSubSelect(Expression where) {……}protected void processSelectItem(SelectItem selectItem) {……}/*** 处理函数* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>* <p> fixed gitee pulls/141</p>** @param function*/protected void processFunction(Function function) {ExpressionList parameters = function.getParameters();if (parameters != null) {parameters.getExpressions().forEach(expression -> {if (expression instanceof SubSelect) {processSelectBody(((SubSelect) expression).getSelectBody());} else if (expression instanceof Function) {processFunction((Function) expression);}});}}/*** 处理子查询等*/protected void processFromItem(FromItem fromItem) {……}/*** 处理 joins** @param joins join 集合*/private void processJoins(List<Join> joins) {……}/*** 处理联接语句*/protected void processJoin(Join join) {……}/*** 处理条件*/protected Expression builderExpression(Expression currentExpression, Table table) {EqualsTo equalsTo = new EqualsTo();equalsTo.setLeftExpression(this.getAliasColumn(table));equalsTo.setRightExpression(tenantLineHandler.getTenantId(table.getName()));if (currentExpression == null) {return equalsTo;}if (currentExpression instanceof OrExpression) {return new AndExpression(new Parenthesis(currentExpression), equalsTo);} else {return new AndExpression(currentExpression, equalsTo);}}/*** 租户字段别名设置* <p>tenantId 或 tableAlias.tenantId</p>** @param table 表对象* @return 字段*/protected Column getAliasColumn(Table table) {StringBuilder column = new StringBuilder();if (table.getAlias() != null) {column.append(table.getAlias().getName()).append(StringPool.DOT);}column.append(tenantLineHandler.getTenantIdColumn(table.getName()));return new Column(column.toString());}@Overridepublic void setProperties(Properties properties) {PropertyMapper.newInstance(properties).whenNotBlank("tenantLineHandler",ClassUtils::newInstance, this::setTenantLineHandler);}
}

4、注入自定义租户插件

在mybatis-plus配置类中注入对应租户插件及其他数据权限插件等。

package com.infypower.vpp.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.infypower.vpp.security.permission.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** 配置文件* @author endcy* @since 2025/9/9*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class MybatisPlusConfig {private final FixTenantLineInnerInterceptor tenantLineInnerInterceptor;/*** admin模块MP插件* 多租户插件、数据权限插件*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 租户代理interceptor.addInnerInterceptor(new FixTenantLineInnerInterceptor(tenantLineInnerInterceptor));// 其他数据权限,根据用户配置进行数据权限控制DataPermissionInterceptor xxxPermissionInterceptor = new ResourceUserPermissionInterceptor();resourceUserPermissionInterceptor.setDataPermissionHandler(new XxxPermissionHandler());interceptor.addInnerInterceptor(xxxPermissionInterceptor);return interceptor;}}

其他配置保持不变。

上述getTenantId和getTenantIdColumn即核心实现,根据不同的用户类型,赋不同的租户字段标识和租户id信息。相比于网上其他多字段租户方案,本方案不依赖额外配置,不需要复杂解析,应该是最简洁且拓展最为便捷的方式。


文章转载自:

http://nKZZyhJs.Lpzqx.cn
http://u4FtTDxG.Lpzqx.cn
http://gNjl8rtI.Lpzqx.cn
http://xryafpIj.Lpzqx.cn
http://UPnvDzdC.Lpzqx.cn
http://bYBkkvi1.Lpzqx.cn
http://T1XYoIR8.Lpzqx.cn
http://1kAZS0Bj.Lpzqx.cn
http://mAPtsCGT.Lpzqx.cn
http://A8xwQ4Yd.Lpzqx.cn
http://FV57UPiy.Lpzqx.cn
http://3NClA4ns.Lpzqx.cn
http://n6sJE5Jv.Lpzqx.cn
http://tB9NLxZL.Lpzqx.cn
http://zYlge4l7.Lpzqx.cn
http://wRKTOor0.Lpzqx.cn
http://eCuNhcIz.Lpzqx.cn
http://H4wDcKAK.Lpzqx.cn
http://0ZhjU25T.Lpzqx.cn
http://8xtpyXST.Lpzqx.cn
http://mrEL09TP.Lpzqx.cn
http://gz8oAtod.Lpzqx.cn
http://8OkxrDT8.Lpzqx.cn
http://3lOnMso3.Lpzqx.cn
http://m4ICM2Od.Lpzqx.cn
http://LS0u4NwQ.Lpzqx.cn
http://l1sjD9CA.Lpzqx.cn
http://mIbJsI4Z.Lpzqx.cn
http://eP4LkZ2M.Lpzqx.cn
http://0BczPVMd.Lpzqx.cn
http://www.dtcms.com/a/374882.html

相关文章:

  • Flutter跨平台工程实践与原理透视:从渲染引擎到高质产物
  • 华为云盘同步、备份和自动上传功能三者如何区分
  • 设计模式第一章(建造者模式)
  • Vue3入门到实战,最新版vue3+TypeScript前端开发教程,笔记02
  • 【Vue】Vue2 与 Vue3 内置组件对比
  • XSS 跨站脚本攻击剖析与防御 - 第一章:XSS 初探
  • vue 去掉el-dropdown 悬浮时出现的边框
  • 常见的排序算法总结
  • [优化算法]神经网络结构搜索(一)
  • php 使用html 生成pdf word wkhtmltopdf 系列2
  • 大数据毕业设计选题推荐-基于大数据的海洋塑料污染数据分析与可视化系统-Hadoop-Spark-数据可视化-BigData
  • 【计算机网络 | 第11篇】宽带接入技术及其发展历程
  • 探索Java并发编程--从基础到高级实践技巧
  • Made in Green环保健康产品认证怎么做?
  • yum list 和 repoquery的区别
  • 解决HTML/JS开发中的常见问题与实用资源
  • Angular 面试题及详细答案
  • AI与AR融合:重塑石化与能源巡检的未来
  • 增强现实光学系统_FDTD_zemax_speos_学习(1)
  • 开学季干货——知识梳理与经验分享
  • Alex Codes团队并入OpenAI Codex:苹果生态或迎来AI编程新篇章
  • The learning process of Decision Tree Model|决策树模型学习过程
  • 六、与学习相关的技巧(下)
  • 《低功耗音频:重塑听觉体验与物联网边界的蓝牙革命》
  • 20250909的学习笔记
  • 金融量化指标--5Sortino索提诺比率
  • 消息三剑客华山论剑:Kafka vs RabbitMQ vs RocketMQ
  • 均值/方差/标注查介绍
  • 深入解析Guava RateLimiter限流机制
  • 开发中使用——鸿蒙子页面跳转到指定Tab页面