程序员の奇妙冒险

返回
我开源的项目 Java MyBatis Spring Boot 多租户
2025-05-23 10:33 阅读:566 评论:0

开源项目-多租户插件druid-multi-tenant-starter

druid-multi-tenant-starter 目标多租户插件,快速集成,开箱即用,支持单个租户以及多个租户,基于alibaba druid 解析SQL,开发过程中只需要像平时一样写SQL语句即可,执行SQL最终会被拦截串改,最终把租户字段添加到SQL语句中,生成新的SQL语句,此过程你无需关

druid-multi-tenant-starter

目标多租户插件,快速集成,开箱即用,支持单个租户以及多个租户,基于alibaba druid 解析SQL,开发过程中只需要像平时一样写SQL语句即可,执行SQL最终会被拦截串改,最终把租户字段添加到SQL语句中,生成新的SQL语句,此过程你无需关心是如何添加。

需要注意的是,插件最终会认为每个表都会存在租户ID字段,所以,每个表都应该出现租户ID字段,否则需要在配置中明确指出需要忽略的表,忽略的表将不会拼接租户ID字段查询条件,临时表亦是如此(否则临时表结果集中需要出现租户ID字段)

支持 SQL 拦截策略种类

  • [x] mybatis_interceptor mybatis拦截器策略,需项目使用的是mybatis ORM框架, 如果项目使用mybatis ORM 框架,推荐使用此方式
  • [x] jpa_interceptor JPA拦截器策略,需项目使用的是JPA ORM框架
  • [x] druid_filter 过滤器策略,不管是哪种 ORM框架,只需要数据库连接使用的数据源是 DruidDataSource 即可

支持忽略tenantId策略

  • [x] 支持原SQL语句中已有tenantId字段条件跳过
  • [x] 支持忽略指定Mapper接口SQL语句添加tenantId字段条件
  • [x] 支持指定表名忽略SQL语句添加tenantId字段条件
  • [x] 支持指定表名前缀忽略SQL语句添加tenantId字段条件
  • [x] 支持指定别名忽略SQL语句添加tenantId字段条件
  • [x] 支持重写skipParser方法,通过获取当前线程变量判断是否跳过SQL语句解析添加tenantId字段条件,需要开发者自行实现逻辑判断
  • [x] 支持指定多数据源忽略SQL语句添加tenantId字段条件,需要开发者重写ignoreDynamicDatasource方法提供获取当前执行的数据源名称
  • [x] 支持@IgnoreTenantIdField注解从1.5.6开始支持使用在任意方法上,1.5.5及以下版本仅支持使用在mybatis Mapper接口方法上,需要在yml中配置enable-pointcut-advisor-ignore-tenant-id: true开启支持

快速开始

  • Spring Boot项目中引入以下依赖
1
2
3
4
5
<dependency>
<groupId>io.github.osinn</groupId>
<artifactId>druid-multi-tenant-starter</artifactId>
<version></version>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
-- 1.5.6 mybatis.tenant.config multi-tenant.config
multi-tenant:
config:
# 是否启用多租户插件
enable: true
# 数据库表租户ID的列名
tenant-id-column: tenant_id
 
