SpringBoot第30讲:SpringBoot集成MySQL - MyBatis-Plus基于字段隔离的多租户

SpringBoot第30讲:SpringBoot集成MySQL - MyBatis-Plus基于字段隔离的多租户

本文是SpringBoot第30讲,主要介绍 MyBatis-Plus的基于字段隔离的多租户实现,以及MyBatis-Plus的基于字段的隔离方式实践和原理。

文章目录

  • SpringBoot第30讲:SpringBoot集成MySQL - MyBatis-Plus基于字段隔离的多租户
    • 1、知识准备
      • 1.1、什么是多租户?
      • 1.2、多租户在数据存储上有哪些实现方式?
        • 1、DB隔离:独立数据库
        • 2、Schema隔离:共享数据库,隔离数据架构
        • 3、字段隔离:共享数据库,共享数据架构
      • 1.3、MyBatis-Plus的基于字段的隔离方式原理是什么?
    • 2、简单示例
      • 2.1、准备DB和依赖配置
      • 2.2、MyBatis-Plus配置
      • 2.3、定义dao
      • 2.4、定义Service接口和实现类
      • 2.5、controller
      • 2.6、简单测试
    • 3、进一步理解
      • 3.1、来自官方的注意点
      • 3.2、插件的顺序
      • 3.3、封装性实践
    • 4、示例源码

1、知识准备

需要了解多租户及常见的实现方式,以及MyBatis-Plus的基于字段的隔离方式原理。

1.1、什么是多租户?

如下解释来源于百度百科

多租户技术(英语:multi-tenancy technology)或称多重租赁技术,是一种软件架构技术,它是在探讨与实现如何于多用户的环境下共用相同的系统或程序组件,并且仍可确保各用户间数据的隔离性

多租户简单来说是指一个单独的实例可以为多个组织服务。多租户技术为共用的数据中心内如何以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍然可以保障客户的数据隔离。一个支持多租户技术的系统需要在设计上对它的数据和配置进行虚拟分区,从而使系统的每个租户或称组织都能够使用一个单独的系统实例,并且每个租户都可以根据自己的需求对租用的系统实例进行个性化配置。

多租户技术可以实现多个租户之间共享系统实例,同时又可以实现租户的系统实例的个性化定制。通过使用多租户技术可以保证系统共性的部分被共享,个性的部分被单独隔离。通过在多个租户之间的资源复用,运营管理维护资源,有效节省开发应用的成本。而且,在租户之间共享应用程序的单个实例,可以实现当应用程序升级时,所有租户可以同时升级。同时,因为多个租户共享一份系统的核心代码,因此当系统升级时,只需要升级相同的核心代码即可。

1.2、多租户在数据存储上有哪些实现方式?

如下解释来源于百度百科

多租户在数据存储上存在三种主要的方案,分别是

1、DB隔离:独立数据库

这是第一种方案,即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本也高。

  • 优点
  1. 为不同的租户提供独立的数据库,有助于简化数据模型的扩展设计,满足不同租户的独特需求;
  2. 如果出现故障,恢复数据比较简单。
  • 缺点
  1. 增大了数据库的安装数量,随之带来维护成本和购置成本的增加。
  2. 这种方案与传统的一个客户、一套数据、一套部署类似,差别只在于软件统一部署在运营商那里。如果面对的是银行、医院等需要非常高数据隔离级别的租户,可以选择这种模式,提高租用的定价。如果定价较低,产品走低价路线,这种方案一般对运营商来说是无法承受的。

2、Schema隔离:共享数据库,隔离数据架构

这是第二种方案,即多个或所有租户共享Database,但一个租户(Tenant)一个Schema

  • 优点
  1. 为安全性要求较高的租户提供了一定程度的逻辑数据隔离,并不是完全隔离;每个数据库可以支持更多的租户数量。
  • 缺点
  1. 如果出现故障,数据恢复比较困难,因为恢复数据库将牵扯到其他租户的数据;
  2. 如果需要跨租户统计数据,存在一定困难。

