Mybatisplus builds multi tenant mode (shared library table, distinguishing tenants by tenant id field)

preface

In recent work, I have encountered application scenarios of multi tenant mode, for which I have consulted a lot of materials. After analyzing the feasibility, the shared library table is selected, which is realized by distinguishing tenants according to the tenant id field. This record is convenient for future reference

1. Before getting familiar with multi tenancy, let's understand what SaaS system is

The following is Baidu Encyclopedia

SaaS platform is a platform for operating SaaS software. SaaS providers build all the network infrastructure, software and hardware operation platforms required by informatization for enterprises, and are responsible for a series of services such as early implementation and later maintenance. Enterprises can use the information system through the Internet without purchasing software and hardware, building computer rooms and recruiting IT personnel. SaaS is a software layout model. Its application is specially designed for network delivery, which is convenient for users to host, deploy and access through the Internet.

In other words, I only need to be able to connect to the Internet and pay the rent to the saas platform, so I can use the system services provided by the saas platform. The most typical examples of this are various cloud platforms, such as Alibaba cloud. Since I can use the services provided by saas platform through the Internet, others can, of course. This creates a multi tenant problem.

2. What is the multi tenant model

Multi tenancy, in short, is an architectural design method, which is a saas system running on one or a group of servers, which can provide services for multiple tenants (customers). The purpose is to enable multiple tenants to use the same set of programs in the Internet environment and ensure data isolation between tenants. From the pattern of this architecture design, it is not difficult to see that the focus of multi tenant architecture is the isolation of data from multiple tenants under the same set of programs. Since the tenant data is stored centrally, the security of the data depends on whether the tenant data can be isolated to prevent the tenant data from being inadvertently or maliciously acquired and tampered with by others.

3. Multi tenant data isolation method

At present, there are three solutions for data isolation of saas multi tenant system, namely:

  • Provide independent database for each tenant
  • Independent tablespaces
  • Tenants by field

Each scheme has its own application.

3.1. Independent data source provided by each tenant

The implementation of this scheme is that all tenants share the same application, but the application back end will connect multiple database systems, and one tenant uses one database system alone. This scheme has the highest level of user data isolation and the best security, and the data between tenants can be physically isolated. But the cost is high.

As shown in the figure below:

3.2. Each tenant provides a separate table space

The implementation of this scheme is that all tenants share the same application, the application back end is only connected to one database system, all tenants share the database system, and each tenant has an independent table space in the database system. The data table structure in the table space is the same. DB2, ORACLE and PostgreSQL. There can be multiple schemas under one database (in mysql, there are multiple databases)

As shown in the figure below:

3.3. Distinguish tenants by tenant id field

This scheme is the simplest design method in the multi tenant scheme, that is, a field used to distinguish tenants (such as tenant id or tenant code) is added to each table to identify which tenant each data belongs to. Its function is much like a foreign key. When querying, each statement should add this field as a filter condition. Its feature is that all tenants' data are stored in the same table, and the data isolation is the lowest, which is completely distinguished by fields.

3.4. Analysis of advantages and disadvantages of three data isolation schemes

Isolation schemecostNumber of tenants supportedadvantageInsufficient
Independent database systemhighlessWith the highest isolation level and the best security, it can meet the unique needs of different tenants, and it is easier to recover data in case of failureThe database needs to be installed independently, and the maintenance cost and purchase cost are high
Shared database, independent tablespaceinMoreIt provides a certain degree of logical data isolation, and a database system can support multiple tenantsIn case of failure, data recovery is relatively complex
Distinguished by tenant id fieldlowA lotThe maintenance and purchase costs are the lowest, and each database can support the largest number of tenantsThe isolation level is the lowest and the security is the lowest. The data backup and recovery is very complex. It needs to be backed up and restored table by table and item by item

4. Use Mybatisplus to build a multi tenant mode (implementation of mode 3: share the library table and distinguish tenants by tenant id field)

4.1.MyBatisPlusConfig.java

package com.bitvalue.gp.sys.config;

import com.bitvalue.gp.sys.core.mybatis.dbid.GunsDatabaseIdProvider;
import com.bitvalue.gp.sys.core.mybatis.fieldfill.CustomMetaObjectHandler;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatisPlusConfig Extension configuration
 *
 * @author tangling
 * @date 2021/4/18 10:49
 */
