• 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> {
    }
    
    • 1
    • 2

    通过IDEA的MybatisX插件,我们可以很方便的依赖数据库表生成相应的实体,MybatisX的具体使用这里就不赘述,网上有很多相应的资料。​Mybatis-Plus对于查询语句可以支持Lambda 表达式,也可以直接写查询字段对应的属性,其中FIELDS是用来定义一些字符串类型的字段,以兼容没法使用Lambda 表达式的场景。

    @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;
        
        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);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

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

    @Mapper
    public interface IpushClientInfoMapper extends XwMapper<UserInfo> {
    }
    
    • 1
    • 2
    • 3

    继承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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    优先使用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 "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface Param {
    
        String value();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    定义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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

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

    @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;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112

    最后在定义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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注解使用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) {
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    五、结尾

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

  • 相关阅读:
    Java项目:SSM网上鲜花商城
    人工智能算法工程师(高级)课程11-自然语言处理之NLP的语言模型-seq2seq模型,seq+注意力与代码详解
    网络安全等级保护2.0自查表 | 管理部分
    UVM中config_db机制的使用方法
    【python入门专项练习】-N04.运算符
    为springboot找到合适的springcloud版本和springcloud alibaba版本
    2022 12 3
    Anaconda的升级、配置及使用
    java毕业设计在线教育平台Mybatis+系统+数据库+调试部署
    性能测试工具Jmeter你所不知道的东西····
  • 原文地址:https://blog.csdn.net/hsf15768615284/article/details/126453907