--
multi-tenant:
config:
# 是否使用MyBatis拦截器方式修改sql
enable: true
# 数据库表租户ID的列名字段
tenant-id-column: tenant_id
# 数据库方言,可选,如果不指定会从事务数据元中自动识别获取方言类型
db-type: mysql
# SQL 拦截策略,默认 mybatis 拦截器,如果不是mybatis,且使用的是 DruidDataSource 数据源,推荐使用 druid_filter 过滤器策略
# druid_filter 只支持使用 DruidDataSource 数据数据源连接数据库
sql-interceptor-strategy: mybatis_interceptor
# 是否启用切面方式忽略租户ID字段,默认不开启,开启后 @IgnoreTenantIdField 注解不仅仅可以使用在mybatis Mapper接口方法上,也可以使用在Service任意接口上
enable-pointcut-advisor-ignore-tenant-id: false
# 是否忽略表按租户ID过滤,默认所有表都按租户ID过滤,指定表名称(区分大小写全等判断)
ignore-table-name:
- user_role # 内部全等判断
ignore-match-table-alias
# 匹配判断指定表别名称是否忽略表按租户ID过滤(区分大小写匹配判断)
- temp # 内部判断 以 temp 为前缀的别名进行忽略过滤
# 根据表名前缀判断是否忽略表按租户ID过滤
ignore-table-name-prefix:
- act_ # 内部判断 以 act_ 为前缀的别名进行忽略过滤
# 多数据源情况下指定忽略跳过的数据源名称(需要重写ITenantService接口中的ignoreDynamicDatasource方法自行提供获取当前执行的数据源名称)
ignore-dynamic-datasource:
- demoDataSource # 内部全等判断
# 数据源包装类型, 默认是 null
#项目中存在 baomidou(苞米豆) 动态数据源,需要指定 baomidou_dynamic_data_source
#
# 项目中使用 sleuth 代理 jdbc 同时使用baomidou(苞米豆) 动态数据源,需要指定 sleuth_baomidou_dynamic_data_source_wrapper ,否则需要禁用 sleuth 中的 jdbc 代理
# 如果项目中使用到 sleuth 同时使用baomidou(苞米豆) 动态数据源,应该禁用 sleuth 中的 jdbc 代理,例如以下配置,否则 sql-interceptor-strategy: druid_filter 是不会添加druid过滤器多租户 SQL 解析
# spring:
# sleuth:
# jdbc:
# enabled: false
data-source-wrapper-type: baomidou_dynamic_data_source
# 是否自定义数据源,默认是 false,true时会调用addCustomDataSourceProxySqlParser()方法,用于自定义数据源添加SQL解析器
enable-custom-data-source: false
  • 如果SQL中表名不在 ignore-table-name 中,则去ignore-match-table-alias匹配查找
  • 执行SQL中存在临时表可以约束指定 ignore-match-table-alias 匹配临时表别名来忽略临时表添加租户ID查询条件

实现提供获取多租户值接口

  • 1.5.5及以下版本需要实现ITenantService接口,1.5.6版本开始需要继承TenantApplicationContext抽象类提供获取多租户ID值

基础方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* ID
*
* @author wency_cai
* @since 1.5.6
*/
@Service
public class TenantServiceImpl extends TenantApplicationContext {
 
@Override
public List<Object> getTenantIds() {
Long tenantId = 1L;
List<Object> tenantIdList = new ArrayList<>();
tenantIdList.add(tenantId);
return tenantIdList;
}
 
}