3、字段隔离:共享数据库,共享数据架构

这是第三种方案,即租户共享同一个Database、同一个Schema,但在表中通过TenantID区分租户的数据。这是共享程度最高、隔离级别最低的模式。

  • 优点
  1. 三种方案比较,第三种方案的维护和购置成本最低,允许每个数据库支持的租户数量最多。
  • 缺点
  1. 隔离级别最低,安全性最低,需要在设计开发时加大对安全的开发量;
  2. 数据备份和恢复最困难,需要逐表逐条备份和还原。
  3. 如果希望以最少的服务器为最多的租户提供服务,并且租户接受以牺牲隔离级别换取降低成本,这种方案最适合。

1.3、MyBatis-Plus的基于字段的隔离方式原理是什么?

这里请看MyBatis的插件机制:MyBatis第八讲:MyBatis插件机制详解与实战

2、简单示例

这里沿用之前的db_user,在表中添加tenant_id,并命名为新的schema db_user_tenant。

2.1、准备DB和依赖配置

创建MySQL的schema db_user_tenant, 导入SQL 文件如下

DROP TABLE IF EXISTS `tb_role`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tb_role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `tenant_id` int DEFAULT NULL,
  `name` varchar(255) NOT NULL,
  `role_key` varchar(255) NOT NULL,
  `description` varchar(255) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `tb_role`
--

LOCK TABLES `tb_role` WRITE;
/*!40000 ALTER TABLE `tb_role` DISABLE KEYS */;
INSERT INTO `tb_role` VALUES (1,1,'admin','admin','admin','2021-09-08 17:09:15','2021-09-08 17:09:15');
/*!40000 ALTER TABLE `tb_role` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tb_user`
--

DROP TABLE IF EXISTS `tb_user`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tb_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `tenant_id` int DEFAULT NULL,
  `user_name` varchar(45) NOT NULL,
  `password` varchar(45) NOT NULL,
  `email` varchar(45) DEFAULT NULL,
  `phone_number` int DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `tb_user`
--

LOCK TABLES `tb_user` WRITE;
/*!40000 ALTER TABLE `tb_user` DISABLE KEYS */;
INSERT INTO `tb_user` VALUES (1,1,'qiwenjie','qwj930828','1172814226@qq.com',1212121213,'afsdfsaf','2021-09-08 17:09:15','2021-09-08 17:09:15');
/*!40000 ALTER TABLE `tb_user` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `tb_user_role`
--

DROP TABLE IF EXISTS `tb_user_role`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `tb_user_role` (
  `user_id` int NOT NULL,
  `role_id` int NOT NULL,
  `tenant_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `tb_user_role`
--

LOCK TABLES `tb_user_role` WRITE;
/*!40000 ALTER TABLE `tb_user_role` DISABLE KEYS */;
INSERT INTO `tb_user_role` VALUES (1,1,1);
/*!40000 ALTER TABLE `tb_user_role` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2022-04-02 12:50:14

引入maven依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

增加yml配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_user_tenant?useSSL=false&autoReconnect=true&characterEncoding=utf8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: qwj930828

mybatis-plus:
  configuration:
  	# 开启二级缓存
    cache-enabled: true
    use-generated-keys: true
    default-executor-type: REUSE
    use-actual-param-name: true
    # 输出SQL log 方便 debug
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

2.2、MyBatis-Plus配置

通过添加 TenantLineInnerInterceptor 来完成。

package springboot.mysql.mybatisplus.tenant.config;

import java.util.List;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.schema.Column;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-plus configuration, add pagination interceptor.
 *
 * @author qiwenjie
 */
@Configuration
public class MyBatisConfig {

    /**
     * inject pagination interceptor.
     *
     * @return pagination
     */
    @Bean
    public PaginationInnerInterceptor paginationInnerInterceptor() {
        return new PaginationInnerInterceptor();
    }

