Mybatis Plus 多租户方案

目录

一、Mybatis Plus 具体实现

二、生产示例


当不同的租户使用同一套程序,这里就需要考虑一个数据隔离的情况。

数据隔离有三种方案:

  1. 独立数据库:简单来说就是一个租户使用一个数据库,这种数据隔离级别最高,安全性最好,但是提高成本。
  2. 共享数据库、隔离数据架构:多租户使用同一个数据裤,但是每个租户对应一个Schema(数据库user)。
  3. 共享数据库、共享数据架构:使用同一个数据库,同一个Schema,但是在表中增加了租户ID的字段,这种共享数据程度最高,隔离级别最低。

一、Mybatis Plus 具体实现

Mybatis Plus 提供了一种多租户的解决方案,基于分页插件进行实现,具体实现代码如下:

1、租户配置 // 以下代码都是基于SpringBoot

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;

@Configuration
@MapperScan("com.baomidou.mybatisplus.samples.tenant.mapper")
public class MybatisPlusConfig {

    /**
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                // 设置当前租户ID,实际情况你可以从cookie、或者缓存中拿都行
                return new LongValue(1);
            }
            @Override
            public String getTenantIdColumn() {
                // 对应数据库租户ID的列名
                return "tenant_id";
            }
            // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
            @Override
            public boolean ignoreTable(String tableName) {
                // 只对user表生效
                return !"user".equalsIgnoreCase(tableName);
            }
        }));
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
//        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

//    @Bean
//    public ConfigurationCustomizer configurationCustomizer() {
//        return configuration -> configuration.setUseDeprecatedExecutor(false);
//    }
}

2、实体类

/**
 * 用户实体对应表 user
 */
@Data
@Accessors(chain = true) // 链式访问,该注解设置为chain=true,生成setter方法返回this(也就是返回的是对象),代替了默认的返回void
@TableName("user")
public class User {
    private Long id;
    /**
     * 租户 ID
     */
    private Long tenantId;
    private String name;

    @TableField(exist = false) // 表中不存在字段
    private String addrName;
}

3、数据库层/Mapper层

租户字段会自动拼接

public interface UserMapper extends BaseMapper<User> {
    /**
     * 自定义SQL:默认也会增加多租户条件
     */
    Integer myCount();

    // 多表也会自动加上
    List<User> getAddrAndUser(@Param("name") String name);
}

自定义 sql 的 xml 文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.baomidou.mybatisplus.samples.tenant.mapper.UserMapper">

    <-- <resultMap id="UserMap" type="com.ms.project.entity.User">
        <id property="id" column="id"/>
        <result property="name" column="global_id"/>
        <result property="gender" column="eqp_code"/>
        <result property="age" column="mp_code"/>
        <result property="telPhone" column="mp_name"/>
        <result property="registerMode" column="measure_parameter"/>
        <result property="thirdPartyId" column="measure_unit"/>
    </resultMap> -->

    <select id="myCount" resultType="java.lang.Integer">
        select count(1) from user
    </select>

