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

news2024/10/1 7:43:31

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.coloradmin.cn/o/836767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实例详解如何选择滤波算法

在机器视觉中,图像滤波器无处不在。例如,它们用于减少图像噪声,改善对比度或检测边缘。本文将向您介绍MVTec HALCON中一些最常用的滤波器,它们是如何工作的以及可以用于什么。 mean_image:均值滤波器 首先,我们读取具有背景纹理的示例图像。我们的目标是在不改变实际信…

OpenLayers实战,OpenLayers获取移动端精确定位,OpenLayers适配App混合H5方式调用手机定位位置并定位到指定点

专栏目录: OpenLayers实战进阶专栏目录 前言 本章讲解OpenLayers如何获取移动端精确定位位置。不使用任何native本地方法,只使用纯js实现。 本篇文章适用于App混合H5方式调用手机精确定位,打包时需要选择GPS位置权限,手机获取定位过程中会弹出是否允许定位的权限提示。 …

健身计划:用思维导图记录你的健身目标、锻炼项目、时间安排等

现在&#xff0c;大家越来越在乎自己的身体健康&#xff0c;健身也成了大家工作之外非常重要的一件事。一个好的健身计划的制定可以让我们的健身计划事半功倍。 思维导图作为一种高效的可视化思维工具&#xff0c;在健身计划制定的过程中&#xff0c;可以让我们的各项任务与时间…

人类机器人编程的心理机制(二)

\qquad 本文是关于“人类机器人编程的心理机制”讨论的第二部分&#xff0c;内容包括&#xff1a; 程序员内化和无意识记忆海量记忆存储宿主编程Self-状态(s)与宿主的接口恐惧条件反射下的无意识记忆恐惧条件反射是T.B.M.C的工作基础 \qquad 注释&#xff1a;本文基本内容主要…

Java稀疏数组

稀疏的概述 当一个数组中大部分元素为0&#xff0c;或者为同一值的数组时&#xff0c;可以使用稀疏数组来保存该数组。 稀疏数组的处理方式是&#xff1a; 记录数组一共有几行几列&#xff0c;有多少个不同值 把具有不同值的元素和行列及值记录在一个小规模的数组中&#xff0c…

docker 部署mysql 5.6集群

docker搭建mysql的集群&#xff08;一主双从&#xff09; 1.拉取镜像 docker pull mysql:5.6 2.启动master容器 docker run -it -d --name mysql_master -p 3306:3306 --ip 192.168.162.100 \ -v /data/mysql_master/mysql:/var/lib/mysql \ -v /data/mysql_master/conf.d…

利用XSS在线平台获取用户cookie

//XSS弹窗&#xff1a; <script>alert("xss")</script> XSS漏洞&#xff1a; //XSS弹窗&#xff1a; <script>alert("xss")</script> //XSS在线平台&#xff1a; <ScRipT sRc//7ix7kigpovxdbtd32fuspgffmtmufo3wwzgnzaltddewtb…

小白到运维工程师自学之路 第六十六集 (docker 网络模型)

一、概述 Docker网络模型是指Docker容器在网络中的通信方式和组织结构。Docker容器通过网络连接&#xff0c;使得容器之间可以相互通信&#xff0c;并与主机和外部网络进行交互。 在Docker中&#xff0c;有几种不同的网络模型可供选择&#xff1a; 1、主机模式&#xff08;H…

实习该选择c++后台开发还是音视频开发?

后台开发:更多是理解需求、分析问题、解决bug等能力、对于逻辑培养有很大的帮助。可以进行软件开发、网络开发、游戏开发、以及之后可能的物联网相关开发。但是容易陷入需求当中&#xff0c;熟练后就会容易乏味 音视频开发: 门槛比较高&#xff0c;需要大量积累&#xff0c;补…

给你一个网站如何测试?