    /**
     * add interceptor.
     *
     * @return MybatisPlusInterceptor
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // TenantLineInnerInterceptor
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                // 实际可以将TenantId放在threadLocal中(比如xxxxContext中),并获取。
                return new LongValue(1);
            }

            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            @Override
            public boolean ignoreTable(String tableName) {
                return false;
            }

            @Override
            public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
                return TenantLineHandler.super.ignoreInsert(columns, tenantIdColumn);
            }
        }));
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor,防止分页失效
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

2.3、定义dao

RoleDao

package springboot.mysql.mybatisplus.tenant.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import springboot.mysql.mybatisplus.tenant.entity.Role;

/**
 * @author qiwenjie
 */
public interface IRoleDao extends BaseMapper<Role> {
}

UserDao

package springboot.mysql.mybatisplus.tenant.dao;

import java.util.List;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import springboot.mysql.mybatisplus.tenant.entity.User;
import springboot.mysql.mybatisplus.tenant.entity.query.UserQueryBean;

/**
 * @author qiwenjie
 */
public interface IUserDao extends BaseMapper<User> {

    List<User> findList(UserQueryBean userQueryBean);
}

这里你也同时可以支持BaseMapper方式和自己定义的xml的方法(比较适用于关联查询),比如findList是自定义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="springboot.mysql.mybatisplus.tenant.dao.IUserDao">

	<resultMap type="springboot.mysql.mybatisplus.tenant.entity.User" id="UserResult">
		<id     property="id"       	column="id"      		/>
		<result property="userName"     column="user_name"    	/>
		<result property="password"     column="password"    	/>
		<result property="email"        column="email"        	/>
		<result property="phoneNumber"  column="phone_number"  	/>
		<result property="description"  column="description"  	/>
		<result property="createTime"   column="create_time"  	/>
		<result property="updateTime"   column="update_time"  	/>
		<collection property="roles" ofType="springboot.mysql.mybatisplus.tenant.entity.Role">
			<result property="id" column="id"  />
			<result property="name" column="name"  />
			<result property="roleKey" column="role_key"  />
			<result property="description" column="description"  />
			<result property="createTime"   column="create_time"  	/>
			<result property="updateTime"   column="update_time"  	/>
		</collection>
	</resultMap>
	
	<sql id="selectUserSql">
        select u.id, u.password, u.user_name, u.email, u.phone_number, u.description, u.create_time, u.update_time, r.name, r.role_key, r.description, r.create_time, r.update_time
		from tb_user u
		left join tb_user_role ur on u.id=ur.user_id
		inner join tb_role r on ur.role_id=r.id
    </sql>
	
