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进行非侵入式拦截处理。

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

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mybatis-Plus 是基于 Mybatis 的扩展工具包,它提供了很多实用的功能,其中包括租户管理。 在 Mybatis-Plus 中,租户管理是通过多租户插件实现的。多租户插件提供了对租户的切换和租户信息的获取等功能。 使用多租户插件,需要在 Mybatis 的配置文件中配置插件: ```xml <!-- 多租户插件 --> <plugins> <plugin interceptor="com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser"> <!-- 租户字段名 --> <property name="tenantColumn" value="tenant_id"/> <!-- 租户 ID 类型 --> <property name="tenantIdType" value="long"/> <!-- 忽略 SQL 解析的表名 --> <property name="ignoreTable" value="sys_tenant"/> <!-- 是否支持动态租户切换 --> <property name="supportTenantSwitch" value="true"/> </plugin> </plugins> ``` 在使用多租户插件时,可以通过注解的方式指定租户信息: ```java @TableName(value = "user") public class User implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String name; @TableField(value = "tenant_id") @MultiTenantColumn private Long tenantId; // 租户 ID // 省略 getter/setter } ``` 在进行查询时,多租户插件会自动在 SQL 语句中加入租户信息的过滤条件,以保证查询结果只包含当前租户的数据: ```java // 查询当前租户的所有用户 List<User> userList = userService.list(new QueryWrapper<User>().lambda() .eq(User::getTenantId, TenantContextHolder.getTenantId())); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值