主要围绕&#xff0c;功能&#xff0c;页面 UI &#xff0c;兼容&#xff0c;性能&#xff0c;安全&#xff0c;这几个方面去聊&#xff0c;首先是制定测试计划&#xff0c;确定测试范围和测试策略&#xff0c;一般包括以下几个部分&#xff1a;功能性测试&#xff1b;界面测试…

vue列表全选反选

1、结果查看 2、 选中时添加样式 3、点击选择调用方法 4、全选反选调用方法

多图在线合成gif怎么弄?图片合成gif在线操作更简单

将多张图片合成起来就可以做成gif动图&#xff0c;相信很多小伙伴都知道这种方法&#xff0c;那么什么样的操作方法能够更加简单快捷的完成gif合成处理呢&#xff1f;想要将多图合成gif&#xff0c;那么利用浏览器来使用在线gif制作&#xff08;https://www.gif.cn/&#xff09…

OLAP与OLTP的异同、定义及优劣

OLAP与OLTP的异同、定义及优劣 OLAP&#xff1a;联机事务处理定义应用场景优缺点 OLTP&#xff1a;联机分析处理定义应用场景优缺点 OLTP与OLAP异同点相同点不同点例子 OLAP&#xff1a;联机事务处理 定义 OLAP&#xff08;Online Analytical Processing&#xff0c;联机分析…

灵魂在高纬度的19个特征

01 喜欢奉献和付出‍ 在高维度的星球里&#xff0c;无私的爱是主旋律&#xff0c;所以是每个灵魂自动的反应。 02 喜欢光明&#xff0c;不喜欢黑暗 喜欢展现自己的阳光的一面&#xff0c;不喜欢自己黑暗的一面、尽量克制自己的不好的思维和行为&#xff0c;所以容易成为一个完…

中小企业实施MES管理系统,这几点需要注意

制造业是中国经济命脉所系&#xff0c;是立国之本、强国之基。作为世界制造大国&#xff0c;制造业一直是热门话题。当下&#xff0c;中小制造企业的产业地位不断提升&#xff0c;想要规范生产制造、提升产品竞争力&#xff0c;进行实施MES管理系统解决方案的企业越来越多。那么…

window安装TensorFlow遇到的问题

环境 windows 10 64位Anaconda 通常来说&#xff0c;官方文档作为一个建议&#xff0c;因为和国内场景会有些比较大的区别&#xff0c;当然参考文档 https://www.tensorflow.org/install/pip?hlzh-cn Anaconda升级 python建议升级到python3.0&#xff0c;这里通过Anaconda…

如何做好数仓BI项目的规划与建设?

BI项目规划和实施方案是保障BI项目顺利落地的首要环节。好的项目规划能有效提升开发人效&#xff0c;缩短项目周期&#xff0c;实现项目预期目标。 构建运营数据的可视化是为企业运营管控提供有效的支撑&#xff0c;成为企业在大数据时代智慧运营的必然路径。 在实际BI项目的…

你看你也哭!与张津剑对谈AI创业与投资;AI二维码/艺术字/Logo视频全教程;互联网AI大模型TOP 10图鉴;开发人员成长指南 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 真格基金X亚马逊云科技开启「AI 超新星计划」&#xff0c;发放海量云资源 真格基金联合亚马逊云科技&#xff0c;推出了「AI超新星计划…

Leetcode-每日一题【剑指 Offer 05. 替换空格】

题目 请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。 示例 1&#xff1a; 输入&#xff1a;s "We are happy."输出&#xff1a;"We%20are%20happy." 限制&#xff1a; 0 < s 的长度 < 10000 解题思路 前置知识 Str…

C++ 指针作为函数参数

C语法中&#xff0c;函数的参数不仅可以是基本类型的变量、对象名、数组名或引用&#xff0c;而且可以是指针。 **如果指针作为形参&#xff0c;在调用时实参将值传递给形参&#xff0c;也就是使实参和形参指针变量指向同一内存地址。**这样在子函数运行过程中&#xff0c;通过…