	<select id="findList" parameterType="springboot.mysql.mybatisplus.tenant.entity.query.UserQueryBean" resultMap="UserResult">
		<include refid="selectUserSql"/>
		where u.id != 0
		<if test="userName != null and userName != ''">
			AND u.user_name like concat('%', #{user_name}, '%')
		</if>
		<if test="description != null and description != ''">
			AND u.description like concat('%', #{description}, '%')
		</if>
		<if test="phoneNumber != null and phoneNumber != ''">
			AND u.phone_number like concat('%', #{phoneNumber}, '%')
		</if>
		<if test="email != null and email != ''">
			AND u.email like concat('%', #{email}, '%')
		</if>
	</select>
</mapper> 

2.4、定义Service接口和实现类

UserService接口

package springboot.mysql.mybatisplus.tenant.service;

import java.util.List;

import com.baomidou.mybatisplus.extension.service.IService;
import springboot.mysql.mybatisplus.tenant.entity.User;
import springboot.mysql.mybatisplus.tenant.entity.query.UserQueryBean;

/**
 * @author qiwenjie
 */
public interface IUserService extends IService<User> {

    List<User> findList(UserQueryBean userQueryBean);
}

User Service的实现类

package springboot.mysql.mybatisplus.tenant.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import tech.pdai.springboot.mysql8.mybatisplus.tenant.dao.IUserDao;
import tech.pdai.springboot.mysql8.mybatisplus.tenant.entity.User;
import tech.pdai.springboot.mysql8.mybatisplus.tenant.entity.query.UserQueryBean;
import tech.pdai.springboot.mysql8.mybatisplus.tenant.service.IUserService;
import java.util.List;

// 实现ServiceImpl接口,可以使得代码复用
@Service
public class UserDoServiceImpl extends ServiceImpl<IUserDao, User> implements IUserService {

    @Override
    public List<User> findList(UserQueryBean userQueryBean) {
        return baseMapper.findList(userQueryBean);
    }
}

Role Service 接口

package springboot.mysql.mybatisplus.tenant.service;

import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import springboot.mysql.mybatisplus.tenant.entity.Role;
import springboot.mysql.mybatisplus.tenant.entity.query.RoleQueryBean;

public interface IRoleService extends IService<Role> {

    List<Role> findList(RoleQueryBean roleQueryBean);
}

Role Service 实现类

package springboot.mysql.mybatisplus.tenant.service.impl;

import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import springboot.mysql.mybatisplus.tenant.dao.IRoleDao;
import springboot.mysql.mybatisplus.tenant.entity.Role;
import springboot.mysql.mybatisplus.tenant.entity.query.RoleQueryBean;
import springboot.mysql.mybatisplus.tenant.service.IRoleService;

@Service
public class RoleDoServiceImpl extends ServiceImpl<IRoleDao, Role> implements IRoleService {

    @Override
    public List<Role> findList(RoleQueryBean roleQueryBean) {
        return lambdaQuery().like(StringUtils.isNotEmpty(roleQueryBean.getName()), Role::getName, roleQueryBean.getName())
                .like(StringUtils.isNotEmpty(roleQueryBean.getDescription()), Role::getDescription, roleQueryBean.getDescription())
                .like(StringUtils.isNotEmpty(roleQueryBean.getRoleKey()), Role::getRoleKey, roleQueryBean.getRoleKey())
                .list();
    }
}

2.5、controller

User Controller

package springboot.mysql.mybatisplus.tenant.controller;

import java.time.LocalDateTime;
import java.util.List;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springboot.mysql.mybatisplus.tenant.entity.User;
import springboot.mysql.mybatisplus.tenant.entity.query.UserQueryBean;
import springboot.mysql.mybatisplus.tenant.entity.response.ResponseResult;
import springboot.mysql.mybatisplus.tenant.service.IUserService;

/**
 * @author qiwenjie
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    /**
     * @param user user param
     * @return user
     */
    @ApiOperation("Add/Edit User")
    @PostMapping("add")
    public ResponseResult<User> add(User user) {
        if (user.getId()==null) {
            user.setCreateTime(LocalDateTime.now());
        }
        user.setUpdateTime(LocalDateTime.now());
        userService.save(user);
        return ResponseResult.success(userService.getById(user.getId()));
    }

    /**
     * @return user list
     */
    @ApiOperation("Query User One")
    @GetMapping("edit/{userId}")
    public ResponseResult<User> edit(@PathVariable("userId") Long userId) {
        return ResponseResult.success(userService.getById(userId));
    }

    /**
     * @return user list
     */
    @ApiOperation("Query User List")
    @GetMapping("list")
    public ResponseResult<List<User>> list(UserQueryBean userQueryBean) {
        return ResponseResult.success(userService.findList(userQueryBean));
    }
}

Role Controller

package springboot.mysql.mybatisplus.tenant.controller;

import java.util.List;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springboot.mysql.mybatisplus.tenant.entity.Role;
import springboot.mysql.mybatisplus.tenant.entity.query.RoleQueryBean;
import springboot.mysql.mybatisplus.tenant.entity.response.ResponseResult;
import springboot.mysql.mybatisplus.tenant.service.IRoleService;

/**
 * @author qiwenjie
 */
@RestController
@RequestMapping("/role")
public class RoleController {

    @Autowired
    private IRoleService roleService;

    /**
     * @return role list
     */
    @ApiOperation("Query Role List")
    @GetMapping("list")
    public ResponseResult<List<Role>> list(RoleQueryBean roleQueryBean) {
        return ResponseResult.success(roleService.findList(roleQueryBean));
    }
}

2.6、简单测试

访问页面:http://localhost:8080/doc.html在这里插入图片描述

拦截之前的SQL

original SQL: select u.id, u.password, u.user_name, u.email, u.phone_number, u.description, u.create_time, u.update_time, r.name, r.role_key, r.description, r.create_time, r.update_time
		from tb_user u
		left join tb_user_role ur on u.id=ur.user_id
		inner join tb_role r on ur.role_id=r.id  
		where u.id != 0

最后执行的SQL中,对联表查询的每个表都加了:tenant_id

023-08-03 11:36:37.519  INFO 10091 --- [nio-8080-exec-5] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1372159021 wrapping com.mysql.cj.jdbc.ConnectionImpl@681a68fa] will not be managed by Spring
==>  Preparing: SELECT u.id, u.password, u.user_name, u.email, u.phone_number, u.description, u.create_time, u.update_time, r.id rid, r.name rname, r.role_key, r.description rdescription, r.create_time rcreate_time, r.update_time rupdate_time FROM tb_user u LEFT JOIN tb_user_role ur ON u.id = ur.user_id AND ur.tenant_id = 1 INNER JOIN tb_role r ON ur.role_id = r.id AND u.tenant_id = 1 AND r.tenant_id = 1 WHERE u.id != 0
==> Parameters: 
<==    Columns: id, password, user_name, email, phone_number, description, create_time, update_time, rid, rname, role_key, rdescription, rcreate_time, rupdate_time
<==        Row: 1, qwj930828, qiwenjie, 1172814226@qq.com, 1212121213, afsdfsaf, 2021-09-08 17:09:15, 2021-09-08 17:09:15, 1, admin, admin, admin, 2021-09-08 17:09:15, 2021-09-08 17:09:15
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@78cf36be]

3、进一步理解

在实际使用字段进行多租户隔离时有哪些注意点呢?

3.1、来自官方的注意点

相关建议

  1. 多租户 != 权限过滤, 不要乱用, 租户之间是完全隔离的!!!
  2. 启用多租户后所有执行的method的sql都会进行处理.
  3. 自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
    1. 不推荐使用官方的,对于权限过滤可以自行魔改,参考这篇项目实战:todo

3.2、插件的顺序

MyBatis-Plus使用多个功能插件需要注意顺序关系

MyBatis-Plus基于字段的多租户是通过插件机制拦截实现的,因为还有很多其它的拦截器,比如:

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

所以需要注意顺序: 使用多个功能需要注意顺序关系,建议使用如下顺序

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

总结: 对 sql 进行单次改造的优先放入,不对 sql 进行改造的最后放入

3.3、封装性实践

实际项目中还需要对配置进行封装。

回看如下的处理, 我们看下可以封装的点:

// TenantLineInnerInterceptor
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
    @Override
    public Expression getTenantId() {
        // 实际可以将TenantId放在threadLocal中(比如xxxxContext中),并获取。
      	// 在实际项目中,我们会从网关上下文中获取用户信息
        return new LongValue(1);
    }

    @Override
    public String getTenantIdColumn() {
        return "tenant_id";
    }

    @Override
    public boolean ignoreTable(String tableName) {
        return false;
    }

    @Override
    public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
        return TenantLineHandler.super.ignoreInsert(columns, tenantIdColumn);
    }
}));

