Mybatis-Plus 多租户插件&属性自动赋值

文章目录

1、Mybatis-Plus 多租户插件

TenantLineInnerInterceptor 是 MyBatis-Plus 提供的一个插件,用于实现多租户的数据隔离。通过这个插件,可以确保每个租户只能访问自己的数据,从而实现数据的安全隔离。

其实就是一个拦截器,用于进行sql 增删改查 时自动添加租户字段

1.1、属性介绍

TenantLineInnerInterceptor 的关键属性是 tenantLineHandler,它是一个 TenantLineHandler 接口的实例,用于处理租户相关的逻辑。

属性名 类型 默认值 描述
tenantLineHandler TenantLineHandler 租户处理器( TenantId 行级 )

TenantLineHandler 接口定义了以下方法:

java 复制代码
public interface TenantLineHandler {

    /**
     * 获取租户 ID 值表达式,只支持单个 ID 值
     *
     * @return 租户 ID 值表达式
     */
    Expression getTenantId();

    /**
     * 获取租户字段名
     * 默认字段名叫: tenant_id
     *
     * @return 租户字段名
     */
    default String getTenantIdColumn() {
        return "tenant_id";//默认
    }

    /**
     * 根据表名判断是否忽略拼接多租户条件
     * 默认都要进行解析并拼接多租户条件
     *
     * @param tableName 表名
     * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
     */
    default boolean ignoreTable(String tableName) {
        return false;
    }

    /**
     * 忽略插入租户字段逻辑
     *
     * @param columns        插入字段
     * @param tenantIdColumn 租户 ID 字段
     * @return
     */
    default boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
        return columns.stream().map(Column::getColumnName).anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));
    }
}

1.2、使用多租户插件

比方我有一张表biz_archive_common

sql 复制代码
-- security_manager.biz_archive_common definition

