MybatisPlus实现多租户架构(Multi-tenancy)

什么是多租户

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


业务场景

不同系统间共用同个数据库或者数据库表,如大企业具有内部的多个系统,但数据结构是相同的,可采用公共数据库,数据不同。


解决方案

租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。


为什么选择MyBatisPlus?

除了一些系统共用的表以外,其他租户相关的表,我们都需要在sql不厌其烦的加上AND t.provider_id = ?查询条件,稍不注意就会导致数据越界,数据安全问题让人担忧。好在有了MybatisPlus这个神器,可以极为方便的实现多租户SQL解析器


实现

每个数据表都需要有一个租户标识(provider_id)


建立数据库表

字段名	        字段类型	   描述
id	          BIGINT(20)    主键
provider_id	  BIGINT(20)	服务商ID
name	      VARCHAR(30)	姓名

provider_id作为租户Id,用于识别租户间的数据。

初始化数据如下

DROP TABLE IF EXISTS user;
CREATE TABLE user
(
    id BIGINT(20) NOT NULL COMMENT '主键',
    provider_id BIGINT(20) NOT NULL COMMENT '服务商ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    PRIMARY KEY (id)
);


INSERT INTO user (id, provider_id, name) VALUES (1, 1, 'Tony老师');
INSERT INTO user (id, provider_id, name) VALUES (2, 1, 'William老师');
INSERT INTO user (id, provider_id, name) VALUES (3, 2, '路人甲');
INSERT INTO user (id, provider_id, name) VALUES (4, 2, '路人乙');
INSERT INTO user (id, provider_id, name) VALUES (5, 2, '路人丙');
INSERT INTO user (id, provider_id, name) VALUES (6, 2, '路人丁');


新建SpringBoot环境

POM文件
集成mybatisPlus及msql数据库
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starterartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starterartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-testartifactId>
    <scope>testscope>
dependency>

<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <scope>providedscope>
dependency>

<dependency>
    <groupId>com.google.guavagroupId>
    <artifactId>guavaartifactId>
    <version>19.0version>
dependency>

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.0.5version>
dependency>

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plusartifactId>
    <version>3.0.5version>
dependency>

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-generatorartifactId>
    <version>3.0.5version>
dependency>


<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>5.1.38version>
dependency>

<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.1.10version>
dependency>

数据源配置文件(appliaction.yml)
spring:
    datasource:
      name: db
      type: com.alibaba.druid.pool.DruidDataSource                  
      url: jdbc:mysql://127.0.0.1:3306/user?characterEncoding=utf-8
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver

实体类及Mapper类

用户实体类

package com.syd.entity;

import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
@Data
@ToString
@Accessors(chain = true)
public class User {
    private Long id;
    private Long providerId;
    private String name;
}

用户UserMapper接口

package com.syd.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wuwenze.entity.User;
public interface UserMapper extends BaseMapper<User> {

}


ApiContext

模拟当前系统上下文,对 provider_id 进行取值、存值。

import com.google.common.collect.Maps;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class ApiContext {
    private static final String KEY_CURRENT_PROVIDER_ID = "KEY_CURRENT_PROVIDER_ID";
    private static final Map<String, Object> mContext = Maps.newConcurrentMap();

    public void setCurrentProviderId(Long providerId) {
        mContext.put(KEY_CURRENT_PROVIDER_ID, providerId);
    }

    public Long getCurrentProviderId() {
        return (Long) mContext.get(KEY_CURRENT_PROVIDER_ID);
    }
}


MybatisPlusConfig(核心配置)
package com.syd.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import com.google.common.collect.Lists;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
@MapperScan("com.syd.mapper")
public class MybatisPlusConfig {

    private static final String SYSTEM_TENANT_ID = "provider_id";
    private static final List<String> IGNORE_TENANT_TABLES = Lists.newArrayList("provider");

    @Autowired
    private ApiContext apiContext;

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

        // SQL解析处理拦截:增加租户处理回调。
        TenantSqlParser tenantSqlParser = new TenantSqlParser()
                .setTenantHandler(new TenantHandler() {

                    @Override
                    public Expression getTenantId() {
                        // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
                        Long currentProviderId = apiContext.getCurrentProviderId();
                        if (null == currentProviderId) {
                            throw new RuntimeException("#1129 getCurrentProviderId error.");
                        }
                        return new LongValue(currentProviderId);
                    }

                    @Override
                    public String getTenantIdColumn() {
                        return SYSTEM_TENANT_ID;
                    }

                    @Override
                    public boolean doTableFilter(String tableName) {
                        // 过滤掉一些表:如租户表(provider)本身不需要执行这样的处理。
                        return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));
                    }
                });
        paginationInterceptor.setSqlParserList(Lists.newArrayList(tenantSqlParser));
     return paginationInterceptor;
 }

  @Bean(name = "performanceInterceptor")
  public PerformanceInterceptor performanceInterceptor() {
      return new PerformanceInterceptor();
  }
}




测试
@Slf4j
@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.JVM)
@SpringBootTest(classes = MybatisPlusMultiTenancyApplication.class)
public class MybatisPlusMultiTenancyApplicationTests {

    @Test
    public void contextLoads() {
    }


    @Autowired
    private ApiContext apiContext;

    @Autowired
    private UserMapper userMapper;

    @Before
    public void before() {
        // 在上下文中设置当前服务商的ID
        apiContext.setCurrentProviderId(1L);
    }
    
    @Test
    public void selectList() {
        userMapper.selectList(null).forEach((e) -> {
            log.info("#selectList, e={}", e);
            // 验证查询的数据是否超出范围
            Assert.assertEquals(apiContext.getCurrentProviderId(), 
                                 e.getProviderId());
        });
    }
    
    @Test
    public void insert() {
        User user = new User().setName("新来的Tom老师");
        Assert.assertTrue(userMapper.insert(user) > 0);

        user = userMapper.selectById(user.getId());
        log.info("#insert user={}", user);

        // 检查插入的数据是否自动填充了租户ID
        Assert.assertEquals(apiContext.getCurrentProviderId(), user.getProviderId());
    }
}


测试结果

自动加上过滤条件

在这里插入图片描述


总结

使用 mybatis-plus 实现多租户只需要要在数据表里加上provider_id 识别字段,然后即可在查询、新增、删除、更新等SQL语句中自动加上where provider_id 字段。

你可能感兴趣的:(数据库,MyBatisPlus)