完整方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* ID
*
* @author wency_cai
*
* @since 1.5.6 TenantApplicationContext ID
*/
@Service
public class TenantServiceImpl extends TenantApplicationContext {
 
@Override
public List<Object> getTenantIds() {
// 查询系统多租户id,如果有多个返回多个值即可
int tenantId = 1;
return Lists.newArrayList(tenantId);
}
 
// 如果需要指定某个数据源跳过设置租户ID,可以重写此方法,并且在yml配置文件中配置 ignore-dynamic-datasource 指定忽略的数据源名称
@Override
public String ignoreDynamicDatasource() {
// 过去当前执行的数据源名称, 空则需要执行解析设置租户ID
return ;
}
 
// 如果开发者需要自行解析SQL,可以重新此方法,并返回新的SQL语句,返回值为空时则继续执行默认解析逻辑,有返回值则直接返回且不会执行默认的解析逻辑
@Override
public String customizeParser(String originalSQL, Object paramTenantId) {
return null;
}
 
// 如果开发者想要在执行默认的解析逻辑前做点什么,可以重写此方法,如果有返回值则会覆盖原始SQL语句(originalSQL)
@Override
public String before(String originalSQL, Object paramTenantId) {
System.out.println("执行默认解析逻辑前 执行了 before方法");
return null;
}
 
// 如果开发者想要在执行默认的解析逻辑后做点什么,可以重写此方法,如果有返回值则会覆盖解析后的SQL语句(newSQL)
@Override
public String after(String newSQL, Object paramTenantId) {
System.out.println("执行默认解析逻辑后 执行了 after方法");
// return newSQL + " and tenant_id = 123456";
return null;
}
 
/**
* (线tenant_id)
* init()sqltenant_id ThreadLocal
* 线tenant_idtenant_idtrue
*
* @return true , false
*/
@Override
public boolean skipParser() {
return false;
}
 
/**
* yml enable-pointcut-advisor = true 使 ThreadLocal
* : threadLocalSkipParserSet > skipParser ThreadLocal > threadLocalSkipParserClear
* 使 ThreadLocal ThreadLocal set myThreadLocal.set(xxxx)
* ThreadLocal
* <p>
* 使 ThreadLocal 线( 使@IgnoreTenantIdField ID)
* 线()使 TransmittableThreadLocal 线 skipParserthreadLocalSkipParserSetthreadLocalSkipParserClear
*
* @since 1.5.6
*/
@Override
public void threadLocalSkipParserSet() {
IGNORE_TENANT.set(Boolean.TRUE);
}
 
/**
* yml enable-pointcut-advisor = true 使 ThreadLocal
* 使 ThreadLocal ThreadLocal clear myThreadLocal.clear()
* ThreadLocal
*
* @since 1.5.6
*/
@Override
public void threadLocalSkipParserClear() {
IGNORE_TENANT.remove();
}
 
/**
* bean sql
* yml enable-custom-data-source = true
*
* @param dataSource
* @param defaultSqlParser sql
* @return sql dataSource , null
* @since 1.5.6
*/
default Object addCustomDataSourceProxySqlParser(Object dataSource, DefaultSqlParser defaultSqlParser) {
 
return null;
}
}
  • 到此整合完成

租户ID从Mapper接口方法中传入

默认调用 ITenantService.getTenantIds() 接口方法获取租户ID

  • 此方式传入tenantId值只支持 mybatis_interceptor 拦截策略(mybatis 拦截器)
1
2
3
4
5
6
7
public interface UserMapper {
 
/**
* IDListSet
*/
User getUserInfoById(@Param("userId") Long userId, @Param("tenant_id") Long tenantId);
}
  • @Param("tenant_id") 中的值 tenant_id <font color=#FF000 >约定</font>对应的是 yml 配置 tenant-id-column 的值
  • 如果Mapper接口方法中传入租户ID,则不会调用 ITenantService.getTenantIds() 接口方法获取租户ID

@IgnoreTenantIdField注解使用

多租户忽略Mapper方法

  • mybatis Mapper接口方法上使用
1
2
3
4
5
6
7
8
public interface UserMapper {
 
/**
* @IgnoreTenantIdField
*/
@IgnoreTenantIdField
void deleteTestIgnoreTenantIdById(Long id);
}

service 服务方法忽略租户ID

  • yml配置为enable-pointcut-advisor-ignore-tenant-id: true,可在任意服务接口方法上使用@IgnoreTenantIdField注解来忽略设置多租户字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class DemoService {
 
@IgnoreTenantIdField
public Role getRole() {
return null;
}
}
 
-- enable-pointcut-advisor-ignore-tenant-id
multi-tenant:
config:
# 需要将此项配置设置为 true
enable-pointcut-advisor-ignore-tenant-id: true
  • 需要注意的是,内部使用的ThreadLocal设置当前线程跳过SQL解析,不支持跨线程跳过解析(即 方法上使用@IgnoreTenantIdField 注解,此方法内又有异步方法,在异步方法内操作数据库,此时不会生效,依然会解析设置租户ID)
  • 如果需要支持跨线程(异步方法),你的框架可参考使用 TransmittableThreadLocal 库创建线程池,然后重写 skipParserthreadLocalSkipParserSetthreadLocalSkipParserClear这三个方法逻辑