1、对于配置

  • 相关配置可以封装到yml, 然后注入进来。

2、对于TenantId

  • 实际可以将TenantId放在threadLocal中(比如xxxxContext中),并获取。

3、对于ignoreTable

  • 比如有些表不要自动进行拦截的,可以在yml中配置并重写ignoreTable方法。

4、对于ignoreInsert

  • 对于插入数据是否需要携带TenantId,可以通过重写ignoreInsert方法。

4、示例源码

todo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/61948.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Qt5.13引入QtWebApp的模块后报错: error C2440: “reinterpret_cast”: 无法从“int”转换为“quintptr”

1、开发环境 Win10-64 qt5.13 msvc2015-64bit-release 2、报错 新建一个demo工程。 引入QtWebApp的httpserver、logging、templateengine三个模块后。 直接运行&#xff0c;&#xff0c;此时报错如下&#xff1a; E:\Qt5.13.1\install\5.13.1\msvc2015_64\include\QtCore…

k8s-服务发现service和ingress

回到目录 service用于集群内部应用的网络调用&#xff0c;处理东西流量 ingress用于集群外部用户访问内部服务&#xff0c;处理南北流量 一 kube-proxy三种代理模式 kubernetes集群中有三层网络&#xff0c;一类是真实存在的&#xff0c;例如Node Network、Pod Network,提供真…