@Configuration
//Scan mapper
@MapperScan(basePackages = {"com.bitvalue.gp.**.mapper"})
public class MyBatisPlusConfig {

    /**
     * mp Multi tenant configuration
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // Multi tenant plug-in
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new CustomTenantLineHandler()));
        // Paging plug-in (ps: if paging plug-in is used in the project, the following line of code can be added, but it must be written after the multi tenant plug-in)
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

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

    /**
     * Automatic injection of custom public fields
     */
    @Bean
    public MetaObjectHandler metaObjectHandler() {
        //Customize sql field filler to automatically fill, create and modify relevant fields
        return new CustomMetaObjectHandler();
    }
}

4.2. Multi tenant plug-in | customtenantlinehandler java

The CustomTenantLineHandler class implements the TenantLineHandler interface and implements

  • getTenantId() method, which is mainly used to set the value of tenant Id, rewrite the SQL statement before the framework processes the SQL statement, and add tenant judgment conditions to the SQL statement. Tenant Id can be obtained from cache, cookie, token, etc. (according to the actual business scenario)
  • getTenantIdColumn() method, which is used to set the field name of tenant Id
  • ignoreTable(String tableName) method, which is used to mark the table that ignores adding tenant ID

The main core is the getTenantId() method. We need to consider how to set the value of the tenant Id and whether the set colleagues will have thread safety problems (most articles are assigned through a field class in a Bean, which may have thread safety problems).

My idea here is that after the user logs in successfully. Store the user's basic information into the context object of the security framework, and generate a token for the user's basic information and tenant Id and return it to the requester. When the requestor visits again, it will carry the token (first intercept the request in the filter and verify that the token can be parsed) for a series of business operations. Finally, when it wants to execute the SQL statement, it will come to the tenant listener and get and set the tenant Id here. Here, I get the token from the request header and get the tenant Id by parsing the token.
There may also be a case that if it is a call between internal mapper s, there is no HttpServerRequest, the token cannot be obtained, and an error will be reported. Here I try/catch this.
The internal call problem is handled in the catch structure by obtaining the account of the currently logged in user from the context object and obtaining the tenant information of the user from the cache according to the user's account.
ps: user account: Tenant Id information is stored in redis after the Spring container is initialized

package com.bitvalue.gp.sys.config;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.bitvalue.gp.core.consts.CommonConstant;
import com.bitvalue.gp.core.context.login.LoginContextHolder;
import com.bitvalue.gp.core.exception.AuthException;
import com.bitvalue.gp.core.exception.ServiceException;
import com.bitvalue.gp.core.pojo.login.SysLoginUser;
import com.bitvalue.gp.sys.core.jwt.JwtPayLoad;
import com.bitvalue.gp.sys.core.jwt.JwtTokenUtil;
import com.bitvalue.gp.core.util.HttpServletUtil;
import com.bitvalue.gp.sys.modular.auth.service.AuthService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

/**
 * Multi tenant processing plug-in
 *
 * @author tangling
 * @date 2021/04/26 13:37
 */
@Slf4j
public class CustomTenantLineHandler implements TenantLineHandler {

    /**
     * User corresponding tenant information cache key
     */
    public static final String TENANT_CASE_KEY = "TENANT_CASE_KEY";

    /**
     * Ignore tables that add tenant ID S
     */
    private static List<String> IGNORE_TABLE_NAMES = Lists.newArrayList(
            "tenant_info"
    );

    /**
     * Get tenant ID value expression
     *
     * @return
     */
    @Override
    public Expression getTenantId() {
        //Tenant Id, which can be obtained from cache, cookie, token, etc
        return new LongValue(returnTenantId());
    }

    /**
     * Get tenant field name (tenant ID field name of database)
     *
     * @return
     */
    @Override
    public String getTenantIdColumn() {
        return "tenant_id";
    }

    /**
     * Judge whether to ignore the splicing multi tenant condition according to the table name
     *
     * @param tableName
     * @return
     */
    @Override
    public boolean ignoreTable(String tableName) {
        return IGNORE_TABLE_NAMES.contains(tableName);
    }

