MyBatis sql拦截器实现一个自动根据租户进行分表的方案

需求描述:

在一个多租户系统中,通过 MyBatis 实现动态数据表分离。具体来说,您希望通过 MyBatis 拦截器在执行 SQL 时自动将表名根据当前租户 ID (tenantId) 进行修改。这样,每个租户的数据就可以存储在专属于它们的表中,实现数据隔离。

需求细节:

  1. 拦截 SQL 语句:使用 MyBatis 的 @Intercepts 注解和 Interceptor 接口来拦截 SQL 语句。

  2. 修改表名:在 SQL 执行时,根据提供的租户 ID (tenantId) 动态地修改 SQL 语句中的表名。例如,如果原表名为 ALARM_RECORD_INFO,租户 ID 为 1001,则修改后的表名应为 ALARM_RECORD_INFO_1001

  3. 获取租户 ID:从 MyBatis Mapper 接口方法的参数中获取 tenantId。这是通过在 Mapper 接口的方法参数上使用 @Param("tenantId") 注解实现的。

  4. 配置:在 mybatis-config.xml 文件中配置拦截器,并指定需要根据租户 ID 分表的表名。

  5. 映射器文件 (AlarmRecordMapper.xml):定义 SQL 语句,尽管 SQL 中写的是原始表名,实际执行时,表名将根据租户 ID 被拦截器动态修改

实现

下面是实现一个 MyBatis 拦截器的完整代码,该拦截器会拦截 SQL 语句,在执行操作时动态地将表名根据当前租户 ID (tenantId) 进行修改,以实现数据隔离。这个实现包括了从 @Repository 注解的 Mapper 接口中获取 tenantId 参数的逻辑。

拦截器实现 (TableTenantSuffixInterceptor.java)

package com.example;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class TableTenantSuffixInterceptor implements Interceptor {
    private Set tableNameSet = new HashSet<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String originalSql = boundSql.getSql();

        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        Object parameterObject = boundSql.getParameterObject();

        String tenantId = getTenantId(parameterObject);

        if (tenantId != null && !tenantId.isEmpty()) {
            for (String tableName : tableNameSet) {
                if (originalSql.toUpperCase().contains(tableName.toUpperCase())) {
                    String newTableName = tableName + "_" + tenantId;
                    String newSql = originalSql.replaceAll(Pattern.quote(tableName), newTableName);
                    metaObject.setValue("delegate.boundSql.sql", newSql);
                    break; // Assume only one table name per SQL
                }
            }
        }

        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
        String tablesByTenant = properties.getProperty("tablesByTenant");
        if (tablesByTenant != null) {
            for (String table : tablesByTenant.split(",")) {
                tableNameSet.add(table.trim());
            }
        }
    }

    private String getTenantId(Object parameterObject) {
        if (parameterObject instanceof Map) {
            Map paramMap = (Map) parameterObject;
            return (String) paramMap.get("tenantId");
        } else {
            MetaObject metaObject = SystemMetaObject.forObject(parameterObject);
            if (metaObject.hasGetter("tenantId")) {
                return (String) metaObject.getValue("tenantId");
            }
        }
        return null;
    }
}

MyBatis 配置文件 (mybatis-config.xml)

在 MyBatis 的配置文件中,注册拦截器并指定需要根据租户 ID 分表的表名。




    
        
            
        
    
    

Mapper 接口示例 (AlarmRecordMapper.java)

package com.example;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Set;

@Repository
public interface AlarmRecordMapper {
    List getAlarmRecordByTaskIds(@Param("tenantId") String tenantId, @Param("taskIds") Set taskIds);
}

AlarmRecordMapper.xml 





    


这个实现中,拦截器 TableTenantSuffixInterceptor 会检查每个执行的 SQL 语句,看它是否包含配置中指定的表名。如果是,它会将表名修改为包含租户 ID 后缀的形式。这个过程依赖于在 SQL 语句中直接使用表名,且假设每个 SQL 语句只涉及一个需要进行租户 ID 后缀处理的表。如果 SQL 语句中包含多个需要处理的表名,或者表名的使用方式更加复杂(例如,动态表名或 SQL 函数中使用表名),则可能需要更复杂的逻辑来正确地替换表名。

你可能感兴趣的:(mybatis,java,sql拦截器,分表方案)