在SASS的大潮流下,相信依然存在很多使用一个数据库为多个租户提供服务的场景,这个情况下一般是多个租户共用同一套表通过sql语句级别来隔离不同租户的资源,比如设置一个租户标识字段,每次查询的时候在后面附加一个筛选条件:TenantId=xxx。这样能低代价、简单地实现多租户服务,但是每次执行sql的时候需要附加字段隔离,否则会出现数据错乱。

此隔离过程应该自动标识完成,所以我今天借助于Mybatis的插件机制来完成一个多租户sql隔离插件。

一、设计需求

1、首先,我们需要一种方案来识别哪些表需要使用多租户隔离,并且确定多租户隔离字段名称。

2、然后拦截mybatis执行过程中的prepare方法,通过改写加入多租户隔离条件,然后替换为我们新的sql。

3、寻找一种方法能多层次的智能的为识别到的数据表添加condition,毕竟CRUD过程都会存在子查询,并且不会丢失原有的where条件。

二、设计思路

对于需求1,我们可以定义一个条件字段决策器,用来决策某个表是否需要添加多租户过滤条件,比如定义一个接口:ITableFieldConditionDecision

/**
* 表字段条件决策器
* 用于决策某个表是否需要添加某个字段过滤条件
*
* @author liushuishang@gmail.com
* @date 2017/12/23 15:49
**/
public interface ITableFieldConditionDecision { /**
* 条件字段是否运行null值
* @return
*/
boolean isAllowNullValue();
/**
* 判决某个表是否需要添加某个字段过滤
*
* @param tableName 表名称
* @param fieldName 字段名称
* @return
*/
boolean adjudge(String tableName, String fieldName); }

然后在使用插件的地方填写必要的参数来初始化决策器

<!--多租户隔离插件-->
<bean class="com.smartdata360.smartfx.dao.plugin.MultiTenantPlugin">
<property name="properties">
<value>
<!--当前数据库方言-->
dialect=postgresql
<!--多租户隔离字段名称-->
tenantIdField=domain
<!--需要隔离的表名称java正则表达式-->
tablePattern=uam_*
<!--需要隔离的表名称,逗号分隔-->
tableSet=uam_user,uam_role
</value>
</property>
</bean>

对于需求2,我们开发一个Mybatis的拦截器:MultiTenantPlugin。抽取出将要预编译的sql语句,加工后再替换,然后Mybatis最终执行的是我们加工过的sql语句。

/**
* 多租户数据隔离插件
*
* @author liushuishang@gmail.com
* @date 2017/12/21 11:58
**/
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class})})
public class MultiTenantPlugin extends BasePlugin

对于需求3,我使用阿里Druid的sql parser模块来实现sql解析和condition附加。其大致过程如下:

(1)把sql解析成一颗AST,基本每个部分都会有一个对象与之对应。

(2)遍历AST,获取select、query和SQLExpr,抽取出表名称和别名,交给决策器判断是否需要添加多租户隔离条件。如果需要添加,则扩展原有condition加上多租户筛选条件;否则不做处理

(3)把修改后的AST重新转成sql语句

执行结果:

三、代码参考

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.smartdata360.smartfx.dao.extension.MultiTenantContent;
import com.smartdata360.smartfx.dao.sqlparser.ITableFieldConditionDecision;
import com.smartdata360.smartfx.dao.sqlparser.SqlConditionHelper;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.sql.Connection;
import java.util.*;
import java.util.regex.Pattern; /**
* 多租户数据隔离插件
*
* @author liushuishang@gmail.com
* @date 2017/12/21 11:58
**/
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class})})
public class MultiTenantPlugin extends BasePlugin { private final Logger logger = LoggerFactory.getLogger(MultiTenantPlugin.class); /**
* 当前数据库的方言
*/
private String dialect;
/**
* 多租户字段名称
*/
private String tenantIdField; /**
* 需要识别多租户字段的表名称的正则表达式
*/
private Pattern tablePattern; /**
* 需要识别多租户字段的表名称列表
*/
private Set<String> tableSet; private SqlConditionHelper conditionHelper; @Override
public Object intercept(Invocation invocation) throws Throwable {
String tenantId = MultiTenantContent.getCurrentTenantId();
//租户id为空时不做处理
if (StringUtils.isBlank(tenantId)) {
return invocation.proceed();
}
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String newSql = addTenantCondition(boundSql.getSql(), tenantId);
MetaObject boundSqlMeta = getMetaObject(boundSql);
//把新sql设置到boundSql
boundSqlMeta.setValue("sql", newSql); return invocation.proceed();
} @Override
public void setProperties(Properties properties) {
dialect = properties.getProperty("dialect");
if (StringUtils.isBlank(dialect))
throw new IllegalArgumentException("MultiTenantPlugin need dialect property value");
tenantIdField = properties.getProperty("tenantIdField");
if (StringUtils.isBlank(tenantIdField))
throw new IllegalArgumentException("MultiTenantPlugin need tenantIdField property value"); String tableRegex = properties.getProperty("tableRegex");
if (!StringUtils.isBlank(tableRegex))
tablePattern = Pattern.compile(tableRegex); String tableNames = properties.getProperty("tableNames");
if (!StringUtils.isBlank(tableNames)) {
tableSet = new HashSet<String>(Arrays.asList(StringUtils.split(tableNames)));
}
if (tablePattern == null || tableSet == null)
throw new IllegalArgumentException("MultiTenantPlugin tableRegex and tableNames must have one"); /**
* 多租户条件字段决策器
*/
ITableFieldConditionDecision conditionDecision = new ITableFieldConditionDecision() {
@Override
public boolean isAllowNullValue() {
return false;
}
@Override
public boolean adjudge(String tableName, String fieldName) {
if (tableRegex != null && tableRegex.matches(tableName)) return true;
if (tableSet != null && tableSet.contains(tableName)) return true;
return false;
}
};
conditionHelper = new SqlConditionHelper(conditionDecision);
} /**
* 给sql语句where添加租户id过滤条件
*
* @param sql 要添加过滤条件的sql语句
* @param tenantId 当前的租户id
* @return 添加条件后的sql语句
*/
private String addTenantCondition(String sql, String tenantId) {
if (StringUtils.isBlank(sql) || StringUtils.isBlank(tenantIdField)) return sql;
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, dialect);
if (statementList == null || statementList.size() == 0) return sql; SQLStatement sqlStatement = statementList.get(0);
conditionHelper.addStatementCondition(sqlStatement, tenantIdField, tenantId);
return SQLUtils.toSQLString(statementList, dialect);
} }
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.*;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.util.JdbcConstants;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils; import java.util.List; /**
* sql语句where条件处理辅助类
*
* @author liushuishang@gmail.com
* @date 2017/12/21 15:05
**/
public class SqlConditionHelper { private ITableFieldConditionDecision conditionDecision; public SqlConditionHelper(ITableFieldConditionDecision conditionDecision) {
this.conditionDecision = conditionDecision;
} /**
* 为sql'语句添加指定where条件
*
* @param sqlStatement
* @param fieldName
* @param fieldValue
*/
public void addStatementCondition(SQLStatement sqlStatement, String fieldName, String fieldValue) {
if (sqlStatement instanceof SQLSelectStatement) {
SQLSelectQueryBlock queryObject = (SQLSelectQueryBlock) ((SQLSelectStatement) sqlStatement).getSelect().getQuery();
addSelectStatementCondition(queryObject, queryObject.getFrom(), fieldName, fieldValue);
} else if (sqlStatement instanceof SQLUpdateStatement) {
SQLUpdateStatement updateStatement = (SQLUpdateStatement) sqlStatement;
addUpdateStatementCondition(updateStatement, fieldName, fieldValue);
} else if (sqlStatement instanceof SQLDeleteStatement) {
SQLDeleteStatement deleteStatement = (SQLDeleteStatement) sqlStatement;
addDeleteStatementCondition(deleteStatement, fieldName, fieldValue);
} else if (sqlStatement instanceof SQLInsertStatement) {
SQLInsertStatement insertStatement = (SQLInsertStatement) sqlStatement;
addInsertStatementCondition(insertStatement, fieldName, fieldValue);
}
} /**
* 为insert语句添加where条件
*
* @param insertStatement
* @param fieldName
* @param fieldValue
*/
private void addInsertStatementCondition(SQLInsertStatement insertStatement, String fieldName, String fieldValue) {
if (insertStatement != null) {
SQLInsertInto sqlInsertInto = insertStatement;
SQLSelect sqlSelect = sqlInsertInto.getQuery();
if (sqlSelect != null) {
SQLSelectQueryBlock selectQueryBlock = (SQLSelectQueryBlock) sqlSelect.getQuery();
addSelectStatementCondition(selectQueryBlock, selectQueryBlock.getFrom(), fieldName, fieldValue);
}
}
} /**
* 为delete语句添加where条件
*
* @param deleteStatement
* @param fieldName
* @param fieldValue
*/
private void addDeleteStatementCondition(SQLDeleteStatement deleteStatement, String fieldName, String fieldValue) {
SQLExpr where = deleteStatement.getWhere();
//添加子查询中的where条件
addSQLExprCondition(where, fieldName, fieldValue); SQLExpr newCondition = newEqualityCondition(deleteStatement.getTableName().getSimpleName(),
deleteStatement.getTableSource().getAlias(), fieldName, fieldValue, where);
deleteStatement.setWhere(newCondition); } /**
* where中添加指定筛选条件
*
* @param where 源where条件
* @param fieldName
* @param fieldValue
*/
private void addSQLExprCondition(SQLExpr where, String fieldName, String fieldValue) {
if (where instanceof SQLInSubQueryExpr) {
SQLInSubQueryExpr inWhere = (SQLInSubQueryExpr) where;
SQLSelect subSelectObject = inWhere.getSubQuery();
SQLSelectQueryBlock subQueryObject = (SQLSelectQueryBlock) subSelectObject.getQuery();
addSelectStatementCondition(subQueryObject, subQueryObject.getFrom(), fieldName, fieldValue);
} else if (where instanceof SQLBinaryOpExpr) {
SQLBinaryOpExpr opExpr = (SQLBinaryOpExpr) where;
SQLExpr left = opExpr.getLeft();
SQLExpr right = opExpr.getRight();
addSQLExprCondition(left, fieldName, fieldValue);
addSQLExprCondition(right, fieldName, fieldValue);
} else if (where instanceof SQLQueryExpr) {
SQLSelectQueryBlock selectQueryBlock = (SQLSelectQueryBlock) (((SQLQueryExpr) where).getSubQuery()).getQuery();
addSelectStatementCondition(selectQueryBlock, selectQueryBlock.getFrom(), fieldName, fieldValue);
}
} /**
* 为update语句添加where条件
*
* @param updateStatement
* @param fieldName
* @param fieldValue
*/
private void addUpdateStatementCondition(SQLUpdateStatement updateStatement, String fieldName, String fieldValue) {
SQLExpr where = updateStatement.getWhere();
//添加子查询中的where条件
addSQLExprCondition(where, fieldName, fieldValue);
SQLExpr newCondition = newEqualityCondition(updateStatement.getTableName().getSimpleName(),
updateStatement.getTableSource().getAlias(), fieldName, fieldValue, where);
updateStatement.setWhere(newCondition);
} /**
* 给一个查询对象添加一个where条件
*
* @param queryObject
* @param fieldName
* @param fieldValue
*/
private void addSelectStatementCondition(SQLSelectQueryBlock queryObject, SQLTableSource from, String fieldName, String fieldValue) {
if (StringUtils.isBlank(fieldName) || from == null || queryObject == null) return; SQLExpr originCondition = queryObject.getWhere();
if (from instanceof SQLExprTableSource) {
String tableName = ((SQLIdentifierExpr) ((SQLExprTableSource) from).getExpr()).getName();
String alias = from.getAlias();
SQLExpr newCondition = newEqualityCondition(tableName, alias, fieldName, fieldValue, originCondition);
queryObject.setWhere(newCondition);
} else if (from instanceof SQLJoinTableSource) {
SQLJoinTableSource joinObject = (SQLJoinTableSource) from;
SQLTableSource left = joinObject.getLeft();
SQLTableSource right = joinObject.getRight(); addSelectStatementCondition(queryObject, left, fieldName, fieldValue);
addSelectStatementCondition(queryObject, right, fieldName, fieldValue); } else if (from instanceof SQLSubqueryTableSource) {
SQLSelect subSelectObject = ((SQLSubqueryTableSource) from).getSelect();
SQLSelectQueryBlock subQueryObject = (SQLSelectQueryBlock) subSelectObject.getQuery();
addSelectStatementCondition(subQueryObject, subQueryObject.getFrom(), fieldName, fieldValue);
} else {
throw new NotImplementedException("未处理的异常");
}
} /**
* 根据原来的condition创建一个新的condition
*
* @param tableName 表名称
* @param tableAlias 表别名
* @param fieldName
* @param fieldValue
* @param originCondition
* @return
*/
private SQLExpr newEqualityCondition(String tableName, String tableAlias, String fieldName, String fieldValue, SQLExpr originCondition) {
//如果不需要设置条件
if (!conditionDecision.adjudge(tableName, fieldName)) return originCondition;
//如果条件字段不允许为空
if (fieldValue == null && !conditionDecision.isAllowNullValue()) return originCondition; String filedName = StringUtils.isBlank(tableAlias) ? fieldName : tableAlias + "." + fieldName;
SQLExpr condition = new SQLBinaryOpExpr(new SQLIdentifierExpr(filedName), new SQLCharExpr(fieldValue), SQLBinaryOperator.Equality);
return SQLUtils.buildCondition(SQLBinaryOperator.BooleanAnd, condition, false, originCondition);
} public static void main(String[] args) {
// String sql = "select * from user s ";
// String sql = "select * from user s where s.name='333'";
// String sql = "select * from (select * from tab t where id = 2 and name = 'wenshao') s where s.name='333'";
// String sql="select u.*,g.name from user u join user_group g on u.groupId=g.groupId where u.name='123'"; // String sql = "update user set name=? where id =(select id from user s)";
// String sql = "delete from user where id = ( select id from user s )"; // String sql = "insert into user (id,name) select g.id,g.name from user_group g where id=1"; String sql = "select u.*,g.name from user u join (select * from user_group g join user_role r on g.role_code=r.code ) g on u.groupId=g.groupId where u.name='123'";
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, JdbcConstants.POSTGRESQL);
SQLStatement sqlStatement = statementList.get(0);
//决策器定义
SqlConditionHelper helper = new SqlConditionHelper(new ITableFieldConditionDecision() {
@Override
public boolean adjudge(String tableName, String fieldName) {
return true;
} @Override
public boolean isAllowNullValue() {
return false;
}
});
//添加多租户条件,domain是字段ignc,yay是筛选值
helper.addStatementCondition(sqlStatement, "domain", "yay");
System.out.println("源sql:" + sql);
System.out.println("修改后sql:" + SQLUtils.toSQLString(statementList, JdbcConstants.POSTGRESQL));
} }

