Mybatis插件-租户ID的注入&拦截应用

背景

Mybatis是一个半自动化的ORM(对象关系映射)框架,它通过XML或注解的方式将Java对象和SQL语句进行映射,从而实现对数据库的增删改查操作。

Mybatis ORM简介

  • 基本概念:Mybatis简化了数据访问层的开发,通过映射配置文件或注解将Java对象与数据库表进行映射,实现了对象关系映射。

  • 优点:Mybatis提供了灵活的SQL编写方式,支持动态SQL,易于与Spring等框架集成,同时减少了代码量,提高了开发效率。

  • 缺点:相比于全自动化的ORM框架如Hibernate,Mybatis需要编写更多的SQL语句,对开发人员的SQL编写能力有一定要求

Mybatis ORM的使用过程

  • 创建SqlSessionFactory:可以通过配置文件或直接编码来创建SqlSessionFactory。

  • 使用SqlSession进行数据库操作:通过SqlSession执行SQL语句,获取结果,并映射到Java对象。

f6775569340a7030fa223438cea24bf7.png

下面快速了解基于springboot-mybatis,以及拦截器的具体实现方案。

引入依赖

<!-- 整合mybatis相关依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>


<!-- 数据库驱动(例如 MySQL) -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

数据库配置

数据库表初始化

CREATE TABLE `user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` varchar(255) NOT NULL DEFAULT '' COMMENT '账号',
    `age` int(2) NOT NULL DEFAULT 0 COMMENT '年龄',
    `email` varchar(255) NOT NULL DEFAULT '' COMMENT '邮箱',
    `tenant_id` bigint(20) NOT NULL DEFAULT 1 COMMENT '租户id,1表示默认租户',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';


xml文件映射sql:UserMapper.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="com.bryant.mapper.UserMapper">


    <resultMap id="BaseResultMap" type="com.bryant.model.UserDetail">
         <id column="id" property="id" jdbcType="INTEGER"/>
         <result column="name" property="name" jdbcType="VARCHAR"></result>
         <result column="age" property="age" jdbcType="INTEGER"></result>
         <result column="email" property="email" jdbcType="VARCHAR"></result>
         <result column="tenant_id" property="tenantId" jdbcType="BIGINT"></result>
    </resultMap>


    <sql id="base_columns">
        `id`, `name`, age, email, tenant_id
    </sql>


    <insert id="insert" parameterType="map" keyColumn="id">
        insert
        into user
            (id, name, age, email, tenant_id)
        values
           (#{record.id,jdbcType=INTEGER},
            #{record.name,jdbcType=VARCHAR},
            #{record.age,jdbcType=INTEGER},
            #{record.email,jdbcType=VARCHAR},
            #{record.tenantId,jdbcType=BIGINT}
            )
    </insert>


    <update id="updateById" parameterType="map">
        update user
        set `age` = #{record.age},
            `name` = #{record.name},
            `email` = #{record.email}
        where id = #{record.id}
    </update>


    <delete id="deleteById">
        delete from user
        where id = #{id}
    </delete>


    <select id="getById" resultMap="BaseResultMap">
        select
        <include refid="base_columns"/>
        from user
        where id = #{id}
            and tenant_id = #{tenantId}
    </select>


</mapper>

应用配置

启动类

@EnableDiscoveryClient
@SpringBootApplication
// 测试自定义启动类加载
@EnableSpringStudy
@EnableCircuitBreaker
// mapper扫描的包路径
@MapperScan("com.bryant.mapper")
public class UserServer {


   public static void main(String[] args) {
       SpringApplication.run(UserServer.class, args);
   }


}

注意标黄的代码段,是对包路径的扫描。

Mybatis配置

/**
 * Full(proxyBeanMethods = true) :proxyBeanMethods参数设置为true时即为:Full 全模式。该模式下注入容器中的同一个组件无论被取出多少次都是同一个bean实例,即单实例对象,在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件
 * Lite(proxyBeanMethods = false) :proxyBeanMethods参数设置为false时即为:Lite 轻量级模式。该模式下注入容器中的同一个组件无论被取出多少次都是不同的bean实例,即多实例对象,在该模式下SpringBoot每次启动会跳过检查容器中是否存在该组件
 */