以下是解析修改后的SQL语句效果

select语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
SELECT id, name, tenant_id
FROM role
WHERE tenant_id = 11
-------------------------------------
 
SELECT *
FROM user s
WHERE s.name = '333'
AND s.tenant_id = 11
-------------------------------------
 
SELECT tenant_id
FROM people
WHERE id IN (
SELECT id
FROM user s
WHERE s.tenant_id = 11
)
AND tenant_id = 11
-------------------------------------
 
SELECT tenant_id
FROM people
WHERE id IN (
SELECT id
FROM user s
WHERE s.tenant_id = 11
)
AND id IN (
SELECT u.id
FROM user u
WHERE u.tenant_id = 11
)
AND tenant_id = 11
-------------------------------------
 
SELECT tenant_id
FROM people
WHERE id IN (
SELECT id
FROM user s
WHERE s.tenant_id = 11
)
AND id IN (
SELECT u.id
FROM user u
WHERE u.tenant_id = 11
)
AND tenant_id IN (
SELECT u.tenant_id
FROM user u
WHERE u.tenant_id = 11
)
AND tenant_id = 11
-------------------------------------
 
SELECT p.tenant_id
, (
SELECT u.name
FROM user u
WHERE u.id = p.user_id
AND u.tenant_id = 11
)
FROM people p
WHERE p.tenant_id = 11
-------------------------------------
 
SELECT p.tenant_id
, (
SELECT u.name
FROM user u
WHERE u.id = p.user_id
AND u.tenant_id = 11
)
, (
SELECT s.name
FROM user s
WHERE s.id = p.user_id
AND s.tenant_id = 11
)
FROM people p
WHERE p.tenant_id = 11
-------------------------------------
 
SELECT p.tenant_id
, (
SELECT u.name
, (
SELECT s.name
FROM user s
WHERE s.id = p.user_id
AND s.tenant_id = 11
)
FROM user u
WHERE u.id = p.user_id
AND u.tenant_id = 11
)
FROM people p
WHERE p.tenant_id = 11
-------------------------------------
 
