# mybatis-plus-multi-tenancy **Repository Path**: wangchengyang/mybatis-plus-multi-tenancy ## Basic Information - **Project Name**: mybatis-plus-multi-tenancy - **Description**: mybatis plus 行多租户实现 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 6 - **Created**: 2023-02-02 - **Last Updated**: 2023-02-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## Mybatis plus 多租户 #### 介绍 官网: [简介 | MyBatis-Plus (baomidou.com)](https://baomidou.com/guide/) #### 官方多租户使用 mybatis plus 官方多租户使用方法,是通过实现TenantLineHandler 接口,实现tenantId的获取方法。 ```java 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;
}
}
```
使用时,在MybatisPlusInterceptor里使用配置上即可
```java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(1L); //最简单实现
}
}));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
```
#### 官方多租户实现原理
mybatis plus 官方实现是使用jsqlparser解析sql成语法树,在JsqlParserSupport抽象类中对sql类型的判断,然后进行不同的处理。
```java
/**
* 执行 SQL 解析
*
* @param statement JsqlParser Statement
* @return sql
*/
protected String processParser(Statement statement, int index, String sql, Object obj) {
if (logger.isDebugEnabled()) {
logger.debug("SQL to parse, SQL: " + sql);
}
if (statement instanceof Insert) {
this.processInsert((Insert) statement, index, sql, obj);
} else if (statement instanceof Select) {
this.processSelect((Select) statement, index, sql, obj);
} else if (statement instanceof Update) {
this.processUpdate((Update) statement, index, sql, obj);
} else if (statement instanceof Delete) {
this.processDelete((Delete) statement, index, sql, obj);
}
sql = statement.toString();
if (logger.isDebugEnabled()) {
logger.debug("parse the finished SQL: " + sql);
}
return sql;
}
```
在mybatis plus的 TenantLineInnerInterceptor 中 继承了JsqlParserSupport 并实现了4种sql的处理方法, 实现了InnerInterceptor 中的beforeQuery(查询) 和 beforePrepare(增加、删除、更新)。
#### 官方多租户缺陷
jsqlparser 对sql 要求比较严格,在 join 语句的处理上存在限制

官方文档中也对此做出了说明

#### 多租户sql处理 的优化
改用 druid 对sql 进行处理
##### 解析sql
自定义sql的解析方法
```java
public String parserSingle(String sql, Object obj) {
deBug(sql, "original");
Long start,end;
start = System.currentTimeMillis();
H2StatementParser parser = new H2StatementParser(sql);
end = System.currentTimeMillis();
System.err.println(end-start);
SQLStatement stmt = parser.parseStatement();
return processParser(stmt, obj);
}
protected String processParser(SQLStatement stmt, Object obj) {
// 查询语句
if (stmt instanceof SQLSelectStatement) {
processSelect((SQLSelectStatement) stmt, obj);
}
// 更新语句
else if (stmt instanceof SQLUpdateStatement) {
processUpdate((SQLUpdateStatement) stmt, obj);
}
//删除语句
else if (stmt instanceof SQLDeleteStatement) {
processDelete((SQLDeleteStatement) stmt, obj);
}
//插入语句
else if (stmt instanceof SQLInsertStatement) {
processInsert((SQLInsertStatement) stmt, obj);
}
String sql = stmt.toString();
deBug(sql, "after process");
final String replaceSql = sql.replace(String.valueOf(Long.MIN_VALUE), String.valueOf(tenantLineHandler.getTenantId()));
deBug(replaceSql, "replaceSql");
return replaceSql;
}
```
##### 自定义拦截器
```java
public class ConditionSqlParserInnerInterceptor extends DruidParserSupport implements InnerInterceptor {}
```
##### insert 、update 、delete 处理
使用druid提供api,轻松搞定
```java
@Override
protected void processInsert(SQLInsertStatement insert, Object obj) {
final List
*
* @return 租户 ID
*/
Long getTenantId();
```
tenant_id继续使用ThreadLocal实现,其余框架,需要设计拦截器,在拦截器中实现tenant_id的设置来实现无感知。
#### @WithoutTenant
可能有一些操作的sql 需要跨租户,可在方法上加上此注解
```java
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WithoutTenant {
}
@Slf4j
@Aspect
@AllArgsConstructor
public class WithoutTenantAspect {
@Around("@annotation(com.convertlab.multitenancystater.annotation.WithoutTenant)")
public Object doWithoutTenant(ProceedingJoinPoint proceedingJoinPoint) {
final String tenant = TenantContext.getCurrentTenant();
try {
TenantContext.clear();
Object object = proceedingJoinPoint.proceed();
return object;
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
TenantContext.setCurrentTenant(tenant);
}
return null;
}
}
```