TenantLineInnerInterceptor源码解读
一、引言
TenantLineInnerInterceptor是MyBatis-Plus中的一个拦截器类,位于com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor,通过MyBatis-Plus的插件机制调用,用于实现表级的多租户功能。
本文基于MyBatis-Plus的3.5.9版本的源码,并fork了代码: https://github.com/changelzj/mybatis-plus/tree/lzj-3.5.9
public class TenantLineInnerInterceptor
extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
private TenantLineHandler tenantLineHandler;
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {...}
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {...}
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {...}
@Override
protected void processInsert(Insert insert, int index, String sql, Object obj) {...}
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {...}
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {...}
protected void processInsertSelect(Select selectBody, final String whereSegment) {...}
protected void appendSelectItem(List<SelectItem<?>> selectItems) {...}
protected Column getAliasColumn(Table table) {...}
@Override
public void setProperties(Properties properties) {...}
@Override
public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {...}
}
多租户和数据权限DataPermissionInterceptor的实现原理是类似的,租户本质上也是一种特殊的数据权限,不同于数据权限的是对于涉及租户的表的增、删、改、查四种操作,都需要对SQL语句进行处理,实现原理是执行SQL前进行拦截,并获取要执行的SQL,然后解析SQL语句中的表,遇到需要租户隔离的表就要进行处理,对于查询、删除和更新的场景,就在现有的SQL条件中追加一个tenant_id = ?
的条件,获取当前操作的用户或要执行的某种任务所属的租户ID赋值给tenant_id
,对于添加操作,则是将tenant_id
字段加入到INSERT列表中并赋值。
TenantLineInnerInterceptor类也像数据权限插件一样继承了用于解析和追加条件的BaseMultiTableInnerInterceptor类,但是BaseMultiTableInnerInterceptor主要是提供了对查询SQL的解析重写能力供插件类使用,本类对于添加数据的场景采用自己实现的解析和重写INSERT SQL的逻辑。
TenantLineInnerInterceptor需要一个TenantLineHandler类型的租户处理器,TenantLineHandler是一个接口,用于给TenantLineInnerInterceptor判断某个表是否需要租户隔离,以及获取租户ID值表达式、租户字段名以及要执行的SQL的列中如果已经包含租户ID字段是否继续,我们使用MyBatis-Plus的租户插件时,需要实现这个接口并在回调方法中将这些信息封装好后返回。
com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler
public interface TenantLineHandler {
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
Expression getTenantId();
/**
* 获取租户字段名
* <p>
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
default String getTenantIdColumn() {
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));
}
}
二、主要源码解读
本文指定租户ID为1001,对各种结构的INSERT SQL解析重写过程进行解读
TenantLineHandler handler = new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(1001);
}
};
2.1 beforeQuery/beforePrepare
逻辑和DataPermissionInterceptor中的实现基本一致,唯一不同的是,租户的实现需要对INSERT类型的SQL进行解析重写。
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), null));
}
@Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
MappedStatement ms = mpSh.mappedStatement();
SqlCommandType sct = ms.getSqlCommandType();
if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), null));
}
}
2.2 processSelect
对SELECT语句的解析和重写,已经在父类BaseMultiTableInnerInterceptor中实现
@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
final String whereSegment = (String) obj;
processSelectBody(select, whereSegment);
List<WithItem> withItemsList = select.getWithItemsList();
if (!CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(withItem -> processSelectBody(withItem, whereSegment));
}
}
2.3 processInsert
该方法是本类中一个很重要的方法,用于对INSERT语句进行解析和重写以实现租户隔离。
@Override
protected void processInsert(Insert insert, int index, String sql, Object obj) {
if (tenantLineHandler.ignoreTable(insert.getTable().getName())) {
// 过滤退出执行
return;
}
List<Column> columns = insert.getColumns();
if (CollectionUtils.isEmpty(columns)) {
// 针对不给列名的insert 不处理
return;
}
String tenantIdColumn = tenantLineHandler.getTenantIdColumn();
if (tenantLineHandler.ignoreInsert(columns, tenantIdColumn)) {
// 针对已给出租户列的insert 不处理
return;
}
columns.add(new Column(tenantIdColumn));
Expression tenantId = tenantLineHandler.getTenantId();
// fixed gitee pulls/141 duplicate update
List<UpdateSet> duplicateUpdateColumns = insert.getDuplicateUpdateSets();
if (CollectionUtils.isNotEmpty(duplicateUpdateColumns)) {
EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new StringValue(tenantIdColumn));
equalsTo.setRightExpression(tenantId);
duplicateUpdateColumns.add(new UpdateSet(new Column(tenantIdColumn), tenantId));
}
Select select = insert.getSelect();
if (select instanceof PlainSelect) { //fix github issue 4998 修复升级到4.5版本的问题
this.processInsertSelect(select, (String) obj);
} else if (insert.getValues() != null) {
// fixed github pull/295
Values values = insert.getValues();
ExpressionList<Expression> expressions = (ExpressionList<Expression>) values.getExpressions();
if (expressions instanceof ParenthesedExpressionList) {
expressions.addExpression(tenantId);
} else {
if (CollectionUtils.isNotEmpty(expressions)) {//fix github issue 4998 jsqlparse 4.5 批量insert ItemsList不是MultiExpressionList 了,需要特殊处理
int len = expressions.size();
for (int i = 0; i < len; i++) {
Expression expression = expressions.get(i);
if (expression instanceof Parenthesis) {
ExpressionList rowConstructor = new RowConstructor<>()
.withExpressions(new ExpressionList<>(((Parenthesis) expression).getExpression(), tenantId));
expressions.set(i, rowConstructor);
} else if (expression instanceof ParenthesedExpressionList) {
((ParenthesedExpressionList) expression).addExpression(tenantId);
} else {
expressions.add(tenantId);
}
}
} else {
expressions.add(tenantId);
}
}
} else {
throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
}
}
首先判断if (CollectionUtils.isEmpty(columns))
:如SQL没有指明要更新的列,则不处理
然后判断if (tenantLineHandler.ignoreInsert(columns, tenantIdColumn))
,如要执行的SQL中已经包含租户ID字段,则可能是已经明确指定了具体的租户ID,同样不处理
然后调用tenantLineHandler
的getTenantIdColumn()
获取租户列的字段名,先把租户的字段名添加到INSERT INTO
后面原有的字段名的最后
之后针对不同结构的SQL,会分别走到不同的分支,针对几种常见的INSERT SQL,分别进行解读:
2.3.1 最常见的新增SQL语句
insert into t_user (name, age) values ('liming', 15)
首先会尝试获取INSERT语句中的查询结构Select select = insert.getSelect()
,并判断是否带有查询结构,这种情况是不带查询结构的,会走到else if (insert.getValues() != null)
这个分支,然后insert.getValues()
获取代表一组值的对象values
紧接着获取values
的结构ExpressionList<Expression> expressions = (ExpressionList<Expression>) values.getExpressions()
得到('liming', 15)
然后,通过if (expressions instanceof ParenthesedExpressionList)
判断是否为带着括号的Expression结构,很显然是,通过expressions.addExpression(tenantId);
将租户ID的值追加到('liming', 15)
的最后,得到SQL:
INSERT INTO t_user (name, age, tenant_id) VALUES ('liming', 15, 1001)
2.3.2 批量新增数据的SQL语句
insert into t_user (name, age) values ('liming', 15), ('zhaoying', 16)
与2.3.1不同的是,这种SQL在通过if (expressions instanceof ParenthesedExpressionList)
判断是否为带着括号的Expression结构时结果为false,因为这种SQL的VALUES
部分结构是('liming', 15), ('zhaoying', 16)
显然不符合,因此会走到else
分支,分别取出其中每个元素(...)
,再去判断每个元素是否为带着括号的Expression结构,显然每个(...)
都符合,因此对每个(...)
中最后一个值后面再追加上租户ID即可,相当于将大的拆散分别处理,最终得到SQL:
INSERT INTO t_user (name, age, tenant_id)
VALUES ('liming', 15, 1001), ('zhaoying', 16, 1001)
2.3.3 ON DUPLICATE KEY UPDATE的SQL
INSERT INTO table_name (col1, col2)
VALUES (val1, val2)
ON DUPLICATE KEY UPDATE col1 = val3, col2 = col4 + 1;
这种SQL,在if (CollectionUtils.isNotEmpty(duplicateUpdateColumns))
处为true,属于添加发生冲突时对冲突的字段进行更新的SQL结构,会先进入这个if分支处理ON DUPLICATE
的部分,意思是如果insert.getDuplicateUpdateSets()
不为空,则会先将tenant_id = 1001
追加到ON DUPLICATE KEY UPDATE
后面,再后面的VALUES (val1, val2, 1001)
的结构和2.3.1处理方式相同
INSERT INTO table_name (col1, col2, tenant_id)
VALUES (val1, val2, 1001)
ON DUPLICATE KEY UPDATE col1 = val3, col2 = col4 + 1, tenant_id = 1001
2.3.4 INSERT SELECT的SQL
INSERT INTO table_name (col1, col2) SELECT col1, col2 FROM another_table
与2.3.1情况相反,这种情况是带查询结构的,这种SQL要添加的值在一个查询结果集中,该方法在获取查询结构Select select = insert.getSelect()
并判断是否带有查询结构时,就会走到if (select instanceof PlainSelect)
中,调用processInsertSelect()
方法并将SQL上获取到的Select结构传入,对SQL中的查询结构进行处理,processInsertSelect方法解读详见2.6,最终得到SQL:
INSERT INTO table_name (col1, col2, tenant_id)
SELECT col1, col2, tenant_id FROM another_table WHERE tenant_id = 1001
2.3.5 SELECT INTO的结构
SELECT col1,col2 INTO table_name2 FROM table_name1
这种会被当成select语句进行处理
2.4 processUpdate
该方法用于解析重写update语句,针对租户的processUpdate方法和数据权限的实现类似但也有区别
/**
* update 语句处理
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
final Table table = update.getTable();
if (tenantLineHandler.ignoreTable(table.getName())) {
// 过滤退出执行
return;
}
List<UpdateSet> sets = update.getUpdateSets();
if (!CollectionUtils.isEmpty(sets)) {
sets.forEach(us -> us.getValues().forEach(ex -> {
if (ex instanceof Select) {
processSelectBody(((Select) ex), (String) obj);
}
}));
}
update.setWhere(this.andExpression(table, update.getWhere(), (String) obj));
}
用于解析和重写update语句的租户逻辑,对于常规的update语句处理较为简单,直接在where后面追加租户过滤条件:update.setWhere(this.andExpression(table, update.getWhere(), (String) obj))
,例如:
UPDATE user SET username = 5 WHERE id = 1
重写后:
UPDATE user SET username = 5 WHERE id = 1 AND tenant_id = 1001
和数据权限拦截器插件的实现不同的是,多租户对于update语句更新后的值是子查询的情况进行了额外处理,对子查询SQL也进行了解析和重写,通过sets.forEach(us -> us.getValues().forEach(ex -> {
获取所有要更新的值并遍历,如果某个值属于子查询结构(ex instanceof Select
)则处理子查询,例如:
UPDATE user
SET username = (SELECT name FROM employee WHERE emp_no = 'UA001')
WHERE id = 1
重写后:
UPDATE user
SET username = (SELECT name FROM employee WHERE emp_no = 'UA001' AND tenant_id = 1001)
WHERE id = 1 AND tenant_id = 1001
2.5 processDelete
删除语句,处理较为简单,处理方式类似简单的update语句,直接追加过滤条件在where
后面即可
/**
* delete 语句处理
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
if (tenantLineHandler.ignoreTable(delete.getTable().getName())) {
// 过滤退出执行
return;
}
delete.setWhere(this.andExpression(delete.getTable(), delete.getWhere(), (String) obj));
}
2.6 processInsertSelect
该方法用于对INSERT...SELECT...
结构后面的SELECT部分进行处理
/**
* 处理 insert into select
* <p>
* 进入这里表示需要 insert 的表启用了多租户,则 select 的表都启动了
*
* @param selectBody SelectBody
*/
protected void processInsertSelect(Select selectBody, final String whereSegment) {
if(selectBody instanceof PlainSelect){
PlainSelect plainSelect = (PlainSelect) selectBody;
FromItem fromItem = plainSelect.getFromItem();
if (fromItem instanceof Table) {
// fixed gitee pulls/141 duplicate update
processPlainSelect(plainSelect, whereSegment);
appendSelectItem(plainSelect.getSelectItems());
} else if (fromItem instanceof Select) {
Select subSelect = (Select) fromItem;
appendSelectItem(plainSelect.getSelectItems());
processInsertSelect(subSelect, whereSegment);
}
} else if(selectBody instanceof ParenthesedSelect){
ParenthesedSelect parenthesedSelect = (ParenthesedSelect) selectBody;
processInsertSelect(parenthesedSelect.getSelect(), whereSegment);
}
}
解读:
1.表:if (fromItem instanceof Table)
针对的是SELECT部分查询的是表的情况
INSERT INTO table_name (col1, col2) SELECT col1, col2 FROM another_table
直接调用父类processPlainSelect
对表where条件追加租户过滤条件,再将租户ID字段名添加到查询字段名列表中即可,得到如下SQL:
INSERT INTO table_name (col1, col2, tenant_id)
SELECT col1, col2, tenant_id FROM another_table WHERE tenant_id = 1001
2.子查询:else if (fromItem instanceof Select)
针对的是SELECT部分查询的是子查询的情况
INSERT INTO table_name (col1, col2)
SELECT col1, col2 FROM (select col1, col2 from another_table) t
先appendSelectItem()
将租户ID字段名添加到查询字段名列表中,然后获取子查询再递归调用当前processInsertSelect
方法,如果子查询中查询的是表,则将租户ID字段名添加到子查询的字段名列表中然后追加租户过滤条件在子查询的where条件上,如果子查询中的查询来源还是子查询,则继续递归解析,最终会得到如下SQL:
INSERT INTO table_name (col1, col2, tenant_id)
SELECT col1, col2, tenant_id FROM (
SELECT col1, col2, tenant_id FROM another_table WHERE tenant_id = 1001
) t
2.7 appendSelectItem
该方法配合processInsertSelect使用,用于将租户ID字段名插入到select后的字段名列表中,使得结果集可以直接作为要添加的值进行批量insert,如果select的字段是模糊的select *
表示的,则不处理,直接跳过
/**
* 追加 SelectItem
*
* @param selectItems SelectItem
*/
protected void appendSelectItem(List<SelectItem<?>> selectItems) {
if (CollectionUtils.isEmpty(selectItems)) {
return;
}
if (selectItems.size() == 1) {
SelectItem item = selectItems.get(0);
Expression expression = item.getExpression();
if (expression instanceof AllColumns) {
return;
}
}
selectItems.add(new SelectItem<>(new Column(tenantLineHandler.getTenantIdColumn())));
}
结束语
该类是MyBatis-Plus的多租户插件实现源码,基本上和数据权限插件的实现逻辑类似,本质上讲租户也是一种特殊的数据权限,根据租户的业务逻辑,本类针对INSERT SQL的解析和重写进行了实现,并对UPDATE SQL做了和数据权限插件不一样的处理:针对更新后的值是子查询的情况也对子查询SQL进行了租户隔离。
原文首发:https://blog.liuzijian.com/post/mybatis-plus-source-tenant-line-inner-interceptor.html
TenantLineInnerInterceptor源码解读的更多相关文章
- SDWebImage源码解读之SDWebImageDownloaderOperation
第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...
- SDWebImage源码解读 之 NSData+ImageContentType
第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...
- SDWebImage源码解读 之 UIImage+GIF
第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...
- SDWebImage源码解读 之 SDWebImageCompat
第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...
- SDWebImage源码解读_之SDWebImageDecoder
第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...
- SDWebImage源码解读之SDWebImageCache(上)
第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...
- SDWebImage源码解读之SDWebImageCache(下)
第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...
- AFNetworking 3.0 源码解读 总结(干货)(下)
承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...
- AFNetworking 3.0 源码解读 总结(干货)(上)
养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要.记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点.它就像时间一样,会自己消散. 前言 终于写完了 AFNetwo ...
- AFNetworking 3.0 源码解读(十一)之 UIButton/UIProgressView/UIWebView + AFNetworking
AFNetworking的源码解读马上就结束了,这一篇应该算是倒数第二篇,下一篇会是对AFNetworking中的技术点进行总结. 前言 上一篇我们总结了 UIActivityIndicatorVie ...
随机推荐
- 【BUUCTF】Easy Java
[BUUCTF]Easy Java 题目来源 收录于:BUUCTF RoarCTF 2019 题目描述 经典登录框 不过SQL注入.目录扫描都没有发现 题解 点击页面的 help 跳转到/Downlo ...
- Opera打不开网页解决办法
打开目录C:\Users\用户名\AppData\Roaming\Opera Software\Opera Stable 2.查找{"country":"CN" ...
- 「一」nginx介绍
应用场景 静态资源(js.css.图片 ) 反向代理 缓存加速(动态资源),比如社区活跃度排名 负载均衡(动态扩容.容灾) API服务 一个请求先经过nginx,再到应用服务器,访问数据库/redis ...
- Redis主从、哨兵
之前安装了redis,但是单节点redis不可靠,现在搭建redis主从,提高可用性. 一.搭建一主二从 redis主从其实是读写分离,主节点写数据,从节点读数据. 1.准备三台redis机器.red ...
- MySQL时间溢出原理、实战影响与全面解决方案
一.问题背景与现象复现 操作场景: 本文将手把手带您了解mysql时间溢出原理.实战影响与全面解决方案,所有代码均通过dblens for mysql数据库工具验证,推荐使用该工具进行可视化数据库管理 ...
- Navicat 如何将表恢复默认状态下
场景: 测试一套流程后,造测试数据非常麻烦的情况下,如何通过更改数据库为默认情况即初始表数据 案例: 比如表原有结构如下图(一) 修改后数据如下图(二): 需求:将图二数据恢复到图一内容下 操作思想: ...
- docker login harbor http login登录
前言 搭建的 harbor 仓库为 http 协议,在本地登录时出现如下报错: docker login http://192.168.xx.xx Username: admin Password: ...
- 【FAQ】HarmonyOS SDK 闭源开放能力 —Map Kit(6)
1.问题描述: 使用华为内置的MapComponent, 发现显示不出来.查看日志, MapRender底层有报错. 解决方案: 麻烦按以下步骤检查下地图服务,特别是签名证书指纹那部分. 1.一般没有 ...
- Oracle锁表及解锁方法
1. 首先查看数据库中哪些表被锁了,找到session ID: 使用sql: select b.owner,b.object_name,a.session_id,a.locked_modefrom v ...
- docker网络冲突解决(修改docker_gwbridge网段)
1·问题 一次生产搭建服务的时候,出现客户端服务器到docker服务断开不通的情况,在docker服务器上抓包可以抓到客户端服务器的包,但是docker服务器不做任何响应 于是ip route 查看本 ...