SELECT u.*
FROM `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*
FROM `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND u.id = 22
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*
, (
SELECT ur.name
FROM role r
WHERE r.id = ur.role_id
AND r.tenant_id = 11
) AS r_name
FROM `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND u.id = 22
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*
FROM `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND ur.role_id IN (
SELECT r.id
FROM role r
WHERE r.tenant_id = 11
)
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*
FROM `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND ur.role_id IN (
SELECT r.id
FROM role r
WHERE r.tenant_id = 11
)
AND ur.test_id IN (
SELECT r.test_id
FROM role r
WHERE r.tenant_id = 11
)
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*
FROM `user` u
LEFT JOIN user_role ur
ON ur.user_id = u.id
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*
FROM `user` u
RIGHT JOIN user_role ur
ON ur.user_id = u.id
AND ur.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT COUNT(*), id
FROM `user`
WHERE tenant_id = 11
GROUP BY id
HAVING COUNT(*) >= 1
-------------------------------------
 
SELECT EXISTS (
SELECT *
FROM `user`
WHERE username = ?
AND tenant_id = 11
) AS d
-------------------------------------
 
SELECT EXISTS (
SELECT tenant_id
FROM people
WHERE id IN (
SELECT id
FROM user s
WHERE s.tenant_id = 11
)
AND tenant_id = 11
) AS d
-------------------------------------
 
SELECT username
FROM `user`
WHERE tenant_id = 11
UNION
SELECT username
FROM account
WHERE tenant_id = 11
-------------------------------------
 
SELECT u.*, r.id AS r_id, r.NAME AS r_name
FROM `user` u
LEFT JOIN user_role ur
ON ur.user_id = u.id
AND ur.tenant_id = 11
LEFT JOIN role r
ON r.id = ur.role_id
AND u.id = 22
AND r.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT u.*, r.id AS r_id, r.NAME AS r_name
FROM `user` u
LEFT JOIN user_role ur
ON ur.user_id = u.id
AND ur.tenant_id = 11
LEFT JOIN role r
ON r.id = ur.role_id
AND u.id = 22
AND r.tenant_id = 11
LEFT JOIN menu m
ON m.role_id = r.id
AND m.tenant_id = 11
WHERE u.tenant_id = 11
-------------------------------------
 
SELECT a, b
FROM (
SELECT *
FROM table_a
WHERE tenant_id = 11
) temp
WHERE temp.a = 'a';
-------------------------------------
 
SELECT temp.*
FROM (
SELECT *
FROM `user`
WHERE tenant_id = 11
) temp
JOIN user_role ur
ON ur.user_id = temp.id
AND ur.tenant_id = 11
WHERE temp.a = 'a'
AND temp.tenant_id = 11;
-------------------------------------
 
SELECT id, name, tenant_id
FROM role
WHERE tenant_id = 1
AND tenant_id = 11

update语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
UPDATE user u
SET ds = ?, u.name = ?, id = 'fdf', ddd = ?
WHERE id = ?
AND u.tenant_id = 11
-------------------------------------
 
UPDATE user u
SET ds = ?, u.name = ?, id = 'fdf', ddd = ?
WHERE id IN (1, 2, 3)
AND u.tenant_id = 11
-------------------------------------
 
UPDATE user u
SET ds = ?, u.name = ?, id = 'fdf', ddd = ?
WHERE id IN (
SELECT ur.user_id
FROM user_role ur
WHERE ur.tenant_id = 11
)
AND u.tenant_id = 11
-------------------------------------
 
UPDATE user u
SET ds = ?, u.name = ?, id = 'fdf', ddd = ?
WHERE id IN (
SELECT ur.user_id
FROM user_role ur
WHERE ur.id = 1
AND ur.tenant_id = 11
)
AND u.tenant_id = 11
-------------------------------------
 
UPDATE `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND u.id = 111
AND ur.tenant_id = 11
SET u.qr_code = '1212'
WHERE u.tenant_id = 11
-------------------------------------
 
UPDATE `user` u
JOIN user_role ur
ON ur.user_id = u.id
AND u.id IN (
SELECT urr.user_id
FROM user_role urr
WHERE urr.tenant_id = 11
)
AND ur.tenant_id = 11
SET u.qr_code = '1212'
WHERE u.tenant_id = 11
-------------------------------------
 
UPDATE user
SET name = CASE
WHEN id = 1 THEN 'name1'
WHEN id = 2 THEN 'name2'
WHEN id = 3 THEN 'name3'
END
WHERE id IN (11, 22, 33, 3)
AND tenant_id = 11;

insert语句

1
2
3
4
INSERT INTO `user` (`id`, `username`, `password`, tenant_id)
VALUES (?, ?, ?, 11),
(?, ?, ?, 11),
(?, ?, ?, 11)

delete语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELETE FROM user
WHERE id = 1
AND tenant_id = 11
-------------------------------------
 
DELETE FROM user
WHERE id IN (
SELECT id
FROM user s
WHERE s.tenant_id = 11
)
AND tenant_id = 11
-------------------------------------
 
DELETE FROM system_permission_data sp
LEFT JOIN system_role_data_relation re
ON sp.id = re.data_id
AND re.tenant_id = 11
LEFT JOIN system_role sr
ON sr.id = re.role_id
AND sr.tenant_id = 11
WHERE sr.id = 1
AND sp.tenant_id = 11
评论 (0)
暂无评论 来抢沙发吧~

目录

SpringWonderland

探索 Spring 生态的奇妙世界

2025-05-23 10:33