高级web前端开发工程师工作的岗位职责(合集)

高级web前端开发工程师工作的岗位职责1 职责&#xff1a; 1、负责产品web端界面设计与用户体验&#xff0c;包括界面操作、用户交互设计; 2、负责基于Html5的web网页前端开发&#xff0c;要求适配主流浏览器; 3、与后端开发工程师合作&#xff0c;高效完成产品模型的展现及交…

命令模式 Command Pattern 《游戏设计模式》学习笔记

对于一般的按键输入&#xff0c;我们通常这么做&#xff0c;直接if按了什么键&#xff0c;就执行相应的操作 在这里我们是将用户的输入和程序行为硬编码在一起&#xff0c;这是我们很自然就想到的最快的做法。 但是如果这是一个大型游戏&#xff0c;往往我们需要实现一个按键…

Java转换流

文章目录 转换流的理解**InputStreamReader****OutputStreamWriter**练习 引入情况1&#xff1a; 使用FileReader 读取项目中的文本文件。由于IDEA设置中针对项目设置了UTF-8编码&#xff0c;当读取Windows系统中创建的文本文件时&#xff0c;如果Windows系统默认的是GBK编码&a…

Spring Boot3.0(一):入门篇

什么是 Spring Boot Spring Boot 是由 Pivotal 团队提供的全新框架&#xff0c;其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。 用我的话来理解&#xff0c;就是 Spring…

深入浅出 Typescript

TypeScript 是 JavaScript 的一个超集&#xff0c;支持 ECMAScript 6 标准&#xff08;ES6 教程&#xff09;。 TypeScript 由微软开发的自由和开源的编程语言。 TypeScript 设计目标是开发大型应用&#xff0c;它可以编译成纯 JavaScript&#xff0c;编译出来的 JavaScript …

C# OpenCvSharp 去水印 图像修复

效果 项目 VS2010.net4.0OpenCvSharp3 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.IO; using System…

【解放ipad生产力】如何在平板上使用免费IDE工具完成项目开发

我的博客即将同步至腾讯云开发者社区&#xff0c;邀请大家一同入驻&#xff1a;https://cloud.tencent.com/developer/support-plan?invite_code3o19zyy2pneoo 前言 很多人应该会像我一样吧&#xff0c;有时候身边没电脑突然要写项目&#xff0c;发现自己的平板没有一点作用&…

《HeadFirst设计模式(第二版)》第五章代码——单例模式

代码文件目录&#xff1a; 初始版本&#xff1a; package Chapter5_SingletonPattern.origin;/*** Author 竹心* Date 2023/8/5**/public class Singleton {private static Singleton uniqueInstance;private Singleton(){}public static Singleton getInstance(){if(uniqueIn…

C++ 派生类成员的标识与访问——作用域分辨符