因为时间和环境限制,仅仅提供一个基础版本,可能测试不够充分,欢迎提出修正意见。

MyBatis多租户隔离插件开发的更多相关文章

  1. 【Network】OVS、VXLAN/GRE、OVN等 实现 Docker/Kubernetes 网络的多租户隔离

    多租户隔离 DragonFlow与OVN | SDNLAB | 专注网络创新技术 Neutron社区每周记(6.25~7.6)| OVS将加入Linux基金会?OVN或抛弃ovsdb? | Unite ...

  2. 如何用Serverless让SaaS获得更灵活的租户隔离和更优的资源开销

    关于SaaS和Serverless,相信关注我的很多读者都已经不陌生,所以这篇不会聊它们的技术细节,而将重点放在SaaS软件架构中引入Serverless之后,能给我们的SaaS软件带来多大的收益. ...

  3. MyBatis 工作流程及插件开发

    1. MyBatis 框架分层架构 2. MyBatis 工作流程 获取 SqlSessionFactory 对象: 解析配置文件(全局映射,Sql映射文件)的每一个信息,并保存在Configurat ...

  4. neutron 多租户隔离的实现以及子网间路由的实现

    1.一个network相当于一个二层网络,使用vxlan 隧道连通所有的CNA节点. 2.一个VPC下有多个network,也就是会分配多个vxlan隧道,这些子网间的路由是通过DVR实现的.DVR就 ...

  5. OpenStack 企业私有云的若干需求(3):多租户和租户间隔离(multi-tenancy and isolation)

    本系列会介绍OpenStack 企业私有云的几个需求: 自动扩展(Auto-scaling)支持 多租户和租户隔离 (multi-tenancy and tenancy isolation) 混合云( ...

  6. mybatis拦截器使用

    目录 mybatis 拦截器接口Interceptor spring boot + mybatis整合 创建自己的拦截器MyInterceptor @Intercepts注解 mybatis拦截器入门 ...

  7. SpringCloud微服务实战——搭建企业级开发框架(二十二):基于MybatisPlus插件TenantLineInnerInterceptor实现多租户功能

    多租户技术的基本概念:   多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且 ...

  8. Mybatis架构学习

    Mybatis架构学习 MyBatis 是支持定制化 SQL.存储过程以及高级映射的持久层框架.MyBatis 封装了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.可以对配置和原生Map使用 ...

  9. mybatis Java API

    既然你已经知道如何配置 MyBatis 和创建映射文件,你就已经准备好来提升技能了. MyBatis 的 Java API 就是你收获你所做的努力的地方.正如你即将看到的,和 JDBC 相比, MyB ...

随机推荐

  1. 1.Nginx 简介

    Nginx是Apache服务器不错的替代品,它能支持高达50 000个并发连接数的响应,而内存,CPU等系统资源消耗却非常低,运行非常稳定. 1.选择Nginx的理由 1.1 可以高并发连接 1.2 ...

  2. Android 开发笔记___textview_聊天室效果

    <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...

  3. DOM Exception error

    INDEX_SIZE_ERR  code 1                              索引是负值,或者超过了索引值 DOMSTRING_SIZE_ERR  code 2       ...

  4. Python爬虫入门:爬虫基础了解

    有粉丝私信我想让我出更基础一些的,我就把之前平台的copy下来了,可以粗略看一下,之后都会慢慢出. 1.什么是爬虫 爬虫,即网络爬虫,大家可以理解为在网络上爬行的一直蜘蛛,互联网就比作一张大网,而爬虫 ...

  5. 和为S的两个数

    题目 输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的. 输出描述: 对应每个测试案例,输出两个数,小的先输出. 思考 注 ...

  6. 使用SQLPLUS创建用户名和表空间

    用sqlplus为oracle创建用户和表空间用sqlplus为oracle创建用户和表空间用Oracle10g自带的企业管理器或PL/SQL图形化的方法创建表空间和用户以及分配权限是相对比较简单的, ...

  7. MUI开发记录——我的考勤

    已经好久没有更新技术博客了,因为最近一直在做跨平台web app应用的开发,由于是刚做这个,也没太多经验同大家分享,可我是一个闲不住的人,我还是决定于百忙之中抽空整理一篇,记录下开发历程......— ...

  8. JSP技术介绍

    1. 技术介绍 JSP即Java Server Page,中文全称是Java服务器语言.它是由Sun Microsystems公司倡导.许多公司参与建立的一种动态网页技术标准,它在动态网页的建设中有强 ...

  9. Markdown规则

    第一次写随想,写的不好还请包涵呀!!! 这两天在用markdown写一些文档,感觉还不错,整理一下其规则,对于新手会有帮助. 1. 文章的标题,一般写在首行,第二行添加"---------- ...

  10. 掌握numpy(四)

    数组的累加(拼接) 在前面讲了使用切片方法能够对数组进行切分,使用copy对切片的数组进行复制,那么数组该如何拼接呢? a1 = np.full((2,3),1)#填充数组 a2 = np.full( ...