    /**
     * Get the token from the request and parse the tenantId from the token
     *
     * @return
     */
    public Long returnTenantId() {
        //The initialization value is saved and the program starts normally
        Long tenantId = 1L;
        //Get token from request header
        try {
            HttpServletRequest request = HttpServletUtil.getRequest();
            AuthService authService = SpringUtil.getBean(AuthService.class);
            String token = authService.getTokenFromRequest(request);
            //token in request header
            if (StringUtils.isNotEmpty(token)) {
                JwtPayLoad jwtPayLoad = JwtTokenUtil.getJwtPayLoad(token);
                tenantId = jwtPayLoad.getTenantId();
            }
        } catch (ServiceException exception) {
            log.info(">>> No, HTTP Service request processing method!");
            //There is no HTTP request processing method
            try {
                //Obtain the current login user account from the authentication context object in security, and match the current operation to the tenant ID in the redis cache according to the account
                String account = LoginContextHolder.me().getSysLoginUser().getAccount();
                if (StringUtils.isNotEmpty(account)) {
                    RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate");
                    Object object = redisTemplate.opsForValue().get(TENANT_CASE_KEY);
                    String jsonMap = object.toString();
                    Map accountTenantMapper = JSON.parseObject(jsonMap, Map.class);
                    tenantId = Long.valueOf(String.valueOf((accountTenantMapper.get(account))));
                    log.info(">>> Internal call! User account:" + account + " | Tenant:" + tenantId);
                }else {
                    log.info(">>> The required parameters are missing!");
                }
            } catch (AuthException e) {
                log.info(">>> " + e.getMessage());
            }
        }
        return tenantId;
    }
}

4.3.HttpServletUtil.java

package com.bitvalue.gp.core.util;

import com.bitvalue.gp.core.exception.ServiceException;
import com.bitvalue.gp.core.exception.enums.ServerExceptionEnum;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * HttpServlet Tool class to obtain the current request and response
 *
 * @author tangling
 * @date 2021/3/30 15:09
 */
public class HttpServletUtil {

    /**
     * Gets the request object of the current request
     */
    public static HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new ServiceException(ServerExceptionEnum.REQUEST_EMPTY);
        } else {
            return requestAttributes.getRequest();
        }
    }

    /**
     * Gets the response object of the current request
     */
    public static HttpServletResponse getResponse() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            throw new ServiceException(ServerExceptionEnum.REQUEST_EMPTY);
        } else {
            return requestAttributes.getResponse();
        }
    }
}

4.4. Customize sql field filler to automatically fill, create and modify relevant fields | custommetaobjecthandler java

package com.bitvalue.gp.sys.core.mybatis.fieldfill;

import cn.hutool.log.Log;
import com.bitvalue.gp.core.context.login.LoginContextHolder;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectionException;

import java.util.Date;

/**
 * Customize sql field filler to automatically fill, create and modify relevant fields
 *
 * @author tangling
 * @date 2021/3/30 15:21
 */
public class CustomMetaObjectHandler implements MetaObjectHandler {

    private static final Log log = Log.get();

    private static final String CREATE_USER = "createUser";

    private static final String CREATE_TIME = "createTime";

    private static final String UPDATE_USER = "updateUser";

    private static final String UPDATE_TIME = "updateTime";

    @Override
    public void insertFill(MetaObject metaObject) {
        try {
            //Set createUser (BaseEntity)
            setFieldValByName(CREATE_USER, this.getUserUniqueId(), metaObject);

            //Set createTime (BaseEntity)
            setFieldValByName(CREATE_TIME, new Date(), metaObject);
        } catch (ReflectionException e) {
            log.warn(">>> CustomMetaObjectHandler There are no relevant fields in the processing process, so it will not be processed");
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        try {
            //Set updateUser (BaseEntity)
            setFieldValByName(UPDATE_USER, this.getUserUniqueId(), metaObject);
            //Set updateTime (BaseEntity)
            setFieldValByName(UPDATE_TIME, new Date(), metaObject);
        } catch (ReflectionException e) {
            log.warn(">>> CustomMetaObjectHandler There are no relevant fields in the processing process, so it will not be processed");
        }
    }

    /**
     * Get user unique id
     */
    private Long getUserUniqueId() {
        try {
            return LoginContextHolder.me().getSysLoginUserId();
        } catch (Exception e) {
            //If not, return - 1
            return -1L;
        }
    }
}

5. Recommendation of relevant good articles

mybatisplus small book multi tenant solution

mybatisplus official website multi tenant solution

Springboot + mybatis plus to implement multi tenant dynamic data source mode

Keywords: Java mybatis-plus MybatisPlus

Added by gmann001 on Fri, 11 Feb 2022 04:30:15 +0200