    <select id="getAddrAndUser" resultType="com.baomidou.mybatisplus.samples.tenant.entity.User">
        select a.name as addr_name, u.id, u.name
        from user_addr a
        left join user u on u.id=a.user_id
        <where>
            <if test="name!=null">
                a.name like concat(concat('%',#{name}),'%')
            </if>
        </where>
    </select>
</mapper>

4、数据源配置和数据库表

application.yml 文件

# DataSource Config
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis-plus?useSSL=false
    username: root
    password: root

# Logger Config
logging:
  level:
    com.baomidou.mybatisplus.samples: debug

数据库脚本

DROP TABLE IF EXISTS user;

CREATE TABLE user
(
	id BIGINT(20) NOT NULL COMMENT '主键ID',
	tenant_id BIGINT(20) NOT NULL COMMENT '租户ID',
	name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
	PRIMARY KEY (id)
);

DROP TABLE IF EXISTS user_addr;

CREATE TABLE USER_ADDR
(
  id BIGINT(20) NOT NULL COMMENT '主键ID',
  user_id BIGINT(20) NOT NULL COMMENT 'user.id',
  name VARCHAR(30) NULL DEFAULT NULL COMMENT '地址名称',
  PRIMARY KEY (id)
);

-- 添加数据
DELETE FROM user;

INSERT INTO user (id, tenant_id, name) VALUES
(1, 1, 'Jone'),(2, 1, 'Jack'),(3, 1, 'Tom'),
(4, 0, 'Sandy'),(5, 0, 'Billie');

INSERT INTO user_addr (id, USER_ID, name) VALUES
(1, 1, 'addr1'),(2,1,'addr2');

补充依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- mybatis-plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <!-- MySQL 连接驱动依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

5、单元测试类

import com.baomidou.mybatisplus.samples.tenant.entity.User;
import com.baomidou.mybatisplus.samples.tenant.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.List;

/**
 * 多租户 Tenant 演示
 */
@SpringBootTest
public class TenantTest {
    @Resource
    private UserMapper mapper;

    // INSERT INTO user (id, name, tenant_id) VALUES (?, ?, 1)
    // SELECT id, tenant_id, name FROM user WHERE id = ? AND user.tenant_id = 1
    @Test
    public void aInsert() { // 添加
        User user = new User();
        user.setName("一一");
        Assertions.assertTrue(mapper.insert(user) > 0);
        user = mapper.selectById(user.getId());
        Assertions.assertTrue(1 == user.getTenantId());
    }

    @Test
    public void bDelete() { // 删除
        Assertions.assertTrue(mapper.deleteById(3L) > 0);
    }

    @Test
    public void cUpdate() { // 修改
        Assertions.assertTrue(mapper.updateById(new User().setId(1L).setName("mp")) > 0);
    }

    @Test
    public void dSelect() { // 查询
        List<User> userList = mapper.selectList(null);
        userList.forEach(u -> Assertions.assertTrue(1 == u.getTenantId()));
    }

    /**
     * 自定义SQL:默认也会增加多租户条件
     * 参考打印的SQL: SELECT count(1) FROM user WHERE user.tenant_id = 1
     */
    @Test
    public void manualSqlTenantFilterTest() {
        System.out.println(mapper.myCount());
    }

    // SELECT a.name AS addr_name, u.id, u.name FROM user_addr a LEFT JOIN user u ON u.id = a.user_id AND u.tenant_id = 1 WHERE a.name LIKE concat(concat('%', ?), '%')
    @Test
    public void testTenantFilter(){
        mapper.getAddrAndUser(null).forEach(System.out::println);
        mapper.getAddrAndUser("add").forEach(System.out::println);
    }
}

官方完整的代码地址:mybatis-plus-samples: MyBatis-Plus Samples 文档

二、生产示例

在实际开发环境中,可以更加灵活的处理

自定义 TenantLineHandler

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.NullValue;
import net.sf.jsqlparser.expression.StringValue;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * 租户维护处理器
 */
@Slf4j
public class MyTenantHandler implements TenantLineHandler {

	@Autowired
	private MyTenantConfigProperties properties;

	/**
	 * 获取租户 ID 值表达式,只支持单个 ID 值
	 * @return 租户 ID 值表达式
	 */
	@Override
	public Expression getTenantId() {
		String tenantId = TenantContextHolder.getTenantId();
		log.debug("当前租户为 >> {}", tenantId);
		if (StrUtil.isBlank(tenantId)) {
			return new NullValue(); // 数据库的值
		}
		return new StringValue(tenantId);
	}

	/**
	 * 获取租户字段名
	 * @return 租户字段名
	 */
	@Override
	public String getTenantIdColumn() {
		return properties.getColumn();
	}

	/**
	 * 根据表名判断是否忽略拼接多租户条件
	 * <p>
	 * 默认都要进行解析并拼接多租户条件
	 * @param tableName 表名
	 * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
	 */
	@Override
	public boolean ignoreTable(String tableName) {
		String tenantId = TenantContextHolder.getTenantId();
		// 租户中ID 为空,查询全部,不进行过滤
		if (StrUtil.isBlank(tenantId)) {
			return Boolean.TRUE;
		}
		return !properties.getTables().contains(tableName);
	}
}

使用 Properties 进行灵活的配置

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * 多租户配置
 */
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "my.mro.tenant")
public class MyTenantConfigProperties {
	/**
	 * 维护租户列名称
	 */
	private String column = "tenant_id";

	/**
	 * 多租户的数据表集合
	 */
	private List<String> tables = new ArrayList<>();
}

然后租户表可以在配置文件中进行维护,application.yml 文件维护示例如下:

# 租户表维护
my:
  mro:
    tenant:
      column: tenant_code
      tables:
        - fault_info
        - component_info
        - demand_acceptance
        - demand_detail

至此,租户方案完成。

  • 8
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值