• Mybatis Plus 多租户id使用


    本文就不多逼逼,直接进入正题。

    什么是多租户

    多租户技术(Multi-TenancyTechnology)又称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下 (此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。简单讲: 在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架 构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。那么重点就很浅显易懂了,多租户的重 点就是同一套程序下实现多用户数据的隔离

    隔离方案

    目前基于多租户的数据库设计方案通常有如下三种:

    • 1、独立数据库 共享数据库

    • 2、独立 Schema 共享数据库

    • 3、共享数据库、共享数据表

    独立数据库

    即一个租户一个数据库。

    优点

    为不同的租户提供独立的数据库,用户数据隔离级别最高,安全性最好,有助于简化数据模型的扩展设计,满足不同租户的独特需求;如果出现故障,恢复数据比较简单。

    缺点

    数据库维护成本和购置成本的大大增加。

    共享数据库,独立 Schema

    即多个或所有租户共享Database,每个租户一个Schema。

    什么是Schema

    • oracle数据库:在oracle中一个数据库可以具有多个用户,那么一个用户一般对应一个Schema,表都是建立在 Schema 中的,(可以简单的理解:在 oracle 中一个用户一套数据库表)

    • mysql数据库:mysql数据中的schema比较特殊,并不是数据库的下一级,而是等同于数据库。比如执行 create schema test 和执行create database test效果是一模一样的

    优点

    为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可以支持更多的租户数量。

    缺点

    如果出现故障,数据恢复比较困难,因为恢复数据库将牵扯到其他租户的数据;

    如果需要跨租户统计数据,存在一定困难。 这种方案是方案一的变种。只需要安装一份数据库服务,通过不同的Schema对不同租户的数据进行隔离。由于数 据库服务是共享的,所以成本相对低廉。

    共享数据库、共享数据表

    即租户共享同一个Database、同一个Schema,但在表中通过tenant_id字段区分租户的数据,表明该记录是属于哪个租户的。这是共享程度最高、隔离级别最低的模式。

    优点

    所有租户使用同一套数据库,所以成本低廉。

    缺点

    隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;这种方案和基于传统应用的数据库设计并没有任何区别,但是由于所有租户使用相同的数据库表,所以需要做好对 每个租户数据的隔离安全性处理,这就增加了系统设计和数据管理方面的复杂程度。 数据备份和恢复最困难,需要逐表逐条备份和还原。

    如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本,这种方案最适合。

    集成

    本文选择的是方案三!如果是自己从零开始进行开发,需要在每条 sql 上加上 tenant_id 条件。那开发成本特别大。但我们使用的是 Mybatis Plus,那就不需要如此复杂了,框架已经集成多租户使用。

    创建表

    1. CREATE TABLE `test_tenant` (
    2. `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    3. `account` varchar(32) DEFAULT NULL,
    4. `email` varchar(64) DEFAULT NULL,
    5. `tenant_id` int(10) unsigned DEFAULT NULL COMMENT '租户id',
    6. PRIMARY KEY (`id`)
    7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    8. 复制代码

    insert 语句

    1. INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (1, 'cxyxj', 'cxyxj.qq.com', 0);
    2. INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (2, 'awesome', 'awesome@163.com', 1);
    3. INSERT INTO `test`.`test_tenant`(`id`, `account`, `email`, `tenant_id`) VALUES (3, 'gongj', 'gongj@163.com', 2);
    4. 复制代码

    注意关键字段tenant_id

    搭建项目

    依赖

    搭建 Boot项目,加入以下依赖:

    1. <properties>
    2. <maven.compiler.source>8</maven.compiler.source>
    3. <maven.compiler.target>8</maven.compiler.target>
    4. <mybatis-plus.version>3.5.0</mybatis-plus.version>
    5. </properties>
    6. <dependencies>
    7. <dependency>
    8. <groupId>com.baomidou</groupId>
    9. <artifactId>mybatis-plus-boot-starter</artifactId>
    10. <version>${mybatis-plus.version}</version>
    11. </dependency>
    12. <dependency>
    13. <groupId>org.springframework.boot</groupId>
    14. <artifactId>spring-boot-starter-web</artifactId>
    15. </dependency>
    16. <dependency>
    17. <groupId>mysql</groupId>
    18. <artifactId>mysql-connector-java</artifactId>
    19. <version>5.1.47</version>
    20. <scope>runtime</scope>
    21. </dependency>
    22. <dependency>
    23. <groupId>org.projectlombok</groupId>
    24. <artifactId>lombok</artifactId>
    25. </dependency>
    26. <dependency>
    27. <groupId>org.springframework.boot</groupId>
    28. <artifactId>spring-boot-starter-test</artifactId>
    29. <scope>test</scope>
    30. <exclusions>
    31. <exclusion>
    32. <groupId>org.junit.vintage</groupId>
    33. <artifactId>junit-vintage-engine</artifactId>
    34. </exclusion>
    35. </exclusions>
    36. </dependency>
    37. </dependencies>
    38. 复制代码

    实体

    1. @Data
    2. public class TestTenant {
    3. @TableId(type = IdType.AUTO)
    4. private Integer id;
    5. private String account;
    6. private String email;
    7. private Integer tenantId;
    8. }
    9. 复制代码

    我们的主键类型为 int,所以需要修改主键策略,修改为自增,默认使用雪花算法生成全局唯一id,长度为19 位。

    mapper接口

    1. public interface TenantMapper extends BaseMapper<TestTenant> {
    2. }
    3. 复制代码

    MybatisPlusConfig

    1. import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    2. import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
    3. import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
    4. import net.sf.jsqlparser.expression.Expression;
    5. import net.sf.jsqlparser.expression.LongValue;
    6. import org.springframework.beans.factory.annotation.Value;
    7. import org.springframework.context.annotation.Bean;
    8. import org.springframework.context.annotation.Configuration;
    9. import java.util.List;
    10. @Configuration
    11. public class MybatisPlusConfig {
    12. @Bean
    13. public MybatisPlusInterceptor mybatisPlusInterceptor() {
    14. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    15. interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
    16. @Override
    17. public Expression getTenantId() {
    18. //获得当前登录用户的租户id
    19. return new LongValue(1111);
    20. }
    21. }));
    22. return interceptor;
    23. }
    24. }
    25. 复制代码

    在 Mybatis Plus 中,一切插件的主体是 InnerInterceptor。 目前已有的功能(官网地址):

    • 自动分页: PaginationInnerInterceptor
    • 多租户: TenantLineInnerInterceptor
    • 动态表名: DynamicTableNameInnerInterceptor
    • 乐观锁: OptimisticLockerInnerInterceptor
    • sql 性能规范: IllegalSQLInnerInterceptor
    • 防止全表更新与删除: BlockAttackInnerInterceptor

    本文使用到的是 TenantLineInnerInterceptor。在我们的代码中,使用了 TenantLineInnerInterceptor 类的有参构造方法。入参为 TenantLineHandler对象。这是比较重要的对象,比如:某一些表不需要拼接多租户条件、多租户的字段名是什么。都是在这个对象中规定。

    1. public interface TenantLineHandler {
    2. // 获得租户ID值 本文写死了 111
    3. Expression getTenantId();
    4. // 数据库字段 默认为 tenant_id
    5. default String getTenantIdColumn() {
    6. return "tenant_id";
    7. }
    8. // 需要忽略拼接条件的表名
    9. // 方法默认返回 false 表示所有表都需要拼多租户条件
    10. default boolean ignoreTable(String tableName) {
    11. return false;
    12. }
    13. // 这个方法在之前版本是没有的!已给出租户列的 insert 不再拼接条件。使用用户给出的值。
    14. // 针对比较特殊的场景,比如:异步添加时,获取不到登录人的租户ID,则给默认租户ID
    15. default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
    16. return columns.stream().map(Column::getColumnName).anyMatch((i) -> {
    17. return i.equalsIgnoreCase(tenantIdColumn);
    18. });
    19. }
    20. }
    21. 复制代码

    配置文件

    1. server:
    2. port: 1998
    3. spring:
    4. datasource:
    5. url: jdbc:mysql:/127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8
    6. username: root
    7. password: xxx
    8. driver-class-name: com.mysql.jdbc.Driver
    9. mybatis-plus:
    10. configuration:
    11. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    12. 复制代码

    测试

    1. @SpringBootTest
    2. public class MybatisPlusApplicationTests {
    3. @Autowired
    4. TenantMapper tenantMapper;
    5. @Test
    6. public void testSelect() {
    7. List<TestTenant> testTenants = tenantMapper.selectList(null);
    8. testTenants.forEach(System.out::println);
    9. }
    10. }
    11. 复制代码

    关注控制台打印的 sql 语句,在 where 语句后面拼接了 test_tenant.tenant_id = 1111的条件。这说明我们的租户隔离达到效果,并且很轻松容易的实现了。

    测试增修删语句

    那我们再来看看其他语句!

    1. @Test
    2. public void testOther() {
    3. System.out.println("测试新增=====");
    4. TestTenant testTenant = new TestTenant();
    5. testTenant.setAccount("hhhh");
    6. testTenant.setEmail("100093");
    7. tenantMapper.insert(testTenant);
    8. System.out.println("测试修改=====");
    9. testTenant.setEmail("@164.com");
    10. tenantMapper.updateById(testTenant);
    11. System.out.println("测试删除=====");
    12. tenantMapper.deleteById(testTenant.getId());
    13. }
    14. 复制代码
    • 测试新增
    1. 测试新增=====
    2. ==> Preparing: INSERT INTO test_tenant (account, email, tenant_id)
    3. VALUES (?, ?, 1111)
    4. ==> Parameters: hhhh(String), 100093(String)
    5. <== Updates: 1
    6. 复制代码
    • 测试修改
    1. 测试修改=====
    2. ==> Preparing: UPDATE test_tenant SET account = ?, email = ? WHERE
    3. test_tenant.tenant_id = 1111 AND id = ?
    4. ==> Parameters: hhhh(String), @164.com(String), 4(Integer)
    5. <== Updates: 1
    6. 复制代码
    • 测试删除
    1. 测试删除=====
    2. ==> Preparing: DELETE FROM test_tenant WHERE test_tenant.tenant_id = 1111 AND id = ?
    3. ==> Parameters: 4(Integer)
    4. <== Updates: 1
    5. 复制代码

    可以得知,当配置了 TenantLineInnerInterceptor插件后,我们的 CRUR SQL 都拼接了我们所指定的字段作为 where 条件。

    特殊处理

    在实际的开发中,肯定不会如此的一帆风顺。肯定会有一些比较特殊的逻辑。

    某表不需要拼接租户条件

    总有一些表是比较特殊的。表中压根就没租户id字段,那这怎么处理呢? 我们只需要重写 TenantLineHandler 类中的ignoreTable方法即可。

    1. /**
    2. * 需要忽略拼接多租户条件的表名
    3. */
    4. @Value("#{'${mybatis-plus.configuration.ignore-tenant-tables:}'.split(',')}")
    5. private List<String> ignoreTenantTables;
    6. //default 方法 默认返回 false 表示所有表都需要拼多租户条件
    7. // 如果有部分 sql 不需要加上租户ID条件
    8. // 可以使用 @InterceptorIgnore(tenantLine = "true") 标注在 Mapper 接口的方法上
    9. // 而 @SqlParser(filter = true) 在 mybatis-plus 3.4 版本中标记为过时
    10. @Override
    11. public boolean ignoreTable(String tableName) {
    12. return ignoreTenantTables.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
    13. }
    14. 复制代码

    在配置文件中将需要忽略的表名进行配置。

    1. mybatis-plus:
    2. configuration:
    3. ignore-tenant-tables: test_tenant
    4. 复制代码

    测试

    1. @Test
    2. public void testSelect() {
    3. List<TestTenant> testTenants = tenantMapper.selectList(null);
    4. testTenants.forEach(System.out::println);
    5. }
    6. 复制代码

    可以看到这一次的查询语句中并没有拼接多租户条件。

    某一条sql不需要拼接

    对于一些拥有租户id字段的表,在某一些场景中,比如:我想获得表中所有数据,不想让它拼接条件。那应该怎么做?注意:这种都是自己自定义 sql 语句。

    我们只需要在自定义的方法上标注一个注解 @InterceptorIgnore。这是官方提供的。

    该注解作用于 xxMapper.java 方法之上,各属性代表对应的插件,各属性不给值则默认为 false,设置为 true 表示忽略拦截。

    自定义 sql

    1. @Select("SELECT id, account, email, tenant_id FROM test_tenant")
    2. @InterceptorIgnore(tenantLine = "true")
    3. List listAll();
    4. 复制代码

    测试

    1. @Test
    2. public void listAll() {
    3. List<TestTenant> testTenants = tenantMapper.listAll();
    4. testTenants.forEach(System.out::println);
    5. }
    6. 复制代码

    额外知识点

    TenantLineHandler 类中,还有一个方法没有介绍,那就是 ignoreInsert方法。这个方法的作用就是如果你在进行 insert 时,我们手动给了租户id字段,则框架不再自动拼接。我们来看看效果吧!

    1. @Test
    2. public void testInsert() {
    3. System.out.println("测试新增=====");
    4. TestTenant testTenant = new TestTenant();
    5. testTenant.setAccount("hhhh");
    6. testTenant.setEmail("100093");
    7. testTenant.setTenantId(11232323);
    8. tenantMapper.insert(testTenant);
    9. }
    10. 复制代码

    可以看到 sql 中 tenant_id 的值,取的是我们指定的值。 我们看看源码是怎么处理的!逻辑在 processInsert方法中。

    如果需要插入的列中,包含知道的租户列,则不进行多租户处理。

    如果还想对 update、delete sql 也进行这种特殊的处理,只需要重写对应的方法 processUpdateprocessDelete


    • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注

  • 相关阅读:
    Edge官方鼠标手势
    Mysql基础 (二)
    FastReport4.6 组件安装
    八皇后问题的实现(思路分析) [数据结构][Java]
    联盟 | 彩漩 X HelpLook,AI技术赋能企业效率提升
    淘宝/天猫API:item_search_shop-获得店铺的所有商品
    分布式事务
    Pulsar-Pulsar 之 pulsar manager
    SSH 远程管理软件 SecureCRT 下载安装教程
    女生学java开发难吗?女生适合学java吗?
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/128092192