Mybatis实现分表插件

分库分表是常见的数据库水平扩展方案之一,Mybatis实现分表插件,可以对数据库进行动态分表,方便进行扩展和管理。下面我将为您详细介绍如何实现Mybatis分表插件,并提供两条示例。

什么是Mybatis分表插件?

Mybatis分表插件是一种Mybatis的插件机制,可以应对分表的需求。通常情况下,将业务数据切分到多个表中,可以极大地提高多线程并发执行时的效率,降低业务处理系统的锁冲突率,从而提高系统的吞吐量和响应速度。

Mybatis分表插件的实现步骤

1. 自定义分表插件类

创建自定义的分表插件类MybatisPlugin,需要继承自Interceptor,并实现其intercept()方法:

import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Properties;

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
@Component
@Data
public class MybatisPlugin implements Interceptor {

  public static final String TABLE_SUFFIX = "_$actualTableSuffix"; // 字段名称为指定数据源所对应的真实表名的后缀

  private HashMap<String, String> tableSuffix = new HashMap<>(); // 存储数据源所对应的真实表名

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
      // 获取参数
      Object[] args = invocation.getArgs();
      MappedStatement ms = (MappedStatement) args[0];
      Object parameter = args[1];
      BoundSql boundSql = ms.getBoundSql(parameter);
      String sql = boundSql.getSql();
      // 重组sql
      String newSql = rebuildSql(sql, boundSql, ms.getConfiguration());
      // 通过反射修改实际操作的数据表名称
      Field field = ms.getClass().getDeclaredField("mappedStatement");
      field.setAccessible(true);
      MappedStatement mappedStatement = (MappedStatement) field.get(ms);
      Field sqlSourceField = mappedStatement.getClass().getDeclaredField("sqlSource");
      sqlSourceField.setAccessible(true);
      updateParameter(mappedStatement.getConfiguration(), newSql, (String) tableSuffix.get(mappedStatement.getId()));
      // 执行SQL
      return invocation.proceed();
  }

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

  @Override
  public void setProperties(Properties properties) {

  }

  private String rebuildSql(String sql, BoundSql boundSql, org.apache.ibatis.session.Configuration configuration) {
      // 获取对应的实际表名
      String actualTableName = (String) tableSuffix.get(configuration.getEnvironment().getId());
      if (StringUtils.isEmpty(actualTableName)) {
          actualTableName = "table1";  // 使用默认的表名
      }
      String newSql = sql.replaceAll("tableName", actualTableName);
      return newSql;
  }

  private void updateParameter(org.apache.ibatis.session.Configuration configuration, String sql, String table) throws NoSuchFieldException, IllegalAccessException {
      // 通过BoundSql重置缓存的sql
      Field field = BoundSql.class.getDeclaredField("sql");
      field.setAccessible(true);
      field.set(configuration, sql);
      // 通过MappedStatement重置缓存的BoundSql
      Field field2 = MappedStatement.class.getDeclaredField("sqlSource");
      field2.setAccessible(true);
      field2.set(configuration.getMappedStatements().get(0), buildSqlSource(configuration, table, sql));
  }

  private Object buildSqlSource(org.apache.ibatis.session.Configuration configuration, String table, String sql) {
      SqlSource sqlSource = new StaticSqlSource(configuration, sql, null);
      return new RawSqlSource(configuration, sqlSource, table);
  }

}

上述代码是自定义的分表插件类的具体实现方式,主要通过intercept()方法实现对$sql替换为真实表名。

2. 对应Mapper.xml创建拦截器

创建对应的Mapper.xml,使用拦截器来处理Mybatis分表插件,在对应的operationType触发时通过plugin拦截器进行切入:

<update id="save" parameterType="com.spring.mybatis.domain.User">  
  insert into tableName(id,name,password) values(#{id},#{name},#{password})  
</update>
<update id="save" parameterType="com.spring.mybatis.domain.User" >
  <plugin interceptor="com.spring.mybatis.plugin.MybatisPlugin" />
</update>

3. 测试分表插件

测试分表插件。对于下面这两条示例SQL:

INSERT INTO `tableName` (userId, name, age, sex)values(2,'Tom',19,'M')
INSERT INTO `tableName` (userId, name, age, sex)values(3,'Jim',20,'F')

当分表插件生效时,通过指定数据源,便可对不同的表进项插入操作。

示例一:实现Mybatis分表插件

假设有 A、B 两个表,在service层中,指定分表插件并设置表名。 当使用 insert into A 表时,自动向 B 表中插入值。

首先自定义插件注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InsertClient {

    String name() default "";

}

针对不同的业务场景,创建相应的注解。下面是自定义分表插件类实现代码:

@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class MybatisSplitTableInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(MybatisSplitTableInterceptor.class);

    private static final String TABLE_NAME = "_A";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        final Object[] args = invocation.getArgs();
        final MappedStatement ms = (MappedStatement) args[0];
        final Object parameter = args[1];
        final MapperMethod mapperMethod = new MapperMethod(ms.getConfiguration().getMapperRegistry(), ms, parameter);
        final InsertClient insertClient = mapperMethod.getMethod().getAnnotation(InsertClient.class);
        if (insertClient == null) {
            return invocation.proceed();
        }
        final BoundSql boundSql = ms.getBoundSql(parameter);
        final String originalSql = boundSql.getSql();
        final String mappedSql = getMappedSql(originalSql, TABLE_NAME);
        for (int i = 0; i < boundSql.getParameterMappings().size(); i++) {
            final ParameterMapping parameterMapping = boundSql.getParameterMappings().get(i);
            final String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                logger.info("跳过多余的参数:{}={}", propertyName, boundSql.getAdditionalParameter(propertyName));
            } else {
                final Object parameterObject = boundSql.getParameterObject();
                final MetaObject metaObject = parameterObject instanceof Map ? null : metaObjectFor(parameterObject);
                final Object value = metaObject == null ? null : metaObject.getValue(propertyName);
                logger.info("参数:{}={}", propertyName, value);
                boundSql.setAdditionalParameter(propertyName, value);
            }
        }
        logger.info("新SQL:{}", mappedSql);
        final BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), mappedSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
        final MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
        args[0] = newMs;
        return invocation.proceed();
    }

    private static String getMappedSql(String originalSql, String tableName) {
        final StringBuilder sb = new StringBuilder(originalSql);
        if (originalSql.contains("values")) {
            final int index = sb.indexOf("(") + 1;
            sb.insert(index, "id,");
            final int endIndex = sb.indexOf(")");
            sb.insert(endIndex, ",'" + tableName + "'");
        }
        sb.insert(sb.indexOf(tableName), "_");
        return sb.toString().replaceFirst(tableName, tableName.toLowerCase());
    }

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

    @Override
    public void setProperties(Properties props) {
        logger.info(props.toString());
    }

    private static class BoundSqlSqlSource implements SqlSource {

        private final BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}

完成自定义分表插件的代码,即可在使用A表的时候,直接插入到B表中。

示例二:另类分表插件的实现

现有几个表,分别是(weibo_201701,weibo_201702,weibo_210703,weibo_201704)等等。如果系统中需要查询某段时间范围内的数据(如201702、201703)时,直接操作查询对应的表会显得非常麻烦。

先创建一个扩展Mybatis的拦截器。在拦截器中,针对操作表名进行统一处理,将表名转换为类似“weibo_2017”的格式,只需要在mapper.xml中操作该格式的表名即可。插件同时会自动查找当前默认情况下所要操作的表,自动进行路由。

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class DynamicTableInterceptor implements Interceptor
{
    public static final String SUFFIX_CUR = "_cur";
    public static final String SUFFIX_S1 = "_s1";
    public static final String SUFFIX_S2 = "_s2";
    ...
}

在使用的时候,只需要设置查询时间即可,插件会帮你路由到正确的数据。


<select id="listActiveUserId" parameterType="int"
resultType="long" useCache="true">
  SELECT uid FROM weibo_${YYYYMM} WHERE active=1
</select>

实现了该插件,则执行该sql时自动切换到正确的数据源上。

总结

Mybatis分表插件可以动态分表插入,方便数据库扩展和管理,其实现步骤为:

  1. 自定义插件类
  2. 对应Mapper.xml创建插件拦截器
  3. 测试分表插件

同时,该插件支持多种业务场景,如直接插入到其他表中,及时间范围分表等,具备高度的兼容性和扩展性。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Mybatis实现分表插件 - Python技术站