@Configuration(proxyBeanMethods = true)
@ConditionalOnProperty(prefix = "users.mybatis.custom", name = "interceptor", havingValue = "true")
@Slf4j
public class MybatisConfig implements InitializingBean {


    @Autowired
    private List<SqlSessionFactory> sqlSessionFactorys;


    @Bean
    @Order(1)
    public TenantIdInjectInterceptor mybatisInterceptor() {
        log.info("TenantIdInjectInterceptor interceptor init...");
        return new TenantIdInjectInterceptor();
    }


    /**
     * @Order(0) 是为了保证 TenantIdInjectInterceptor拦截器先于sqlMonitorInterceptor拦截器执行
     * @return
     */
    @Bean
    @Order(0)
    public SqlMonitorInterceptor sqlMonitorInterceptor() {
        log.info("SqlMonitorInterceptor interceptor init...");
        return new SqlMonitorInterceptor();
    }


    @Override
    public void afterPropertiesSet() throws Exception {
    }


}


Mybatis插件配置

SQL租户ID检测插件-TenantIdInjectInterceptor

/**
 * 拦截器做了2个事情
 * - 1.打印原sql
 * - 2.注入自定义参数
 *
 * Mybatis 插件的执行顺序有两种:
 * 1、不同拦截对象执行顺序,如下:
 *     Executor` -> `StatementHandler` -> `ParameterHandler` -> `ResultSetHandler`
 * 2、拦截相同对象执行顺序,取决于 mybatis-config.xml 中 <plugin> 配置顺序,越靠后,优先级越高。
 * 3、拦截相同对象执行顺序,如果是使用的配置类加载,则取决于配置类的加载顺序,加载顺序,越靠后,优先级越高;
 */
