【Mybatis】mybatis拦截器+自定义注解完成简单的水平分表

一、背景

1.1 环境信息

依赖版本
windows10
mysqlMysql 8.0.25
SpringBoot2.4.10

1.2 场景

  1. 当我们只需要对一张表进行水平分表,且只会对该表进行简单的增删改查。
  2. 不进行复杂的分组、聚合、排序、分页和关联查询。
  3. 使用mycat和sharding-jdbc这些分库分表工具和中间件觉得太重时。

1.3 表信息

  1. 产品信息表(product_info)根据产品id取模的方式水平分表为product_info_1和product_info_2。
  2. 产品描述表(product_desc)根据产品id取模的方式分为product_desc_1和product_desc_2。
  3. 城市表(t_city)不分表。
    在这里插入图片描述

二、实现思路

2.1 概述

通过自定义注解+自定义mybatis拦截器的方式实现对sql的拦截,然后重写逻辑sql,将逻辑表替换成真实的表。

  1. 使用时在mapper类上加上自定义注解,注解需要指明逻辑表名和分表策略。
  2. 通过实现mybatis的Interceptor接口自定义拦截器,来拦截带有自定义注解的mapper类。在拦截器里我们可以获取到具体执行的方法的参数列表,参数值、要执行的sql(逻辑sql)以及注解信息。
  3. 根据注解信息获取到对应的分表策略,分表策略可以根据参数列表和参数值及逻辑表名计算出真实表名。再将逻辑sql里的逻辑表名替换成真是表名,再替换掉执行的sql,从而达到水平分表的目的。

2.2 代码实现

源码地址

2.2.1 自定义mybatis拦截器

package com.lh.boot.mybatis.fkfb.config;

import cn.hutool.extra.spring.SpringUtil;
import com.lh.boot.mybatis.fkfb.config.strategy.AbstractSplitTableStrategy;
import com.lh.boot.mybatis.fkfb.config.strategy.StrategyManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

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

/**
 * @author: StarrySky
 * @createDate: 2021/8/23 10:05
 * @version: 1.0
 * @description:
 */
@Slf4j
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class MyInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // id为执行的mapper方法的全路径名
        String id = mappedStatement.getId();
        BoundSql boundSql = statementHandler.getBoundSql();
        // 注解逻辑判断 添加注解了才拦截
        Class<?> classType = Class.forName(id.substring(0, mappedStatement.getId().lastIndexOf(".")));
        if (classType.isAnnotationPresent(TableSeg.class)) {
            TableSeg tableSeg = classType.getAnnotation(TableSeg.class);
            String sql = rewriteSql(tableSeg, boundSql);
            //通过反射修改sql语句
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, sql);
        }
        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
        log.warn("MyInterceptor=======" + properties.toString());
    }

    /**
     * 重新sql
     *
     * @param tableSeg 注解
     * @param boundSql sql信息
     * @return 重写后的sql
     */
    private String rewriteSql(TableSeg tableSeg, BoundSql boundSql) {
        String sql = boundSql.getSql();
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        StrategyManager strategyManager = SpringUtil.getBean(StrategyManager.class);
        AbstractSplitTableStrategy strategy = strategyManager.getStrategy(tableSeg.strategy());
        String newTableName = strategy.doSharding(tableSeg.tableName(), parameterMappings, parameterObject);
        String newSql = sql.replaceAll(tableSeg.tableName(), newTableName);
        log.info("rewriteSql=======> logicTable={}", tableSeg.tableName());
        log.info("rewriteSql=======> logicSql={}", sql);
        log.info("rewriteSql=======> newTableName={}", newTableName);
        log.info("rewriteSql=======> newSql={}", newSql);
        return newSql;
    }
}

2.2.2 自定义注解

package com.lh.boot.mybatis.fkfb.config;

import java.lang.annotation.*;

