目录
一、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
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 {
/**
* 自定义SQL:默认也会增加多租户条件
*/
Integer myCount();
// 多表也会自动加上
List getAddrAndUser(@Param("name") String name);
}
自定义 sql 的 xml 文件
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');
补充依赖
org.springframework.boot
spring-boot-starter
com.baomidou
mybatis-plus-boot-starter
mysql
mysql-connector-java
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 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();
}
/**
* 根据表名判断是否忽略拼接多租户条件
*
* 默认都要进行解析并拼接多租户条件
* @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 tables = new ArrayList<>();
}
然后租户表可以在配置文件中进行维护,application.yml 文件维护示例如下:
# 租户表维护
my:
mro:
tenant:
column: tenant_code
tables:
- fault_info
- component_info
- demand_acceptance
- demand_detail
至此,租户方案完成。