如何进行企业营销型网站建设规划,个人网站服务器一年多少钱,基层建设 官方网站,这么做国外网站的国内镜像站Mybatis-Plus 租户使用 文章目录 Mybatis-Plus 租户使用一. 前言1.1 租户存在的意义1.2 租户框架 二. Mybatis-plus 租户2.1 租户处理器2.2 前置准备1. 依赖2. 表及数据准备3. 代码生成器 2.3 使用 三. 深入使用3.1 前言3.2 租户主体设值#xff0c;取值3.3 部分表全量db操作3…Mybatis-Plus 租户使用 文章目录 Mybatis-Plus 租户使用一. 前言1.1 租户存在的意义1.2 租户框架 二. Mybatis-plus 租户2.1 租户处理器2.2 前置准备1. 依赖2. 表及数据准备3. 代码生成器 2.3 使用 三. 深入使用3.1 前言3.2 租户主体设值取值3.3 部分表全量db操作3.4 全量db操作3.5 自由控制全量/租户对db操作3.6 feign传递租户 四. 源码地址 一. 前言
1.1 租户存在的意义
多租户数据隔离与管理是在一个共享的软件应用程序中托管多个租户客户的数据并确保每个租户的数据完全隔离、安全和可管理。需要支持相同数据(记录)可由具有不同容量的多个租户。
1.2 租户框架
mybatis-plus租户插件使用
由于业务数据都需要通过对数据库的crud而此时mybatis-plus作为orm框架提供了租户功能我们可以合理利用拓展应用于自己的业务范畴中
二. Mybatis-plus 租户
2.1 租户处理器 我们通过实现TenantLineHandler即可实现对数据库中的表进行租户过滤 属性名类型默认值描述TenantLineHandlerTenantLineHandler租户处理器 TenantId 行级 实现租户过滤
这里我们通过匿名内部类实现TenantLineHandler。(也可以新建立一个类实现)向MybatisPlusInterceptor添加我们所实现的匿名内部类注入bean ps配置完后就可实现对表进行租户筛选读者们可自行测试 由于后面我对该代码进行了加工该代码会出现一些自定义逻辑在里面但原理是不变的 /*** author zhanghp* since 2023/11/20 14:29*/
Configuration
public class TenantConfig {Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {MybatisPlusInterceptor interceptor new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {Overridepublic Expression getTenantId() {// 默认租户为1return new LongValue(1);}Overridepublic String getTenantIdColumn() {return properties.getTenantId();}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件Overridepublic boolean ignoreTable(String tableName) {return false;}}));return interceptor;}}
2.2 前置准备
1. 依赖
各依赖版本继承至父依赖可自行查看 dependencies!-- 启动 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- 配置解析 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-configuration-processor/artifactIdoptionaltrue/optional/dependency!-- aop --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactIdscopeprovided/scope/dependency!-- test --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency!-- mybatis-plus --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactId/dependency!-- mysql jdbc --dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependency!-- 代码生成器 --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-generator/artifactId/dependency!-- 生成器模版引擎 --dependencygroupIdorg.apache.velocity/groupIdartifactIdvelocity-engine-core/artifactId/dependency!-- alibaba thread local --dependencygroupIdcom.alibaba/groupIdartifactIdtransmittable-thread-local/artifactId/dependency!-- lombok --dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency!-- hutool all --dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactId/dependency/dependencies2. 表及数据准备
完整配置application.yml
这里我沿用了Spring自带的属性配置spring.sql.init初始化数据库的schemadata。 建表sql | 数据 当启动项目后自动进行建表初始化表及相应的数据 spring.io官网对配置文件的概述可自行搜索spring.sql.init解锁更多使用方式 指定数据库配置指定出初始化的建表sql及对应的数据
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: zhp.1221url: jdbc:mysql://127.0.0.1:3306/test?useSSLfalseuseUnicodetruecharacterEncodingUTF-8serverTimezoneUTCsql:init:# 建表sqlschema-locations: classpath:db/schema.sql# 数据data-locations: classpath:db/data.sql# 生成模式mode: always
3. 代码生成器
调用代码生成Generate.java自动生成表实体类的相关类 这里就不进行展示了自动代码生成若有不懂的地方可看Mybatis plus 自动生成代码与自定义模板 2.3 使用
MpTest.java测试对db的增删改查 /*** author zhanghp* since 2023/11/20 14:21*/
public class MpTest extends AppTest {Resourceprivate DemoMapper demoMapper;Testpublic void select() {demoMapper.selectList(null);}Testpublic void insert() {demoMapper.insert(Demo.builder().id(100).age(18).name(草莓熊).build());}Testpublic void update() {demoMapper.updateById(Demo.builder().id(100).age(20).name(皮卡).build());}Testpublic void delete() {demoMapper.deleteById(Demo.builder().id(100).build());}}select查询结果日志 insert插入日志 update修改日志 delete删除日志 三. 深入使用
3.1 前言
Saas多租户使用时其实面临着许多技术问题我拿几个公司所遇见的经典问题进行举例
如何传递tenat_id有一些配置表永远不需要租户过滤在一个请求里不需要租户过滤在一个请求里部分数据db需要租户过滤部分数据db不需要租户过滤微服务feign传递由于我的代码项目是单体架构我只简单赘述如何实现…
3.2 租户主体设值取值
一个租户的设值一般分为两种
一个请求里附带租户值一般放在请求头header自定义设值引用
设值后需要找个地方存储这里引用alibaba的TransmittableThreadLocal进行值的存储也方便后续取值所以这里新建立一个TenantContextHolder类 TENAN_ID进行值的存储与读取 TENANT_SKIP进行租户是否过滤也就是对db是否进行租户过滤的判断where tenant_id ?下一节会讲到 UtilityClass对所有方法加static对类加final不允许new /*** author zhanghp* since 2023/11/20 20:28*/
Data
UtilityClass
public class TenantContextHolder {/*** tenant_id*/private final ThreadLocalLong TENAT_ID new TransmittableThreadLocal();/*** 获取租户* return 租户id*/public Long getTenantId() {return TENAT_ID.get();}/*** 设置租户* param tenantId 租户*/public void setTenantId(Long tenantId) {TENAT_ID.set(tenantId);}/*** 清空租户信息*/public void clear() {TENAT_ID.remove();}} 当有了这个类后那我们需要重新实现TenantLineHandler由于之前已经定义了一个TenantConfig的bean来对租户处理器并注入到容器中若我们再次定义一个会导致bean冗余让spring找不到使用哪个进而导致启动报错。 那么问题来了有没有一种方式我既要能控制新定义的TenangLineHandler存在老的也兼容呢
这时我们使用一下ConditionalOnMissingBean使用在TenantConfig上和Order使用在DeepTenantConfig即可读者可自行浏览
DeepTenantConfig ConditionalOnProperty(prefix “custom.tenant”, value “deep-config”, havingValue “true”) 代表我是否要使用这个bean注入为什么要有这个注解来控制bean注入呢是因为要兼容上一章的租户使用TenantConfig选择注入哪个若没有该注解则都会默认注入DeepTenantConfig的bean控制该bean的注入在application.yml里的custom.tenant.deep-config /*** author zhanghp* since 2023/11/24 17:51*/
Configuration
public class DeepTenantConfig {BeanOrder(Integer.MIN_VALUE)ConditionalOnProperty(prefix custom.tenant, value deep-config, havingValue true)public MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {Overridepublic Expression getTenantId() {// 获取租户Long tenantId TenantContextHolder.getTenantId();// 租户为空则返回空if (tenantId null) {return new NullValue();}// 返回租户return new LongValue(tenantId);}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件Overridepublic boolean ignoreTable(String tableName) {// 租户为空则对该表不进行租户的操作Long tenantId TenantContextHolder.getTenantId();if (tenantId null) {return Boolean.TRUE;}return false;}}));return interceptor;}}
这些都处理好后我们在配置中把custom.tenant.deep-config设置为true这样读取的是我们上面定义的bean 我们在创建一个请求处理器类进行租户的处理
TenantInterceptor 当一次请求进来会进行租户值当处理并存储在刚才我们所建立的TenantContextHolder类里进行租户值处理 /*** author zhanghp* date 2023-11-25 13:48*/
public class TenantInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (StrUtil.isNotBlank(tenant-id)) {TenantContextHolder.setTenantId(Convert.toLong(tenantId));}return true;}Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {TenantContextHolder.clear();}
}
将拦截器添加到拦截器注册类里 /*** author zhanghp* date 2023-11-25 16:53*/
Configuration
public class CustomInterceptorConfig implements WebMvcConfigurer {Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(getRequestContextInterceptor());}Beanpublic TenantInterceptor getRequestContextInterceptor() {return new TenantInterceptor();}
} 现在我们创建一个controller /*** p* mybatis - demo表 前端控制器* /p** author zhp* since 2023-11-20*/
RestController
RequestMapping(/zhanghp/demo)
public class DemoController {Resourceprivate DemoService demoService;GetMapping(/get)public void getall(){ListDemo all demoService.getAll();if (IterUtil.isEmpty(all)) {return;}all.forEach(System.out::println);}
}现在我们打开apifox/postman进行一次请求操作看是否会生效
http://localhost:8888/zhanghp/demo/get请求头添加tenant_id的值
发送请求
结果日志打印 3.3 部分表全量db操作
⭐️场景
其实在业务中有一些配置表字典表其实是不需要租户的就比如城市配置表城市就这些加租户也是没有任何意义的
⭐️解决
实现其实也是很简单只需要在DeepTenantConfig里的的ignoreTables方法进行一些逻辑处理就可以
实现步骤
在application.yml进行表的配置读取配置并在DeepTenantConfig添加自己的逻辑
这里由于读取了配置我喜欢把自定义的配置定义一个类通过该类对配置读取读者可自行查看这里就不赘述了
配置
custom:tenant:# 是否注入deepTenantConfig的bean开关deep-config: true# 租户字段名称tenant-id: tenant_id# 忽略租户的表ignore-tables:- demo01- demo02逻辑新增 /*** author zhanghp* since 2023/11/24 17:51*/
Configuration
public class DeepTenantConfig {BeanOrder(Integer.MIN_VALUE)ConditionalOnProperty(prefix custom.tenant, value deep-config, havingValue true)public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {MybatisPlusInterceptor interceptor new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {Overridepublic Expression getTenantId() {// 获取租户Long tenantId TenantContextHolder.getTenantId();// 租户为空则返回空if (tenantId null) {return new NullValue();}// 返回租户return new LongValue(tenantId);}Overridepublic String getTenantIdColumn() {return properties.getTenantId();}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件Overridepublic boolean ignoreTable(String tableName) {Long tenantId TenantContextHolder.getTenantId();if (tenantId null) {return Boolean.TRUE;}// 忽略表对配置为空则所有表都进行租户操作if (IterUtil.isEmpty(properties.getIgnoreTables())) {return false;}// 指定的表永远不进行租户操作if (properties.getIgnoreTables().contains(tableName)) {return true;}return false;}}));return interceptor;}
}读者自行练习就可就不再演示
3.4 全量db操作
⭐️场景
在一个请求里这个请求涉及到的表是不需要租户过滤那么读者就会想到那在上一节的配置文件加啊。
这里的一个请求不需要对A表进行过滤但另一个请求对A是需要进行租户过滤的那这时加配置文件是行不通的
⭐️解决
TenantContextHolder里定义一个过滤租户的threadlocal在DeepTenantConfig的ignoreTables()方法新增逻辑判断
/*** author zhanghp* since 2023/11/20 20:28*/
Data
UtilityClass
public class TenantContextHolder {/*** tenant_id*/private final ThreadLocalLong TENAT_ID new TransmittableThreadLocal();/*** 租户过滤标识*/private final ThreadLocalBoolean TENANT_SKIP new TransmittableThreadLocal();/*** 获取租户* return 租户id*/public Long getTenantId() {return TENAT_ID.get();}/*** 设置租户* param tenantId 租户*/public void setTenantId(Long tenantId) {TENAT_ID.set(tenantId);}/*** 设置是否过滤的标识*/public void setTenantSkip() {TENANT_SKIP.set(Boolean.TRUE);}/*** 获取是否跳过租户过滤的标识* return true-过滤 false-不过滤*/public Boolean getTenantSkip() {return TENANT_SKIP.get() ! null TENANT_SKIP.get();}/*** 清空租户信息*/public void clear() {TENAT_ID.remove();clearSkip();}/*** 清空租户过滤标识*/public void clearSkip(){TENANT_SKIP.remove();}}
DeepTenantConfig /*** author zhanghp* since 2023/11/24 17:51*/
Configuration
public class DeepTenantConfig {BeanOrder(Integer.MIN_VALUE)ConditionalOnProperty(prefix custom.tenant, value deep-config, havingValue true)public MybatisPlusInterceptor mybatisPlusInterceptor(TenantProperties properties) {MybatisPlusInterceptor interceptor new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {Overridepublic Expression getTenantId() {// 获取租户Long tenantId TenantContextHolder.getTenantId();// 租户为空则返回空if (tenantId null) {return new NullValue();}// 返回租户return new LongValue(tenantId);}Overridepublic String getTenantIdColumn() {return properties.getTenantId();}// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件Overridepublic boolean ignoreTable(String tableName) {// 1.是否跳过租户对该表对操作if (TenantContextHolder.getTenantSkip()) {return Boolean.TRUE;}// 2.租户为空则对该表不进行租户的操作Long tenantId TenantContextHolder.getTenantId();if (tenantId null) {return Boolean.TRUE;}// 3.忽略表对配置为空则所有表都进行租户操作if (IterUtil.isEmpty(properties.getIgnoreTables())) {return false;}// 4.指定的表永远不进行租户操作if (properties.getIgnoreTables().contains(tableName)) {return true;}return false;}}));return interceptor;}}
在controller层新加一个TenantContextHolder.setTenantSkip()即可
GetMapping(/get2)public void getall2(){TenantContextHolder.setTenantSkip();ListDemo all demoService.list();if (IterUtil.isEmpty(all)) {return;}all.forEach(System.out::println);}读者自行练习即可
3.5 自由控制全量/租户对db操作
⭐️场景
在一个请求里进行了对A表和B表操作但是A表是不需要租户而B表又是需要租户
⭐️解决
对A表的操作提取为一个a方法对B的操作提取出一个b方法利用上节的TENANT_SKIP让a方法跳过租户当a方法执行后利用切面进行TENANT_SKIP的清空这时b方法就会带着租户对B表进行处理
这里我写了一个注解TenanClear 作用域方法类 作用执行当前方法里的所有db操作都不带租户通过属性globalFlag进行控制后续方法是否添加租户过滤 ps是实现原理在 TenantClearAspect /*** author zhanghp* since 2023/11/21 20:41*/
Target({ ElementType.METHOD, ElementType.TYPE })
Retention(RetentionPolicy.RUNTIME)
Documented
public interface TenantClear {/*** p执行该方法后是否全局过滤租户标志/p* ul* litrue执行完该线程之前后续对db操作不添加租户操作/li* lifalse执行完当前方法后后续方法的db操作添加租户操作/li* /ul** return 默认后续不添加租户操作*/boolean globalFlag() default true;
}该注解的作用当添加该注解后当前方法进行的所有db操作都不带租户处理后续方法执行是否带租户通过属性globalFlag决定 创建a方法并创建b方法 在a方法添加TenantClear(globalFlag false)然后在controller层先调用a在调用b 输出结果a不带租户b带租户 /*** p* mybatis - demo表 服务实现类* /p** author zhp* since 2023-11-20*/
Service
public class DemoServiceImpl extends ServiceImplDemoMapper, Demo implements DemoService {Overridepublic void b() {super.list();}OverrideTenantClear(globalFlag false)public void a(){super.list();}}
在controller层调用 GetMapping(/tenant-clear)public void tenantClear(){Console.log(执行a方法);demoService.a();Console.log(执行b方法);demoService.b();}结果打印 3.6 feign传递租户
由于本项目是单体项目不涉及微服务在这里只是简单概述
实现步骤
实现feign自带的原生扩展RequestInterceptor类重写apply方法处理tenant-id的传递
public class CustomerFeignInterceptor implements RequestInterceptor {public CustomerFeignInterceptor() {}public void apply(RequestTemplate requestTemplate) {if (TenantContextHolder.getTenantId() null) {log.debug(租户ID为空feign不传递);} else {requestTemplate.header(tenant-id, new String[]{TenantContextHolder.getTenantId().toString()});}}
}四. 源码地址
⭐️giteehttps://gitee.com/zhp1221/java/tree/master/lab_02_mybatis_plus/tenant 文章转载自: http://www.morning.glkhx.cn.gov.cn.glkhx.cn http://www.morning.kzslk.cn.gov.cn.kzslk.cn http://www.morning.pcngq.cn.gov.cn.pcngq.cn http://www.morning.pcgmw.cn.gov.cn.pcgmw.cn http://www.morning.qxlhj.cn.gov.cn.qxlhj.cn http://www.morning.swdnr.cn.gov.cn.swdnr.cn http://www.morning.cwknc.cn.gov.cn.cwknc.cn http://www.morning.bwttj.cn.gov.cn.bwttj.cn http://www.morning.fgqbx.cn.gov.cn.fgqbx.cn http://www.morning.gcxfh.cn.gov.cn.gcxfh.cn http://www.morning.jzfxk.cn.gov.cn.jzfxk.cn http://www.morning.cpwmj.cn.gov.cn.cpwmj.cn http://www.morning.qxbsq.cn.gov.cn.qxbsq.cn http://www.morning.pjqxk.cn.gov.cn.pjqxk.cn http://www.morning.ryfqj.cn.gov.cn.ryfqj.cn http://www.morning.qcdtzk.cn.gov.cn.qcdtzk.cn http://www.morning.wgqtj.cn.gov.cn.wgqtj.cn http://www.morning.pmbcr.cn.gov.cn.pmbcr.cn http://www.morning.xrtsx.cn.gov.cn.xrtsx.cn http://www.morning.nqbkb.cn.gov.cn.nqbkb.cn http://www.morning.heleyo.com.gov.cn.heleyo.com http://www.morning.lqypx.cn.gov.cn.lqypx.cn http://www.morning.taipinghl.cn.gov.cn.taipinghl.cn http://www.morning.bqrd.cn.gov.cn.bqrd.cn http://www.morning.horihe.com.gov.cn.horihe.com http://www.morning.jftl.cn.gov.cn.jftl.cn http://www.morning.sthp.cn.gov.cn.sthp.cn http://www.morning.tbnpn.cn.gov.cn.tbnpn.cn http://www.morning.gnghp.cn.gov.cn.gnghp.cn http://www.morning.clpfd.cn.gov.cn.clpfd.cn http://www.morning.hotlads.com.gov.cn.hotlads.com http://www.morning.jklns.cn.gov.cn.jklns.cn http://www.morning.qflwp.cn.gov.cn.qflwp.cn http://www.morning.pycpt.cn.gov.cn.pycpt.cn http://www.morning.chehb.com.gov.cn.chehb.com http://www.morning.kjksn.cn.gov.cn.kjksn.cn http://www.morning.czrcf.cn.gov.cn.czrcf.cn http://www.morning.wyppp.cn.gov.cn.wyppp.cn http://www.morning.webpapua.com.gov.cn.webpapua.com http://www.morning.ksjmt.cn.gov.cn.ksjmt.cn http://www.morning.eronghe.com.gov.cn.eronghe.com http://www.morning.mjpgl.cn.gov.cn.mjpgl.cn http://www.morning.qyxnf.cn.gov.cn.qyxnf.cn http://www.morning.jjzrh.cn.gov.cn.jjzrh.cn http://www.morning.ybqlb.cn.gov.cn.ybqlb.cn http://www.morning.qtyfb.cn.gov.cn.qtyfb.cn http://www.morning.wrdlf.cn.gov.cn.wrdlf.cn http://www.morning.xqspn.cn.gov.cn.xqspn.cn http://www.morning.ylqb8.cn.gov.cn.ylqb8.cn http://www.morning.npbgj.cn.gov.cn.npbgj.cn http://www.morning.zlsmx.cn.gov.cn.zlsmx.cn http://www.morning.rjkfj.cn.gov.cn.rjkfj.cn http://www.morning.nlywq.cn.gov.cn.nlywq.cn http://www.morning.yrmgh.cn.gov.cn.yrmgh.cn http://www.morning.yqqgp.cn.gov.cn.yqqgp.cn http://www.morning.gwdkg.cn.gov.cn.gwdkg.cn http://www.morning.qdrrh.cn.gov.cn.qdrrh.cn http://www.morning.pnmtk.cn.gov.cn.pnmtk.cn http://www.morning.ydrml.cn.gov.cn.ydrml.cn http://www.morning.plqkz.cn.gov.cn.plqkz.cn http://www.morning.msbct.cn.gov.cn.msbct.cn http://www.morning.yckrm.cn.gov.cn.yckrm.cn http://www.morning.tqrxm.cn.gov.cn.tqrxm.cn http://www.morning.mkrjf.cn.gov.cn.mkrjf.cn http://www.morning.kllzy.com.gov.cn.kllzy.com http://www.morning.nqbcj.cn.gov.cn.nqbcj.cn http://www.morning.weitao0415.cn.gov.cn.weitao0415.cn http://www.morning.wnpps.cn.gov.cn.wnpps.cn http://www.morning.pjrgb.cn.gov.cn.pjrgb.cn http://www.morning.xbmwm.cn.gov.cn.xbmwm.cn http://www.morning.srtw.cn.gov.cn.srtw.cn http://www.morning.rbylq.cn.gov.cn.rbylq.cn http://www.morning.qggm.cn.gov.cn.qggm.cn http://www.morning.kryn.cn.gov.cn.kryn.cn http://www.morning.dqwkm.cn.gov.cn.dqwkm.cn http://www.morning.ckhry.cn.gov.cn.ckhry.cn http://www.morning.gqfks.cn.gov.cn.gqfks.cn http://www.morning.btlmb.cn.gov.cn.btlmb.cn http://www.morning.gmwdl.cn.gov.cn.gmwdl.cn http://www.morning.mgmqf.cn.gov.cn.mgmqf.cn