/**
 * @author: StarrySky
 * @createDate: 2021/8/23 11:14
 * @version: 1.0
 * @description: 自定义注解
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TableSeg {
    /**
     * 逻辑表名
     *
     * @return String
     */
    String tableName();

    /**
     * 分表策略
     *
     * @return 策略名
     */
    String strategy();

}

2.2.3 策略管理者

package com.lh.boot.mybatis.fkfb.config.strategy;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: StarrySky
 * @createDate: 2021/8/23 15:33
 * @version: 1.0
 * @description: 策略管理者
 */
@Slf4j
@Component
public class StrategyManager {

    private final Map<String, AbstractSplitTableStrategy> strategies = new ConcurrentHashMap<>(10);

    public AbstractSplitTableStrategy getStrategy(String key) {
        return strategies.get(key);
    }

    public Map<String, AbstractSplitTableStrategy> getStrategies() {
        return strategies;
    }

    public void registerStrategy(String key, AbstractSplitTableStrategy strategy) {
        if (strategies.containsKey(key)) {
            log.error("Key is already in use! key={}", key);
            throw new RuntimeException("Key is already in use! key=" + key);
        }
        strategies.put(key, strategy);
    }
}

2.2.4 分表策略抽象类

package com.lh.boot.mybatis.fkfb.config.strategy;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.ParameterMapping;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import java.util.List;

/**
 * @author: StarrySky
 * @createDate: 2021/8/23 15:09
 * @version: 1.0
 * @description: 分表策略抽象类
 */
@Slf4j
public abstract class AbstractSplitTableStrategy {

    /**
     * 策略管理者
     */
    @Autowired
    private StrategyManager strategyManager;

    abstract String key();

    @PostConstruct
    public void init() {
        this.register();
    }

    /**
     * @param logicTableName 逻辑表名
     * @param list           映射
     * @param val            值
     * @return 实际表名
     */
    public abstract String doSharding(String logicTableName, List<ParameterMapping> list, Object val);

    protected final void register() {
        String name = key();
        strategyManager.registerStrategy(name, this);
    }

    /**
     * 从mybatis映射中取指定的值
     *
     * @param list        映射集
     * @param val         参数值
     * @param shardingKey 分片键
     * @return 分片键对应的值
     */
    protected String getShardingValue(List<ParameterMapping> list, Object val, String shardingKey) {
        JSONObject obj;
        if (val.toString().contains("=")) {  //用变量传值
            String replaceAll = val.toString().replaceAll("=", ":");
            obj = (JSONObject) JSONObject.parse(replaceAll);
        } else {   //用对象传值
            obj = (JSONObject) JSONObject.parse(JSON.toJSONString(val));
        }
        for (ParameterMapping para : list) {
            String property = para.getProperty();
            log.info("abstract getShardingValue! shardingKey={} property={} value={}", shardingKey, property, obj.get(property));
            if (para.getProperty().equals(shardingKey)) {
                return obj.getString(shardingKey); //获取制定sql参数
            }
        }
        throw new RuntimeException("Sharding value is null! shardingKey=" + shardingKey);
    }

}

2.2.5 产品表分表策略

package com.lh.boot.mybatis.fkfb.config.strategy;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.ParameterMapping;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author: StarrySky
 * @createDate: 2021/8/23 13:46
 * @version: 1.0
 * @description:
 */
@Slf4j
@Component
public class ProductSplitTableStrategy extends AbstractSplitTableStrategy {
    public static final String PRODUCT_STRATEGY = "PRODUCT_STRATEGY";

    @Override
    public String key() {
        return PRODUCT_STRATEGY;
    }

    @Override
    public String doSharding(String logicTableName, List<ParameterMapping> list, Object val) {
        /**
         * 根据订单id取模分表
         */
        String orderId = getShardingValue(list, val, "productId");
        return logicTableName + "_" + (Long.parseLong(orderId) % 2 + 1);
    }

}

2.2.6 产品描述表分表策略

