Mybatis-Plus之单表操作和分表查询

一、序言

之前分享过关于Mybatis-Plus的模块集成和代码分层,文本分享关于Mybatis-Plus的单表操作和分表查询。

Mybatis-Plus对于单表提供了很强大的CRUD功能,核心主要还是依赖于Entity和Mapper,通过定义Entity和Mapper,Mybatis-Plus便能获取到表信息TableInfo,有了表的基本信息后便可为所欲为。

二、单表操作

以客户端信息统计查询为例,想要使用Mybatis-Plus,首先就是创建Entity和Mapper,出于扩展考虑,自定义XwMapper为统一接口。

public interface XwMapper<T> extends BaseMapper<T> {
}

通过MybatisX插件生成对应的Entity。

@TableName(value ="ipush_client_info")
public class IpushClientInfo implements Serializable {

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId
    private Long id;
    /**
     * 应用的主键
     */
    private Long appId;
    /**
     * 设备注册id
     */
    private String xwRegId;
    /**
     * 手机厂商的regId
     */
    private String thirdRegId;
    /**
     * 推送平台标识
     */
    private Integer platformFlag;
    /**
     * 使用的厂商通道标识
     */
    private String pushChannel;
    /**
     * 透传消息regid
     */
    private String passThroughRegId;
    /**
     * 透传消息通道
     */
    private String passThroughChannel;
    /**
     * 手机操作系统标识
     */
    private Integer os;
    /**
     * 设备唯一标识
     */
    private String deviceId;
    /**
     * 操作系统版本信息
     */
    private String osVersion;
    /**
     * 手机型号
     */
    private String phoneModel;
    /**
     * 手机品牌
     */
    private String brand;
    /**
     * 别名
     */
    private String userAlias;
    /**
     * 1在线,0离线
     */
    private Integer status;
    
    public static class FIELDS {
        public static final String BRAND = "brand";
        public static final String APP_Id = "app_id";
        public static final String CREATE_TIME = "create_time";
        public static final String USER_COUNT = "user_count";
    }

    /**
    * 省略get/set方法
    */ 

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

定义IpushClientInfoMapper,Mybatis-Plus提供单表的CRUD功能,具体可以查看官网文档:https://baomidou.com/pages/49cc81/。

@Mapper
public interface IpushClientInfoMapper extends XwMapper<UserInfo> {
}

继承XwWrapper定义IpushClientInfoWrapper,Wrapper中要求指定对应的Mapper和对应的Entity。Mybatis-Plus提供了Wrapper用于条件处理,支持使用QueryWrapper直接传入操作的列名,也支持使用Lambda表达式,最后调用baseMapper进行处理。

@Component
public class IpushClientInfoWrapper extends XwWrapper<IpushClientInfoMapper, IpushClientInfo> {
    
    /**
     * 使用QueryWrapper
     * @param startTime
     * @param endTime
     * @return
     */
    public List<IpushUserStatByDay> getIpushUserStatByDay(Date startTime, Date endTime){
        QueryWrapper<IpushClientInfo> query = Wrappers.query();
        query.select(
                IpushClientInfo.FIELDS.APP_Id,
                IpushClientInfo.FIELDS.BRAND
        ).between(IpushClientInfo.FIELDS.CREATE_TIME, startTime, endTime)
        .groupBy(IpushClientInfo.FIELDS.APP_Id, IpushClientInfo.FIELDS.BRAND);
        List<IpushClientInfo> clientInfos = baseMapper.selectList(query);
        return clientInfos.stream().map(IpushUserStatByDay::build).collect(Collectors.toList());
    }

