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日志体系及集成jar包梳理分析

    混乱的Java日志体系及集成jar包梳理分析是一篇旨在帮助Java开发者理解Java日志体系和集成jar包的文章。本文将围绕Java日志体系的问题、集成jar包的例子、分析Java日志框架的实现等多方面展开讲解。 一、Java日志体系的问题 在Java开发过程中,我们经常需要使用日志来帮助我们进行调试。但是,Java日志体系却十分混乱,不同的日志框架都有着自…

    Java 2023年5月19日
    00
  • JDBC增删改查和查唯一的完整代码解析

    JDBC增删改查和查唯一的完整代码解析 什么是JDBC? JDBC(Java Data Base Connectivity,Java 数据库连接)是Java语言中用于访问数据库的应用程序接口。它提供了一种标准的方法来访问任何的关系型数据库。 JDBC的四种操作 JDBC主要支持以下四种操作:- 插入(Insert)- 删除(Delete)- 更新(Updat…

    Java 2023年6月15日
    00
  • Java编程中字节流与字符流IO操作示例

    下面是“Java编程中字节流与字符流IO操作示例”的完整攻略: 1. 前言 IO(Input/Output,输入输出)是程序中非常重要的一部分,它关乎数据在程序中的读写以及处理。在Java中,IO的对象分为两个大类:字节流和字符流。在进行IO操作时,我们需要根据不同的需求选用字节流或者字符流。本文将详细讲解Java编程中字节流与字符流IO操作示例。 2. 字…

    Java 2023年5月26日
    00
  • SpringMVC+ZTree实现树形菜单权限配置的方法

    下面是完整攻略: 1. 准备工作 1.1 搭建SpringMVC项目 首先我们需要搭建一个SpringMVC项目,这里不做过多介绍,建议使用Maven进行管理。 1.2 引入ZTree插件 在搭建完SpringMVC项目后,在项目中引入ZTree插件。可以使用CDN的方式,也可以下载到本地引入。 1.3 数据库设计 在实现权限配置时,需要通过数据库保存树形菜…

    Java 2023年6月16日
    00
  • Java数据类型转换详解

    Java数据类型转换详解 在Java编程中,我们需要对不同的数据类型进行转换,使其能够满足我们的需求。本文将详细讲解Java数据类型转换的相关知识。 基本数据类型 Java中的数据类型可以分为两类,基本数据类型和引用数据类型。基本数据类型包括整型、浮点型、字符型、布尔型,下面分别介绍。 整型 整型包括byte、short、int和long这四种类型。其中,b…

    Java 2023年5月26日
    00
  • 详解springSecurity之java配置篇

    关于“详解springSecurity之java配置篇”完整攻略,我们来详细说一下。 简介 SpringSecurity是基于Spring框架的安全框架,主要解决的是在应用程序中如何安全地进行身份认证和授权。本篇文档主要讲解如何使用Java配置的方式来进行SpringSecurity的配置。 步骤 1. 添加spring-security配置依赖 在 pom…

    Java 2023年5月20日
    00
  • java多线程有序读取同一个文件

    要实现Java多线程有序读取同一个文件,可以使用以下步骤: 步骤一:打开文件流 首先,需要创建一个FileInputStream对象,该对象可以打开文件流并准备读取数据。代码示例如下: FileInputStream fis = new FileInputStream("file.txt"); 步骤二:创建 BufferedReader …

    Java 2023年5月19日
    00
  • Spring Security的过滤器链机制

    Spring Security是一个流行的企业级安全框架,它可以提供应用程序的验证和授权服务。在Spring Security中,过滤器链(Filter Chain)是其中一个重要的概念。 Spring Security的过滤器链 Spring Security的过滤器链是一个由多个过滤器组成的链式结构,用于对每一个请求进行处理。当一个请求进入Spring …

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