CREATE TABLE `biz_archive_common` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '数据主键ID',
  `title_name` varchar(255) DEFAULT NULL COMMENT '题名',
  `secrecy_level_id` bigint(20) DEFAULT NULL COMMENT '密级id',
  `archive_num` varchar(510) DEFAULT NULL COMMENT '档号(照片号)',
  `roll_num` varchar(31) DEFAULT NULL COMMENT '案卷号(册号/带号)',
  `abandon` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否废弃 默认 0false/1true',
  `del` varchar(200) NOT NULL DEFAULT '0' COMMENT '是否删除 默认 0false/1true',
  `create_user` varchar(31) DEFAULT NULL COMMENT '创建者账户',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(31) DEFAULT NULL COMMENT '更新者账户',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `archive_company_id` bigint(20) DEFAULT NULL COMMENT '全宗单位ID',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `indexArchiveCompanyId` (`archive_company_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='档案共有信息表';

maven

xml 复制代码
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatisplus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

yml

yml 复制代码
server:
  port: 8001
  #address: 127.0.0.1
#spring数据源配置
spring:
  application:
    name: token #项目名
  # 数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security_manager?serverTimezone=GMT%2B8&useUnicode=true&useSSL=false&characterEncoding=utf-8
    username: root
    password: root
    druid:
      initial-size: 20
      min-idle: 20
      max-active: 100
      max-wait: 10000
      time-between-eviction-0runs-millis: 60000
      min-evictable-idle-time-millis: 30000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: true
      test-on-return: true


# mybatis-plus配置
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  configuration:
    map-underscore-to-camel-case: true # 数据库下划线自动转驼峰标示关闭
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志配置
  mapper-locations: classpath*:/mapper/**/*.xml

ThreadLocalUtil

java 复制代码
package cn.js.util;

import java.util.HashMap;
import java.util.Map;

/**
 * Description:
 *
 * @Author Js
 * @Create 2024-11-17 14:12
 * @Version 1.0
 */
public class ThreadLocalUtil {


    //1 初始化TreadLocal
    private static ThreadLocal<Map<String, Object>> RES = new ThreadLocal<Map<String, Object>>() {

        /**
         * 和继承ThreadLocal 类一样,也是一个方法的复写
         */
        protected Map<String, Object> initialValue() {


            return new HashMap<String, Object>();
        }

        ;
    };

    /*
     * 给线程里面设置一个值
     */
    public static void set(String name, Object object) {

        Map<String, Object> map = RES.get(); // 取出来的map 集合位null
        map.put(name, object);
    }

    /**
     * 从线程里面取值
     */
    public static Object get(String name) {

        Map<String, Object> map = RES.get();
        if (!map.containsKey(name)) {


            return null;
        }
        return map.get(name);
    }

    /**
     * 清空线程的值
     */
    public static void clear() {
        Map<String, Object> map = RES.get();
        map.clear();
        map = null; // jvm 自动回收

    }



}

实现 定义,注入租户处理器插件

java 复制代码
package cn.js.config;

import cn.js.util.ThreadLocalUtil;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
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.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Js
 * @Description
 * @Date 2024-11-15 21:34
 * @Version 1.0
 **/
@Configuration
@AutoConfigureBefore(MybatisPlusAutoConfiguration.class)
public class PaginationInterceptorConfig {
    @Bean
    public MybatisPlusInterceptor addMybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler()));
        return interceptor;
    }
    

    private class TenantLineHandler implements com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler{

        /**
         * 获取当前租户 ID。
         */
        @Override
        public Expression getTenantId() {
            Object id = ThreadLocalUtil.get("id");
            Long tenantId=Long.valueOf(String.valueOf(id));
            // 返回租户ID的表达式,LongValue 是 JSQLParser 中表示 bigint 类型的 class
            return new LongValue(tenantId);
        }


        /**
         * 表结构中那个字段用于拼接多租户条件
         */
        @Override
        public String getTenantIdColumn() {
            return "archive_company_id";
        }


        /**
         * 默认返回false:表示所有表都需要拼接多租户条件
         * tableName:表名称
         */
        @Override
        public boolean ignoreTable(String tableName) {
            //如果那些表不需要拼接多租户条件,
            List<String> tableList = new ArrayList<>();
            tableList.add("sql_version");
            tableList.add("User");
            tableList.add("Kf");
            if(tableList.contains(tableName)){
                //如果不需要添加的表名称在list中,就返回false,不用拼接租户条件
                return true;

            }
            return false;
        }


        /**
         * 获取租户 ID 字段名。
         */
        @Override
        public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) {
            return com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler.super.ignoreInsert(columns, tenantIdColumn);
        }
    }

}

测试

java 复制代码
package cn.js.controller;

import cn.js.domain.BizArchiveCommon;
import cn.js.service.BizArchiveCommonService;
import cn.js.util.ThreadLocalUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Author Js
 * @Description
 * @Date 2024-11-15 21:26
 * @Version 1.0
 **/
@RequestMapping("/common")
@RestController
public class BizArchiveCommonController {
    @Resource
    private BizArchiveCommonService bizArchiveCommonService;

    @GetMapping("/getAll")
    public List<BizArchiveCommon> getAll() {

        ThreadLocalUtil.set("id",1645);
        List<BizArchiveCommon> archiveCommons = bizArchiveCommonService.list();
        return archiveCommons;
    }

}
domian
java 复制代码
package cn.js.domain;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @Author Js
 * @Description
 * @Date 2024-11-15 21:22
 * @Version 1.0
 **/
@Data
@TableName(value="biz_archive_common")
public class BizArchiveCommon {

    private Long id;
    private String titleName;
    private Long secrecyLevelId;
    private String archiveNum;
    private String rollNum;
    @TableLogic(value = "false", delval = "true")
    private Boolean abandon;
    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private String createUser;
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * 修改人
     */
    @TableField(fill = FieldFill.UPDATE)
    private String updateUser;
    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
    /**
     * 是否逻辑删除,true:删除 false:未删除
     */
    @TableLogic(value = "false", delval = "true")
    private Boolean del;
    /**
     * 全宗单位id
     */
    private Long archiveCompanyId;

}
service & ServiceImpl
java 复制代码
package cn.js.service;

import cn.js.domain.BizArchiveCommon;
import com.baomidou.mybatisplus.extension.service.IService;

public interface BizArchiveCommonService extends IService<BizArchiveCommon> {
}



package cn.js.service.impl;

import cn.js.domain.BizArchiveCommon;
import cn.js.mapper.BizArchiveCommonMapper;
import cn.js.service.BizArchiveCommonService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * @Author Js
 * @Description
 * @Date 2024-11-15 21:27
 * @Version 1.0
 **/
@Service
public class BizArchiveCommonServiceImpl extends ServiceImpl<BizArchiveCommonMapper, BizArchiveCommon> implements  BizArchiveCommonService {
}
mapper
java 复制代码
package cn.js.mapper;

import cn.js.domain.BizArchiveCommon;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;



@Mapper
public interface BizArchiveCommonMapper extends BaseMapper<BizArchiveCommon> {
}
shell 复制代码
JDBC Connection [HikariProxyConnection@639980080 wrapping com.mysql.cj.jdbc.ConnectionImpl@7c12090] will not be managed by Spring
==>  Preparing: SELECT id, title_name, secrecy_level_id, archive_num, roll_num, abandon, del, create_user, create_time, update_user, update_time, archive_company_id FROM biz_archive_common WHERE archive_company_id = 1645
==> Parameters: 
<==      Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38e12bd4]

测试mapper.xml 方式

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="cn.js.mapper.BizArchiveCommonMapper">


    <select id="ones" resultType="cn.js.domain.BizArchiveCommon">
        select * from biz_archive_common where id=1
    </select>
</mapper>
java 复制代码
@RequestMapping("/common")
@RestController
public class BizArchiveCommonController {
    @Resource
    private BizArchiveCommonService bizArchiveCommonService;

   
    @GetMapping("/getOne")
    public BizArchiveCommon getOnes() {

        ThreadLocalUtil.set("id",1645);
        BizArchiveCommon archiveCommon = bizArchiveCommonService.getones();
        return archiveCommon;
    }

}
shell 复制代码
JDBC Connection [HikariProxyConnection@1523711906 wrapping com.mysql.cj.jdbc.ConnectionImpl@6db5719b] will not be managed by Spring
==>  Preparing: SELECT * FROM biz_archive_common WHERE id = 1 AND archive_company_id = 1645
==> Parameters: 
<==      Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70ef05c0]

1.3、不使用多租户插件

可能我们并不是所有的sql语句都需要拼接租户条件,那该如何解决,只需要在相应的mapper接口上面添加注解

java 复制代码
@InterceptorIgnore(tenantLine = "true")
java 复制代码
package cn.js.mapper;

import cn.js.domain.BizArchiveCommon;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;



@Mapper
public interface BizArchiveCommonMapper extends BaseMapper<BizArchiveCommon> {
    @InterceptorIgnore(tenantLine = "true")
    BizArchiveCommon ones();
}

2、实体对象的属性自动赋值

比方说表中有这个4个字段,我们在新增,修改的时候能不能自动插入,而不是每次,操作的时候我们给他插入

使用

1. 定义实体类

在实体类中,你需要使用 @TableField 注解来标记哪些字段需要自动填充,并指定填充的策略。

java 复制代码
public class User {
    @TableField(fill = FieldFill.INSERT)
    private String createTime;

    @TableField(fill = FieldFill.UPDATE)
    private String updateTime;

    // 其他字段...
}

2. 实现 MetaObjectHandler

创建一个类来实现 MetaObjectHandler 接口,并重写 insertFillupdateFill 方法。

java 复制代码
package cn.js.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * Description:
 *
 * @Author Js
 * @Create 2024-11-17 15:39
 * @Version 1.0
 */
@Component
@Slf4j
public class MetaObjectHandlerConfig implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
       log.info("开始插入填充...");
       this.strictInsertFill(metaObject,"createTime", LocalDateTime.class,LocalDateTime.now());
       this.strictInsertFill(metaObject,"createUser", String.class,"张三");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("开始更新填充...");
        this.strictUpdateFill(metaObject,"updateTime", LocalDateTime.class,LocalDateTime.now());
        this.strictUpdateFill(metaObject,"updateUser", String.class,"王五");

    }
}

3. 配置自动填充处理器

确保你的 MyMetaObjectHandler 实现类被 Spring 管理,可以通过 @Component@Bean 注解来实现。

注意事项

  • 自动填充是直接给实体类的属性设置值。
  • 如果属性没有值,入库时会是 null
  • MetaObjectHandler 提供的默认方法策略是:如果属性有值则不覆盖,如果填充值为 null 则不填充。
  • 字段必须声明 @TableField 注解,并设置 fill 属性来选择填充策略。
  • 填充处理器需要在 Spring Boot 中声明为 @Component@Bean
  • 使用 strictInsertFillstrictUpdateFill 方法可以根据注解 FieldFill.xxx、字段名和字段类型来区分填充逻辑。
  • 如果不需区分,可以使用 fillStrategy 方法。
  • update(T entity, Wrapper<T> updateWrapper) 时,entity 不能为空,否则自动填充失效。
  • update(Wrapper<T> updateWrapper) 时不会自动填充,需要手动赋值字段条件。

4.测试

java 复制代码
package cn.js.controller;

import cn.js.domain.BizArchiveCommon;
import cn.js.service.BizArchiveCommonService;
import cn.js.util.ThreadLocalUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Author Js
 * @Description
 * @Date 2024-11-15 21:26
 * @Version 1.0
 **/
@RequestMapping("/common")
@RestController
public class BizArchiveCommonController {
    @Resource
    private BizArchiveCommonService bizArchiveCommonService;

  

    @GetMapping("/save")
    public Boolean save() {

        ThreadLocalUtil.set("id",1645);
        BizArchiveCommon bizArchiveCommon = new BizArchiveCommon();
        bizArchiveCommon.setTitleName("这是新增的");
        bizArchiveCommon.setSecrecyLevelId(123L);
        bizArchiveCommon.setArchiveNum("8080-25201-38245");
        bizArchiveCommon.setRollNum("25201");
        bizArchiveCommon.setAbandon(false);
        boolean archiveCommon = bizArchiveCommonService.save(bizArchiveCommon);
        return archiveCommon;
    }



    @GetMapping("/update")
    public Boolean update() {

        ThreadLocalUtil.set("id",1645);
        BizArchiveCommon bizArchiveCommon = new BizArchiveCommon();
        bizArchiveCommon.setId(1L);
        bizArchiveCommon.setTitleName("这是新增的,进行修改!");
        bizArchiveCommon.setSecrecyLevelId(123L);
        bizArchiveCommon.setArchiveNum("8080-25201-38245");
        bizArchiveCommon.setRollNum("25201");
        bizArchiveCommon.setAbandon(false);
        boolean b = bizArchiveCommonService.updateById(bizArchiveCommon);
        return b;
    }

}
新增
shell 复制代码
JDBC Connection [HikariProxyConnection@1186756471 wrapping com.mysql.cj.jdbc.ConnectionImpl@22ef7a2e] will not be managed by Spring
==>  Preparing: INSERT INTO biz_archive_common (id, title_name, secrecy_level_id, archive_num, roll_num, abandon, create_user, create_time, archive_company_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1645)
==> Parameters: 1858059911531872257(Long), 这是新增的(String), 123(Long), 8080-25201-38245(String), 25201(String), false(Boolean), 张三(String), 2024-11-17T16:09:38.650358300(LocalDateTime)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b266740]

修改
shell 复制代码
JDBC Connection [HikariProxyConnection@724111921 wrapping com.mysql.cj.jdbc.ConnectionImpl@5db6c17a] will not be managed by Spring
==>  Preparing: UPDATE biz_archive_common SET title_name = ?, secrecy_level_id = ?, archive_num = ?, roll_num = ?, abandon = ?, update_user = ?, update_time = ? WHERE id = ? AND del = false AND archive_company_id = 1645
==> Parameters: 这是新增的,进行修改!(String), 123(Long), 8080-25201-38245(String), 25201(String), false(Boolean), 王五(String), 2024-11-17T16:22:59.641094300(LocalDateTime), 1(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5390448d]
相关推荐
鸿·蒙25 分钟前
【MyBatis 源码阅读与笔记】Mapper 接口的动态代理实现
java·mybatis
paopaokaka_luck31 分钟前
【366】基于springboot的高校物品捐赠管理系统
java·spring boot·后端
H_kiwi1 小时前
APT 参与者将恶意软件嵌入 macOS Flutter 应用程序中
java·python·安全·flutter·macos·安全威胁分析·安全性测试
杨荧1 小时前
【开源免费】基于Vue和SpringBoot的私人健身与教练预约管理系统(附论文)
java·前端·javascript·vue.js·spring boot·spring cloud·开源
万物皆字节1 小时前
spring-webmvc根据请求路径找到对应的 HandlerMethod
java·后端·spring
飞天大拖把1 小时前
ThreadLocal原理及其内存泄漏
java
大海星辰7981 小时前
java itext后端生成pdf导出
java·pdf
黄昏_2 小时前
2024版本IDEA创建Sprintboot项目下载依赖缓慢
java·ide·intellij-idea
大G哥2 小时前
基于K8S1.28.2实验rook部署ceph
java·ceph·云原生·容器·kubernetes
阿华的代码王国2 小时前
【Bug合集】——Java大小写引起传参失败,获取值为null的解决方案
java·开发语言·springboot