    /**
     * 使用LambdaQueryWrapper
     * @param startTime
     * @param endTime
     * @param appId
     * @return
     */
    public int getAddUserCount(Date startTime, Date endTime, Long appId){
        LambdaQueryWrapper<IpushClientInfo> query = Wrappers.lambdaQuery();
        query.eq(IpushClientInfo::getAppId, appId)
             .between(IpushClientInfo::getCreateTime, startTime, endTime);
        return Parser.parserInt(baseMapper.selectCount(query));
    }
}

优先使用LambdaQueryWrapper操作,对于字段中存在函数的则使用QueryWrapper进行处理,对于结果则通过流式处理进行类型转换,数据类型转换的方法统一定义在DTO中,方法命名按照如下标准:

方法 含义
transfer() DTO转化成Entity
build() Entity转化成DTO

三、分表查询

对于分表的处理,可以通过Mybatis-Plus动态表名插件(DynamicTableNameInnerInterceptor)来实现,实现的思路主要如下:

自定义注解@MPShardingAnno来声明需要分表操作的方法,以及@Param声明需要进行补充表名的后缀。在注解解析器中,获取当前的表后缀存放到ThreadLocal中,在插件解析的时候从ThreadLocal中获取表后缀进行表名替换,等处理完后再释放对应的资源,出于扩展的考虑,还支持SpEL表达式。

自定义注解:

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

    /**
     * 获取分表日期的SpEL表达式
     */
    String dateExp() default "";

    /**
     * 已经拼装好的分天表后缀。如果配置了dateExp,则自动忽略该配置
     */
    String tableSuffix() default "";
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {

    String value();
}

定义TableNameContext来维护表后缀的上下文关系。

public class TableNameContext {

    private TableNameContext() {
    }

    /**
     * 维护表后缀
     */
    private static ThreadLocal<String> suffixTableLocal = new ThreadLocal<>();

    /**
     * 维护方法嵌套关系
     */
    private static ThreadLocal<Integer> numberThreadLocal = ThreadLocal.withInitial(() -> 0);

    /**
     * 初始化表后缀
     * @param tableSuffix
     */
    public static void initSuffix(String tableSuffix) {
        suffixTableLocal.set(tableSuffix);
    }

    /**
     * 获取表后缀
     * @return
     */
    public static String getSuffix(){
        return suffixTableLocal.get();
    }

    /**
     * 记录当前进入声明了{@link MPShardingAnno}的方法数
     */
    public static void addThreadNum() {
        Integer number = numberThreadLocal.get();
        number++;
        numberThreadLocal.set(number);
    }

    /**
     * 记录当前结束声明了{@link MPShardingAnno}的方法数
     * @return
     */
    public static Integer reduceThreadNum(){
        Integer number = numberThreadLocal.get();
        number--;
        numberThreadLocal.set(number);
        return number;
    }

    /**
     * 释放资源
     */
    public static void release(){
        suffixTableLocal.remove();
        numberThreadLocal.remove();
    }
}

自定义拦截切面,其中代码的注释都说明得很清楚,这里就不再累述。

@Component
@Aspect
@Order(1)
public class MPShardingAspect {

    @Pointcut("@annotation(com.keduw.common.mybatisplus.annotation.MPShardingAnno)")
    public void pointcut() {
    }

    /**
     * 从代理的参数中获取表表后缀
     */
    @Before("pointcut()")
    public void beforeExecute(JoinPoint joinPoint) throws NoSuchMethodException {
        Object target = joinPoint.getTarget();
        MethodSignature methdSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methdSignature.getMethod();
        // 获取注解所在方法的参数定义
        Parameter[] parameters = method.getParameters();
        if (parameters == null || parameters.length == 0) {
            parameters = new Parameter[0];
        }

        MPShardingAnno shardingAnno = method.getAnnotation(MPShardingAnno.class);
        // 针对jdk代理做兼容
        if (shardingAnno == null) {
            Method declaredMethod = target.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
            shardingAnno = declaredMethod.getAnnotation(MPShardingAnno.class);
            parameters = declaredMethod.getParameters();
        }
        String dateExp = shardingAnno.dateExp();

        // 获取spring的SpEL表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();

        // 获取注解所在方法的参数值
        List<String> paramNameList = new ArrayList<>();
        Object[] args = joinPoint.getArgs();
        for (Parameter parameter : parameters) {
            Param paramNameAnno = parameter.getAnnotation(Param.class);
            if (paramNameAnno != null) {
                String paramName = paramNameAnno.value();
                paramNameList.add(paramName);
            } else {
                paramNameList.add(parameter.getName());
            }
        }
        for (int i = 0; i < paramNameList.size(); i++) {
            context.setVariable(paramNameList.get(i), args[i]);
        }

        // 获取分表后缀
        String tableSuffix = "";
        if (StringUtils.isNotBlank(dateExp)) {
            tableSuffix = ShardingUtil.getTableSuffix(getDateFromExp(dateExp, parser, context));
        } else {
            tableSuffix = getTableSuffixFromExp(shardingAnno.tableSuffix(), parser, context);
        }
        // 记录表后缀
        TableNameContext.initSuffix(tableSuffix);

        // 记录调用的深度
        TableNameContext.addThreadNum();
    }