@Slf4j
@Intercepts(value = {
        @Signature(type = Executor.class, method = "update", args ={MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args =
                {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args =
                {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class TenantIdInjectInterceptor implements Interceptor {


    //用于匹配字符串中的tenant_id关键字,后面可以跟任意数量的空格,然后是等号=,再后面可以跟任意数量的空格,最后是一个问号?。
    // 这个正则表达式使用了Pattern.CASE_INSENSITIVE标志,表示匹配时不区分大小写。
    // 匹配问号,因为问号在正则表达式中有特殊含义(表示前面的字符出现0次或1次),所以需要用两个反斜杠进行转义
    private static final Pattern p = Pattern.compile("tenant_id(\\s+)?=(\\s+)?\\?", Pattern.CASE_INSENSITIVE);
    private static final String SQL_IGNORED_CHARACTOR = "[\\t\\n\\r]";
    /**
     * 核心逻辑在intercept方法,内部实现 sql 获取,参数解析,耗时统计
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("TenantIdInjectInterceptor interceptor start...");


        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        String namespace = mappedStatement.getId();
        String className = namespace.substring(0, namespace.lastIndexOf("."));
        String methodName = mappedStatement.getId()
                .substring(mappedStatement.getId().lastIndexOf(".") + 1);
        Class<?> c = Class.forName(className);


        //如果Class有注解指明要忽略本次拦截,则放弃拦截
        if (c.isAnnotationPresent(TenantIdInjectConfig.class) && c.getAnnotation(TenantIdInjectConfig.class).ignore()) {
            return invocation.proceed();
        }


        Method[] ms = c.getMethods();
        Method method = Arrays.stream(ms).filter(m -> m.getName().equals(methodName)).findAny().get();
        //如果method 有注解指明要忽略本次拦截,则放弃拦截
        if (method.isAnnotationPresent(TenantIdInjectConfig.class) && method.getAnnotation(TenantIdInjectConfig.class)
                .ignore()) {
            return invocation.proceed();
        }


        //判断SQL中是否存在tenant_id 字段,如果存在,认为已经考虑多租户的情况,否则将SQL拦截下来
        BoundSql boundSql = mappedStatement.getSqlSource().getBoundSql(args[1]);
        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll(SQL_IGNORED_CHARACTOR, " ");
        boolean sqlWithTenantIdParam;
        if (mappedStatement.getSqlCommandType().equals(SqlCommandType.INSERT)) {
            //insert语句只判断是否有tenant_id
            sqlWithTenantIdParam = sql.contains("tenant_id");
        } else {
            //其他语句判断是否有tenant_id=?
            sqlWithTenantIdParam = p.matcher(sql).find();
        }


        if (!sqlWithTenantIdParam) {
            log.error("缺少对多租户的支持,tenant_id 字段缺失,sql:{}", sql);
            throw new RuntimeException("缺少对多租户的支持,tenant_id字段缺失");
        }


        //这里使用默认的租户id=1
        String defaultTenantId = "1";
        Map map;
        if (args[1] == null) {
            //没有参数列表
            map = new MapperMethod.ParamMap<>();
        } else if (!(args[1] instanceof MapperMethod.ParamMap)) {
            //单参数
            Map tempMap = new MapperMethod.ParamMap<>();
            Parameter[] parameters = method.getParameters();
            Parameter param = parameters[0];
            //第一个参数获取@Param注解,然后获取值
            if (param.isAnnotationPresent(Param.class)) {
                String paramName = param.getAnnotation(Param.class).value();
                tempMap.put(paramName, args[1]);
            } else if (checkTypeType(param)) {
                //如果是基础类型抛出异常
                tempMap.put(param.getName(), args[1]);
            } else {
                //如果没有指定@Param,将单参数的属性单独拎出来处理
                Object arg = args[1];
                Field[] fields = arg.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (!Modifier.isStatic(field.getModifiers())) {
                        if (!field.isAccessible()) {
                            field.setAccessible(true);
                        }
                        tempMap.put(field.getName(), field.get(arg));
                    }
                }
            }
            args[1] = tempMap;
        }


        //如果是多参数列表时直接转map即可
        map = (Map) args[1];
        if (!StringUtils.isBlank(defaultTenantId)) {
            map.put("tenantId", Long.parseLong(defaultTenantId));
        }


        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    private boolean checkTypeType(Object object) {
        if (object.getClass().isPrimitive()
                || object instanceof String
                || object instanceof Boolean
                || object instanceof Double
                || object instanceof Float
                || object instanceof Long
                || object instanceof Integer
                || object instanceof Byte
                || object instanceof Short) {
            return true;
        } else {
            return false;
        }
    }
}

SQL监控插件-SqlMonitorInterceptor

@Slf4j
@Intercepts(value = {
        @Signature(type = Executor.class, method = "update", args ={MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args =
                {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args =
                {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class SqlMonitorInterceptor implements Interceptor {


    private static final String SQL_IGNORED_CHARACTOR = "[\\t\\n\\r]";


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("SqlMonitorInterceptor interceptor start...");


        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        BoundSql boundSql = mappedStatement.getSqlSource().getBoundSql(args[1]);
        // 1.打印原sql
        log.info("SqlMonitorInterceptor original sql: {}", boundSql.getSql().toLowerCase(Locale.CHINA).replace(SQL_IGNORED_CHARACTOR, " "));


        // BoundSql就是封装myBatis最终产生的sql类


        String sqlId = mappedStatement.getId();
        // 获取节点的配置
        Configuration configuration = mappedStatement.getConfiguration();
        // 获取到最终的sql语句
        log.info("SqlMonitorInterceptor sql after parse : {}", getSql(configuration, boundSql, sqlId));
        return invocation.proceed();
    }


    // 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号;对参数是null和不是null的情况作了处理
    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
                    DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }




    /**
     * 进行 ?符号的替换
     * @param configuration
     * @param boundSql
     * @return
     */
    private static String parseSql(Configuration configuration, BoundSql boundSql) {
        // 获取参数
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // sql语句中多个空格都用一个空格代替
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
            // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果根据parameterObject.getClass()可以找到对应的类型,则替换
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?",
                        Matcher.quoteReplacement(getParameterValue(parameterObject)));
            } else {
                // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?",
                                Matcher.quoteReplacement(getParameterValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        // 该分支是动态sql
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?",
                                Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        // 打印出缺失,提醒该参数缺失并防止错位
                        sql = sql.replaceFirst("\\?", "缺失");
                    }
                }
            }
        }
        return sql;
    }


    /**
     * 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
     * @param configuration
     * @param boundSql
     * @param sqlId
     * @return
     */
    private static String getSql(Configuration configuration, BoundSql boundSql, String sqlId)
    {
        String sql = parseSql(configuration, boundSql);
        StringBuilder str = new StringBuilder(100);
        str.append(sqlId);
        str.append(":");
        str.append(sql);
        return str.toString();
    }


}

自定义租户配置注解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantIdInjectConfig {


    boolean ignore() default false;


}

插件配置到SqlSession

/**
 * Full(proxyBeanMethods = true) :proxyBeanMethods参数设置为true时即为:Full 全模式。该模式下注入容器中的同一个组件无论被取出多少次都是同一个bean实例,即单实例对象,在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件
 * Lite(proxyBeanMethods = false) :proxyBeanMethods参数设置为false时即为:Lite 轻量级模式。该模式下注入容器中的同一个组件无论被取出多少次都是不同的bean实例,即多实例对象,在该模式下SpringBoot每次启动会跳过检查容器中是否存在该组件
 */
@Configuration(proxyBeanMethods = true)
@ConditionalOnProperty(prefix = "users.mybatis.custom", name = "interceptor", havingValue = "true")
@Slf4j
public class MybatisConfig implements InitializingBean {


    @Bean
    @Order(1)
    public TenantIdInjectInterceptor mybatisInterceptor() {
        log.info("TenantIdInjectInterceptor interceptor init...");
        return new TenantIdInjectInterceptor();
    }


    /**
     * @Order(0) 是为了保证 TenantIdInjectInterceptor拦截器先于sqlMonitorInterceptor拦截器执行
     * @return
     */
    @Bean
    @Order(0)
    public SqlMonitorInterceptor sqlMonitorInterceptor() {
        log.info("SqlMonitorInterceptor interceptor init...");
        return new SqlMonitorInterceptor();
    }


    @Override
    public void afterPropertiesSet() throws Exception {
    }


}

分析:

  • 这里将插件定义bean,交给容器管理即可,Mybatis会自动检测并加载到SqlSession


配置中心

数据库配置 - users-dev.properties

## mybatis依赖
spring.datasource.url = jdbc:mysql://localhost:3306/mac_m1?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=12345678
#数据库名称
spring.datasource.name=mac_m1
#默认情况下mybatis是不开启SQL日志输出,需要手动配置,com.riant.dao为mapper文件夹路径
logging.level.com.riant.mybatis.dao=debug
#指定mapper的配置文件的路径是mapper文件夹下的所有 xml文件。
mybatis.mapper-locations=classpath:mapper/*.xml

服务接口和应用配置

配置中心读取配置-bootstrap.properties

# 使用配置中心的master分支的{service-name}-dev.properties配置内容
spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.profile=dev
spring.cloud.config.label=master

服务Mybatis配置-application.properties

server.port=8891


## mybatis依赖
#指定mapper的配置文件的路径是mapper文件夹下的所有 xml文件。
mybatis.mapper-locations=classpath:mapper/*.xml
#默认情况下mybatis是不开启SQL日志输出,需要手动配置,com.riant.dao为mapper文件夹路径
logging.level.com.riant.mybatis.dao=debug


## 自定义sql拦截器
users.mybatis.custom.interceptor=true

controller

@RestController
public class MybatisController {


    @Autowired
    private UserService userService;


    @PostMapping("/user_insert")
    public UserDetail user_insert() {
        UserDetail detail = UserDetail.builder()
                .age(new Random().nextInt(100))
                .email(new Random().nextInt(100000000) + "@qq.com")
                .name("bryant" + new Random().nextInt(1111))
                .tenantId(new Random().nextLong())
                .build();
        return userService.insert(detail);
    }


    @GetMapping("/user_select")
    public UserDetail user_select(@RequestParam("id") Long id) {
        return userService.getById(id);
    }


}

service

public interface UserService {


    UserDetail insert(UserDetail detail);


    UserDetail getById(Long id);


    void update(UserDetail detail);


    void delete(Long id);


}




@Service
public class UserServiceImpl implements UserService {


    @Autowired
    private UserMapper userMapper;


    @Override
    public UserDetail insert(UserDetail detail) {
        if (Objects.isNull(detail)) {
            throw new RuntimeException("detail is null");
        }
        userMapper.insert(detail);
        return detail;
    }


    @Override
    public UserDetail getById(Long id) {
        return userMapper.getById(id);
    }


    @Override
    public void update(UserDetail detail) {
        userMapper.updateById(detail);
    }


    @Override
    public void delete(Long id) {
        userMapper.deleteById(id);
    }
}

mapper

@Repository
public interface UserMapper {


    void insert(@Param("record") UserDetail user);


    @TenantIdInjectConfig(ignore = true)
    void updateById(@Param("record") UserDetail user);


    @TenantIdInjectConfig(ignore = true)
    void deleteById(@Param("id") Long id);


    UserDetail getById(@Param("id") Long id);


}

model

@Data
@Builder
public class UserDetail implements Serializable {


    private static final long serialVersionUID = 2235541748764244156L;


    private Long id;
    private String name;
    private Integer age;
    private String email;
    private Long tenantId;


}

服务测试

注册服务

http://localhost:8881/,可以看到注册中心和user服务已经注册上去了。

8ae857999ed77255d67a80021b7925bc.png

增删改查

1932f6cb6cf144b416d5adf1248f8561.png

8bf1bad437c433bf56aa938394e7462b.png

插件日志:

2024-08-26 22:51:52.756 DEBUG [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] o.s.web.servlet.DispatcherServlet        : GET "/user_select?id=1", parameters={masked}
2024-08-26 22:51:52.757  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] c.b.c.mysql.TenantIdInjectInterceptor    : 
TenantIdInjectInterceptor interceptor start...
2024-08-26 22:51:52.758  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] c.b.config.mysql.SqlMonitorInterceptor   : SqlMonitorInterceptor interceptor start...
2024-08-26 22:51:52.758  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] 
c.b.config.mysql.SqlMonitorInterceptor   : SqlMonitorInterceptor original sql: 
        select
        `id`, `name`, age, email, tenant_id
        from user
        where id = ?
            and tenant_id = ?
2024-08-26 22:51:52.758  INFO [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] 
c.b.config.mysql.SqlMonitorInterceptor   : SqlMonitorInterceptor sql after parse : com.bryant.mapper.UserMapper.getById:
        select `id`, `name`, age, email, tenant_id from user where id = 1 and tenant_id = 1
2024-08-26 22:51:52.761 DEBUG [users,28ec46443db7b606,28ec46443db7b606,true] 3427 --- [nio-8891-exec-8] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

总结

Mybatis使用了非常多的设计模式:

  • 动态代理技术,Mybatis对执行器Executor使用了动态代理增强技术,从而让拦截器得以实现拦截功能;

  • 责任链模式,利用拦截器chain,Executor的各个handler进行非侵入式拦截处理。

这些后面我们会逐渐一一讲解。

&lt;think&gt;首先,用户的问题是:&ldquo;我想查找在若依框架中将MyBatis升级为MyBatis-Plus并实现多租户插件的方法&rdquo;。用户指定了参考站内引用,提供了四个引用:[^1], [^2], [^3], [^4]。这些引用讨论了MyBatis-Plus和MyBatis-Flex的优点、多租户功能等。 关键点: - 用户想在若依框架(RuoYi Framework)中将MyBatis升级到MyBatis-Plus。 - 然后实现多租户插件。 从引用中提取相关信息: - [^1]:提到MyBatis-Plus可以减少代码,提高效率。 - [^2]:讨论未来趋势,提到多租户支持。 - [^3]:具体关于在商城中使用MyBatis-Plus实现多租户功能,涉及TenantLineInnerInterceptor。 - [^4]:详细介绍MyBatis-Plus多租户插件的用法,包括如何过滤SQL不需要租户ID。 系统级指令: - 回答中使用中文。 - 行内数学表达式用$...$(但这里可能不适用数学)。 - 独立公式用$$...$$(不适用)。 - LaTeX语法正确(主要是代码块)。 - 生成相关问题。 - 引用的段落末尾添加引用标识,如[^1]。 用户可见层:回答结构清晰,逐步解决问题,保证真实可靠。 我的回答结构: 1. **理解问题**:确认用户需求。 2. **升级MyBatisMyBatis-Plus**:解释如何在若依框架中升级。 3. **实现多租户插件**:使用MyBatis-Plus的多租户功能。 4. **参考引用**:融入引用中的信息。 5. **代码示例**:提供代码块以展示实现。 6. **相关问题**:生成相关问题。 逐步回答: - **升级MyBatisMyBatis-Plus**: - 若依框架是基于Spring Boot的开源框架,默认使用MyBatis- 升级步骤:添加MyBatis-Plus依赖、替换MyBatis配置、更新Mapper等。 - **实现多租户插件**: - 引用[^4]提供了过滤SQL的方法。 - 引用[^3]提到TenantLineInnerInterceptor。 - 实现多租户需要配置拦截器、定义租户ID字段等。 详细步骤: 1. **添加MyBatis-Plus依赖**:在pom.xml中添加依赖。 2. **配置MyBatis-Plus**:在配置文件中设置。 3. **集成多租户插件**: - 使用TenantLineInnerInterceptor。 - 参考引用[^4]中的过滤方法。 - 定义租户ID处理逻辑。 4. **示例代码**:提供Java配置代码。 生成相关问题: - 相关问题:基于用户查询,如优化、常见问题等。 确保引用: - 在回答中引用相关段落,添加[^1][^3][^4]。 最终回答大纲: - 介绍:简要说明升级和实现多租户的关键点。 - 步骤1:升级MyBatisMyBatis-Plus。 - 步骤2:配置多租户插件- 代码示例。 - 注意事项。 - 相关问题。 &lt;/think&gt;### 在若依框架中将MyBatis升级为MyBatis-Plus并实现多租户插件的方法 在若依框架(RuoYi)中,将原生MyBatis升级到MyBatis-Plus并集成多租户功能,可以显著简化数据访问层开发,减少重复代码,提升效率30%以上[^1]。MyBatis-Plus提供了开箱即用的多租户插件(如`TenantLineInnerInterceptor`),支持租户ID自动注入和SQL过滤。下面是逐步实现方法,基于引用[^3][^4]的最佳实践和官方文档。 --- #### **步骤1: 升级MyBatisMyBatis-Plus** 1. **添加依赖**:在若依项目的`pom.xml`文件中,移除原有MyBatis依赖,添加MyBatis-Plus起步依赖。 ```xml &lt;!-- 移除旧依赖 --&gt; &lt;!-- &lt;dependency&gt; --&gt; &lt;!-- &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt; --&gt; &lt;!-- &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt; --&gt; &lt;!-- &lt;/dependency&gt; --&gt; &lt;!-- 添加MyBatis-Plus依赖 --&gt; &lt;dependency&gt; &lt;groupId&gt;com.baomidou&lt;/groupId&gt; &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt; &lt;version&gt;3.5.7&lt;/version&gt; &lt;!-- 使用最新稳定版本 --&gt; &lt;/dependency&gt; ``` 2. **更新配置**:在`application.yml`中,配置MyBatis-Plus全局设置,替换原有MyBatis配置。 ```yaml mybatis-plus: configuration: map-underscore-to-camel-case: true # 开启驼峰命名 global-config: db-config: id-type: auto # 主键策略 mapper-locations: classpath*:mapper/**/*.xml # 映射文件路径 ``` 3. **替换Mapper接口**:将原有Mapper接口继承`BaseMapper`,例如: ```java // 旧接口:public interface UserMapper extends Mapper&lt;User&gt; {} // 新接口: public interface UserMapper extends BaseMapper&lt;User&gt; {} ``` 升级后,MyBatis-Plus将自动增强CRUD操作,减少基础代码量[^1]。 --- #### **步骤2: 实现多租户插件** 多租户功能通过`TenantLineInnerInterceptor`拦截器实现,自动在SQL中注入租户ID字段(如`tenant_id`)。引用[^3]在商城项目中验证了此方案的可行性。 1. **定义租户处理器**:创建类实现`TenantLineHandler`接口,指定租户ID字段和值获取方式(通常从Session或Token中获取)。 ```java public class CustomTenantHandler implements TenantLineHandler { @Override public String getTenantIdColumn() { return &quot;tenant_id&quot;; // 数据库中的租户ID字段名 } @Override public Expression getTenantId() { // 从当前用户上下文中获取租户ID(示例使用固定值,实际从SecurityUtils获取) Long tenantId = SecurityUtils.getTenantId(); // 若依框架的租户管理模块 return new LongValue(tenantId); } @Override public boolean ignoreTable(String tableName) { // 忽略不需要租户过滤的表(如系统表) return &quot;sys_config&quot;.equals(tableName); // 示例忽略sys_config表 } } ``` 2. **配置多租户插件**:在MyBatis配置类中注册拦截器。 ```java @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加多租户插件 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new CustomTenantHandler())); return interceptor; } } ``` 3. **SQL过滤(可选)**:引用[^4]提到,部分SQL可能需跳过租户ID注入。可通过注解或配置过滤器实现: - **方法1:使用`@SqlParser`注解**(适用于特定Mapper方法) ```java public interface UserMapper extends BaseMapper&lt;User&gt; { @SqlParser(filter = true) // 跳过租户过滤 @Update(&quot;update user set status=0 where id=#{id}&quot;) int adminUpdateStatus(Long id); } ``` - **方法2:配置全局SQL解析器**(在`application.yml`中启用) ```yaml mybatis-plus: global-config: sql-parser-cache: true # 启用SQL解析缓存(MyBatis-Plus 3.1.1+无需此配置) ``` --- #### **步骤3: 集成若依框架的租户管理** 若依框架内置多租户模块(引用[^3]描述),需将MyBatis-Plus插件与其结合: 1. **租户分配**:在若依后台【平台管理】-【平台用户管理】中,为平台用户分配租户。用户登录后,可从Session获取当前租户ID。 2. **租户切换**:实现前端租户切换功能(如左上角下拉菜单),切换时更新后端租户上下文。 ```java // 示例:在Controller中处理租户切换 @PostMapping(&quot;/switchTenant&quot;) public R switchTenant(Long tenantId) { SecurityUtils.setCurrentTenantId(tenantId); // 存储到ThreadLocal return R.ok(); } ``` 3. **实体类修改**:为相关实体类添加`tenantId`字段,并确保数据库表包含该列。 --- #### **注意事项** - **数据库变更**:所有需租户隔离的表必须添加`tenant_id`字段(类型建议为BIGINT)。 - **性能优化**:引用[^2]提到,MyBatis-Plus的轻量设计适合云环境;避免在全表扫描时忽略租户ID- **版本兼容**:使用MyBatis-Plus最新版(如v4.3),支持智能主键策略[^2]。若依框架升级时需测试兼容性。 - **测试验证**:确保插入、查询、更新操作自动注入租户ID,忽略表正常执行。 实践证明,此方案能简化多租户实现,让开发者聚焦业务逻辑[^1]。若遇到问题,参考MyBatis-Plus官方文档或若依社区案例。 --- ### 相关问题 1. 如何优化MyBatis-Plus多租户插件的性能? 2. 若依框架中如何实现动态数据源切换(如分库分表)? 3. MyBatis-Plus与MyBatis-Flex在多租户场景下的优缺点对比?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后台技术汇

对你的帮助,是对我的最好鼓励。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值