package com.lh.boot.mybatis.fkfb.config.strategy;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.ParameterMapping;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
public class ProductDescSplitTableStrategy extends AbstractSplitTableStrategy {

    public static final String PRODUCT_DESC_STRATEGY = "PRODUCT_DESC_STRATEGY";

    @Override
    public String key() {
        return PRODUCT_DESC_STRATEGY;
    }

    @Override
    public String doSharding(String logicTableName, List<ParameterMapping> list, Object val) {
        /**
         * 根据产品id取模分表
         */
        String orderId = getShardingValue(list, val, "productId");
        return logicTableName + "_" + (Long.parseLong(orderId) % 2 + 1);
    }
}

2.3 使用

2.3.1 产品信息表mapper接口

package com.lh.boot.mybatis.fkfb.mapper;

import com.lh.boot.mybatis.fkfb.config.TableSeg;
import com.lh.boot.mybatis.fkfb.config.strategy.ProductSplitTableStrategy;
import com.lh.boot.mybatis.fkfb.entity.ProductInfo;
import com.lh.boot.mybatis.fkfb.entity.ProductInfoVO;

import java.util.List;

@TableSeg(tableName = "product_info", strategy = ProductSplitTableStrategy.PRODUCT_STRATEGY)
public interface ProductInfoMapper {
    int deleteByPrimaryKey(Long productId);

    int insert(ProductInfo record);

    int insertSelective(ProductInfo record);

    ProductInfo selectByPrimaryKey(Long productId);

    int updateByPrimaryKeySelective(ProductInfo record);

    int updateByPrimaryKey(ProductInfo record);
}

2.3.2 产品信息表mapper.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.lh.boot.mybatis.fkfb.mapper.ProductInfoMapper">
    <resultMap id="BaseResultMap" type="com.lh.boot.mybatis.fkfb.entity.ProductInfo">
        <constructor>
            <idArg column="product_id" javaType="java.lang.Long" jdbcType="BIGINT"/>
            <arg column="store_id" javaType="java.lang.Long" jdbcType="BIGINT"/>
            <arg column="price" javaType="java.math.BigDecimal" jdbcType="DECIMAL"/>
            <arg column="product_name" javaType="java.lang.String" jdbcType="VARCHAR"/>
            <arg column="city" javaType="java.lang.String" jdbcType="VARCHAR"/>
            <arg column="status" javaType="java.lang.String" jdbcType="VARCHAR"/>
        </constructor>
    </resultMap>
    <sql id="Base_Column_List">
        product_id, store_id, price, product_name, city, status
    </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from product_info
        where product_id = #{productId,jdbcType=BIGINT}
    </select>

    <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
        delete from product_info
        where product_id = #{productId,jdbcType=BIGINT}
    </delete>
    <insert id="insert" parameterType="productInfo">
        insert into product_info (product_id, store_id, price,
        product_name, city, status
        )
        values (#{productId,jdbcType=BIGINT}, #{storeId,jdbcType=BIGINT}, #{price,jdbcType=DECIMAL},
        #{productName,jdbcType=VARCHAR}, #{city,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}
        )
    </insert>
    <insert id="insertSelective" parameterType="productInfo">
        insert into product_info
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="productId != null">
                product_id,
            </if>
            <if test="storeId != null">
                store_id,
            </if>
            <if test="price != null">
                price,
            </if>
            <if test="productName != null">
                product_name,
            </if>
            <if test="city != null">
                city,
            </if>
            <if test="status != null">
                status,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="productId != null">
                #{productId,jdbcType=BIGINT},
            </if>
            <if test="storeId != null">
                #{storeId,jdbcType=BIGINT},
            </if>
            <if test="price != null">
                #{price,jdbcType=DECIMAL},
            </if>
            <if test="productName != null">
                #{productName,jdbcType=VARCHAR},
            </if>
            <if test="city != null">
                #{city,jdbcType=VARCHAR},
            </if>
            <if test="status != null">
                #{status,jdbcType=VARCHAR},
            </if>
        </trim>
    </insert>
    <update id="updateByPrimaryKeySelective" parameterType="productInfo">
        update product_info
        <set>
            <if test="storeId != null">
                store_id = #{storeId,jdbcType=BIGINT},
            </if>
            <if test="price != null">
                price = #{price,jdbcType=DECIMAL},
            </if>
            <if test="productName != null">
                product_name = #{productName,jdbcType=VARCHAR},
            </if>
            <if test="city != null">
                city = #{city,jdbcType=VARCHAR},
            </if>
            <if test="status != null">
                status = #{status,jdbcType=VARCHAR},
            </if>
        </set>
        where product_id = #{productId,jdbcType=BIGINT}
    </update>
    <update id="updateByPrimaryKey" parameterType="productInfo">
        update product_info
        set store_id = #{storeId,jdbcType=BIGINT},
        price = #{price,jdbcType=DECIMAL},
        product_name = #{productName,jdbcType=VARCHAR},
        city = #{city,jdbcType=VARCHAR},
        status = #{status,jdbcType=VARCHAR}
        where product_id = #{productId,jdbcType=BIGINT}
    </update>