    @After("pointcut()")
    public void afterExecute() {
        Integer number = TableNameContext.reduceThreadNum();
        if(number <= 0){
            TableNameContext.release();
        }
    }

    /**
     * 根据日期的表达式,获取日期值
     *
     * @param dateExp 日期表达式
     * @param parser  表达式解析器
     * @param context 自定义上下文
     */
    private Date getDateFromExp(String dateExp, ExpressionParser parser, StandardEvaluationContext context) {
        if (StringUtils.isBlank(dateExp)) {
            throw new IllegalArgumentException("dateExp is empty");
        }
        Object dateObj = parser.parseExpression(dateExp).getValue(context);
        if (dateObj == null) {
            throw new IllegalArgumentException("dateExp is empty");
        }

        Date dateVal = null;
        if (dateObj instanceof Date) {
            dateVal = (Date) dateObj;
        } else if (dateObj instanceof Long) {
            dateVal = new Date((Long) dateObj);
        } else if (dateObj.getClass().isPrimitive() && "long".equals(dateObj.getClass().getTypeName())) {
            dateVal = new Date(Parser.parserLong(dateObj));
        } else {
            throw new IllegalArgumentException("dateExp support dataType:java.util.Date/java.lang.Long/java.lang.String/Long");
        }
        return dateVal;
    }

    private String getTableSuffixFromExp(String suffixExp, ExpressionParser parser, StandardEvaluationContext context) {
        if (StringUtils.isBlank(suffixExp)) {
            return "";
        }
        Object msgTypeObj = parser.parseExpression(suffixExp).getValue(context);
        return msgTypeObj != null ? msgTypeObj.toString() : null;
    }

}

最后在定义Mybatis-Plus插件中引入动态表名插件,进行表名替换。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    //动态表名插件
    DynamicTableNameInnerInterceptor innerInterceptor = new DynamicTableNameInnerInterceptor();
    innerInterceptor.setTableNameHandler((sql, tableName) -> {
        String suffix = TableNameContext.getSuffix();
        if(StringUtils.isNotBlank(suffix)){
            tableName += suffix;
        }
        return tableName;
    });
    interceptor.addInnerInterceptor(innerInterceptor);
    return interceptor;
}

注解使用Spring的Aspect切面来实现功能,所以需要启用Spring的Aspect切面支持,如果使用到了事务,需要在@Transactional注解的地方同时加上该注解。可以通过dateExp指定表后缀,也可以直接通过tableSuffix声明,具体使用如下:

@MPShardingAnno(dateExp = "#postTime")
public List<MsgFrame> getMsgFrameByCancel(long packId, @Param("postTime") Date postTime) {
    
}

@MPShardingAnno(dateExp = "#schedulePack.postTime")
public List<IccMsgFrame> findFrameByPackId(@Param("schedulePack") SchedulePack pack) {
    
}

@MPShardingAnno(tableSuffix = "#suffix")
public List<ContactsTerminal> getContactsTerminalByCode(String code, @Param("suffix") String tableSuffix) {
    
}

五、结尾

文章主要分享关于Mybatis-Plus的单表操作和分表查询,是代码改造实际落地过程中的一些思考和设计,后续还会继续分享关于Mybatis-Plus使用的一些心得,有兴趣的可以留下你的关注,互相学习。

你可能感兴趣的:(MybatisPlus,Mybatis-Plus,Mybatis,Java,CRUD,分表)