在派生类中&#xff0c;成员可以按访问属性分为以下四种&#xff1a; &#xff08;1&#xff09;不可访问成员。这是从基类私有成员继承下来的&#xff0c;派生类或是建立派生类对象的模块都无法访问到它们&#xff0c;如果从派生类继续派生新类&#xff0c;也是无法访问的。 &…

京东开源的、高效的企业级表格可视化搭建解决方案:DripTable

DripTable 是京东零售推出的一款用于企业级中后台的动态列表解决方案&#xff0c;项目基于 React 和 JSON Schema&#xff0c;旨在通过简单配置快速生成页面动态列表来降低列表开发难度、提高工作效率。 DripTable 目前包含以下子项目&#xff1a;drip-table、drip-table-gene…

Qt下开发基于QGIS的应用程序

Qt下开发基于QGIS的应用程序 目的版本说明1、Qt的安装2、MSVC套件与Windows 10 SDK的下载3、QGIS开发有关的库文件下载4、环境搭建5、QGIS开发环境搭建6、展示网页地图 目的 由于有在背景地图上进行动态轨迹&#xff08;曲线&#xff09;显示的需要&#xff0c;故采用QtQGIS的…

MySQL 与MongoDB区别

一、什么是MongoDB呢 ? MongoDB 是由C语言编写的&#xff0c;是一个基于分布式文件存储的开源数据库系统。在高负载的情况下&#xff0c;添加更多的节点&#xff0c;可以保证服务器性能。 MongoDB 旨在为WEB应用提供可扩展的高性能数据存储解决方案。 MongoDB 将数据存储为一…

深入理解ClickHouse跳数索引教程

跳数索引 影响ClickHouse查询性能的因素很多。在大多数场景中&#xff0c;关键因素是ClickHouse在计算查询WHERE子句条件时是否可以使用主键。因此&#xff0c;选择适用于最常见查询模式的主键对于表的设计至关重要。 然而&#xff0c;无论如何仔细地调优主键&#xff0c;不可…

12.物联网操作系统之多任务核心

一。列表及列表项概念以及应用 1.freeRTOS列表介绍 列表项都是由链表生成&#xff0c;想要了解列表项&#xff0c;首先应该把上述的链表都要搞懂。 这是列表项的组件列表。 2.列表及列表项的定义 列表是双向链表构成&#xff0c;原因是双向链表的插入与删除效率高&#xff0c…

第一课-前提-Stable Diffusion 教程

学习 SD 的前提是电脑配置! SD 参考配置: 建议选择台式机 i5 CPU, 内存16GB,N卡 RTX3060, 8G显存以上的配置(最低配) 在此基础上的配置越高越好。 比如,cpu i7 更好,显卡能有 RTX4090 更好,32显存要能有最好,嘿嘿嘿。 如何查看自己的显卡配置? Win+R 输入 “dxdiag…

高斯过程回归 | Matlab实现高斯过程回归预测(Gaussian Process Regression)

文章目录 效果一览文章概述研究内容程序设计参考资料效果一览 文章概述 高斯过程回归 | Matlab实现高斯过程回归多输入单输出预测(Gaussian Process Regression) 研究内容 高斯过程回归(Gaussian Process Regression)是一种基于概率的非参数回归方法,用于建模输入变量和目…

TestNG中实现多线程并行,提速用例的执行时间

TestNG是一个开源自动化测试工具&#xff0c;TestNG源于Junit&#xff0c;最初用来做单元测试&#xff0c;可支持异常测试&#xff0c;忽略测试&#xff0c;超时测试&#xff0c;参数化测试和依赖测试。 除了单元测试&#xff0c;TestNG的强大功能让他在接口和UI自动化中也占有…

vue中vue-lazyload报错

1.问题&#xff1a; 说明&#xff1a;也就是版本不兼容&#xff0c;我安装的是vue2,因此需要 "vue-lazyload": "^1.2.6"或者更低 2.解决 npm i vue-lazyload1.2.6
最新文章