</mapper>

2.3.3 产品描述表mapper接口

package com.lh.boot.mybatis.fkfb.mapper;


import com.lh.boot.mybatis.fkfb.config.TableSeg;
import com.lh.boot.mybatis.fkfb.config.strategy.ProductDescSplitTableStrategy;
import com.lh.boot.mybatis.fkfb.entity.ProductDesc;

@TableSeg(tableName = "product_desc", strategy = ProductDescSplitTableStrategy.PRODUCT_DESC_STRATEGY)
public interface ProductDescMapper {
    int deleteByPrimaryKey(Long productId);

    int insert(ProductDesc record);

    int insertSelective(ProductDesc record);

    ProductDesc selectByPrimaryKey(Long productId);

    int updateByPrimaryKeySelective(ProductDesc record);

    int updateByPrimaryKey(ProductDesc record);
}

2.3.4 产品描述表mapper.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.lh.boot.mybatis.fkfb.mapper.ProductDescMapper">
    <resultMap id="BaseResultMap" type="com.lh.boot.mybatis.fkfb.entity.ProductDesc">
        <constructor>
            <idArg column="product_id" javaType="java.lang.Long" jdbcType="BIGINT"/>
            <arg column="store_id" javaType="java.lang.Long" jdbcType="BIGINT"/>
            <arg column="product_size" javaType="java.lang.String" jdbcType="VARCHAR"/>
            <arg column="stock" javaType="java.lang.Long" jdbcType="BIGINT"/>
            <arg column="desc_info" javaType="java.lang.String" jdbcType="VARCHAR"/>
        </constructor>
    </resultMap>
    <sql id="Base_Column_List">
    product_id, store_id, product_size, stock, desc_info
  </sql>
    <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from product_desc
        where product_id = #{productId,jdbcType=BIGINT}
    </select>
    <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
    delete from product_desc
    where product_id = #{productId,jdbcType=BIGINT}
  </delete>
    <insert id="insert" parameterType="com.lh.boot.mybatis.fkfb.entity.ProductDesc">
    insert into product_desc (product_id, store_id, product_size, 
      stock, desc_info)
    values (#{productId,jdbcType=BIGINT}, #{storeId,jdbcType=BIGINT}, #{productSize,jdbcType=VARCHAR}, 
      #{stock,jdbcType=BIGINT}, #{descInfo,jdbcType=VARCHAR})
  </insert>
    <insert id="insertSelective" parameterType="com.lh.boot.mybatis.fkfb.entity.ProductDesc">
        insert into product_desc
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="productId != null">
                product_id,
            </if>
            <if test="storeId != null">
                store_id,
            </if>
            <if test="productSize != null">
                product_size,
            </if>
            <if test="stock != null">
                stock,
            </if>
            <if test="descInfo != null">
                desc_info,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="productId != null">
                #{productId,jdbcType=BIGINT},
            </if>
            <if test="storeId != null">
                #{storeId,jdbcType=BIGINT},
            </if>
            <if test="productSize != null">
                #{productSize,jdbcType=VARCHAR},
            </if>
            <if test="stock != null">
                #{stock,jdbcType=BIGINT},
            </if>
            <if test="descInfo != null">
                #{descInfo,jdbcType=VARCHAR},
            </if>
        </trim>
    </insert>
    <update id="updateByPrimaryKeySelective" parameterType="com.lh.boot.mybatis.fkfb.entity.ProductDesc">
        update product_desc
        <set>
            <if test="storeId != null">
                store_id = #{storeId,jdbcType=BIGINT},
            </if>
            <if test="productSize != null">
                product_size = #{productSize,jdbcType=VARCHAR},
            </if>
            <if test="stock != null">
                stock = #{stock,jdbcType=BIGINT},
            </if>
            <if test="descInfo != null">
                desc_info = #{descInfo,jdbcType=VARCHAR},
            </if>
        </set>
        where product_id = #{productId,jdbcType=BIGINT}
    </update>
    <update id="updateByPrimaryKey" parameterType="com.lh.boot.mybatis.fkfb.entity.ProductDesc">
    update product_desc
    set store_id = #{storeId,jdbcType=BIGINT},
      product_size = #{productSize,jdbcType=VARCHAR},
      stock = #{stock,jdbcType=BIGINT},
      desc_info = #{descInfo,jdbcType=VARCHAR}
    where product_id = #{productId,jdbcType=BIGINT}
  </update>
</mapper>

三、个人总结

  1. 这种是一个相对简单的实现水平分表的方式,不用依赖多余的框架和中间件。
  2. 主要还是利用mybais拦截器实现改写sql,从而达到水平分表的方式。
  3. 只是在执行sql前对sql里面的表名进行了替换,实现相对简单。只支持单表的增删改查,不支持复杂的一些操作,例如分组、排序、聚合、分页以及多表关联查询。
  4. 现在我们已经实现了水平分表,如何实现水平分库呢?请参考【Java】Aop+自定义注解实现水平分库。
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
MyBatis拦截器自定义注解MyBatis框架中的两个重要特性。下面我会分别解释它们的作用和用法。 MyBatis拦截器是一种机制,可以在执行SQL语句的过程中对其进行拦截和修改。它提供了一种方便的方式来扩展和自定义MyBatis的功能。拦截器可以在SQL语句执行前后、参数设置前后、结果集处理前后等关键点进行拦截,并对其进行修改或增强。 要实现一个MyBatis拦截器,你需要实现`Interceptor`接口,并重写其中的方法。其中最重要的方法是`intercept`,它接收一个`Invocation`对象作为参数,通过该对象你可以获取到当前执行的SQL语句、参数等信息,并可以对其进行修改。另外还有`plugin`方法和`setProperties`方法用于对拦截器进行初始化。 自定义注解是一种用于标记和配置特定功能的注解。在MyBatis中,你可以使用自定义注解来配置一些特殊的功能,比如动态SQL的条件判断、结果集映射等。通过自定义注解,你可以将一些常用的功能封装成注解,并在需要时直接使用。 要使用自定义注解,你需要先定义一个注解,并在相应的地方使用该注解。然后通过MyBatis的配置文件或者Java代码进行配置,告诉MyBatis如何处理这些注解。在MyBatis的执行过程中,它会根据注解的配置来动态生成相应的SQL语句或者进行特定的处理。 总结一下,MyBatis拦截器自定义注解MyBatis框架中的两个重要特性。拦截器可以对SQL语句进行拦截和修改,自定义注解可以用于配置一些特殊功能。它们都提供了一种扩展和自定义MyBatis功能的方式。如果你有具体的问题或者需要更详细的示例代码,欢迎继续提问!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值