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

文章目录

  • 一、背景
    • 1.1 环境信息
    • 1.2 场景
    • 1.3 表信息
  • 二、实现思路
    • 2.1 概述
    • 2.2 代码实现
      • 2.2.1 自定义mybatis拦截器
      • 2.2.2 自定义注解
      • 2.2.3 策略管理者
      • 2.2.4 分表策略抽象类
      • 2.2.5 产品表分表策略
      • 2.2.6 产品描述表分表策略
    • 2.3 使用
      • 2.3.1 产品信息表mapper接口
      • 2.3.2 产品信息表mapper.xml
      • 2.3.3 产品描述表mapper接口
      • 2.3.4 产品描述表mapper.xml
  • 三、个人总结

一、背景

1.1 环境信息

依赖 版本
windows 10
mysql Mysql 8.0.25
SpringBoot 2.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)不分表。
    【Mybatis】mybatis拦截器+自定义注解完成简单的水平分表_第1张图片

二、实现思路

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


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


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+自定义注解实现水平分库。

你可能感兴趣的:(Mysql,spring,boot,java,mysql,mybatis)