(0)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • Java虚拟机JVM性能优化(二):编译器

    先来进行一下标题的规划。根据要求,我们需要详细讲解Java虚拟机JVM性能优化中,关于编译器的攻略。因此,建议的标题是:Java虚拟机JVM性能优化(二):编译器优化攻略。 编译器优化攻略 1. 基础概念 编译器是Java虚拟机中负责将Java源代码编译成机器码的一个组件。为了提高Java应用的运行效率,必须对编译器进行优化。 2. 热点代码优化 通过JIT…

    Java 2023年5月20日
    00
  • spring boot整合mybatis+mybatis-plus的示例代码

    下面我给您讲解一下“spring boot整合mybatis+mybatis-plus的示例代码”的完整攻略。 步骤1 – 添加依赖 首先,我们需要在 pom.xml 中添加以下依赖: <!– Spring Boot Mybatis Starter –> <dependency> <groupId>org.mybati…

    Java 2023年5月20日
    00
  • Java 对象深拷贝工具类的实现

    概述: Java 对象深拷贝是指将一个对象完全复制另一个对象,即所有属性和属性值都被复制,并且两个对象之间没有相互影响。在 Java 开发中经常会使用对象深拷贝,比如在进行对象传参、克隆等场合都需要进行对象深拷贝。本文将详细讲解 Java 对象深拷贝工具类的实现。 实现: Java 中提供了两种方式实现深拷贝:Serializable 和 Cloneable…

    Java 2023年5月26日
    00
  • Springboot之restTemplate的配置及使用方式

    Spring Boot之RestTemplate的配置及使用方式 在Spring Boot中,可以使用RestTemplate来发送HTTP请求。RestTemplate是Spring框架提供的一个用于访问RESTful服务的客户端工具,可以方便地发送HTTP请求并处理响应。本文将详细讲解RestTemplate的配置及使用方式,包括如何配置RestTemp…

    Java 2023年5月15日
    00
  • Javaweb使用Maven工具与Tomcat的方法详解

    Javaweb使用Maven工具与Tomcat的方法详解 什么是Maven? Maven是一个Java项目管理工具,它可以帮助我们管理项目的依赖,构建,测试等工作。 为什么需要Maven? 抽象依赖关系,易于维护 统一构建方式,减少人为出错 有助于代码重用 前置条件 在开始Maven项目之前,您需要做一些准备工作: 安装Java JDK 安装Apache M…

    Java 2023年5月20日
    00
  • 基于Cookie使用过滤器实现客户每次访问只登录一次

    概述 使用过滤器来实现客户端每次访问只登录一次,需要使用Cookie来保存会话信息。把用户的登录状态作为一个标识存储到Cookie中,通过过滤器来检查Cookie中是否存在标识,如果存在则表示用户已经登录过,直接放行请求;如果不存在,则表示用户未登录或者会话已失效,需要跳转到登录界面进行身份验证。 实现步骤 2.1 配置过滤器 在web.xml文件中添加如下…

    Java 2023年6月16日
    00
  • 内存溢出的原因有哪些?

    以下是关于内存溢出的完整使用攻略: 什么是内存溢出? 内存溢出是指程序在申请内存时,没有足够的内存空间可供使用,导致程序无法正常运行。内存溢出是一种常见的程序错误,如果不及时处理,会导致程序崩溃或者系统崩溃。 内存溢出的原因 内存溢出的原因主要有以下几点: 1. 内存申请过大 在程序中,如果申请的内存空间过大,就会导致内存溢出。例如,在 C++ 中,如果使用…

    Java 2023年5月12日
    00
  • SpringMVC中常用参数校验类注解使用示例教程

    SpringMVC中常用参数校验类注解使用示例教程 在SpringMVC中,参数校验是非常重要的,它可以帮助我们在控制器中对请求参数进行校验,确保数据的有效性和安全性。本文将详细介绍SpringMVC中常用的参数校验类注解,并提供两个示例说明。 常用参数校验类注解 在SpringMVC中,常用的参数校验类注解有以下几种: @NotNull:用于校验参数不为n…

    Java 2023年5月17日
    00
合作推广
合作推广
分享本页
返回顶部