• Mybatis-Plus之复查连表查询的设计和实现


    一、序言

    Mybatis-Plus-Join对于连表查询映射成一个对象能够完美支持,但是对于一对一,一对多上面的方式就不支持,与之对应的Mybatis-Plus-Join提供了@EntityMapping@FieldMapping通过注解的方式处理,但是本质是将关联关系拆分成了多条SQL语句去执行,在查询效率上会有损耗。

    二、优化方案

    针对上面连表的查询场景的问题,回归到我们使用xml配置文件的方式,Mybatis对其的处理本质是将数据库的查询数据在内存里进行聚合和填充。

    参考Mybatis的设计和约定大于配置的思想,在Service自定了selectJoin()selectJoinList()用来支持连表查询,底部是调用baseMapper#selectMaps()查出所有的数据,再参考JPA的设计,通过自定义的@OneToMany@OneToOne注解声明的对象映射关系,可以获取到的连表查询的元数据信息,再通过反射便可完成对象的数据聚合和填充。

    三、使用示例

    3.1 一对一查询

    以查询发送信息和发送状态的业务场景为例,先配置好数据对应的映射关系,一对一查询使用@OneToOne,要求传入refEntity,指定对应查询映射的Entity。

    @TableName(value ="keduw_msg_ticket")
    public class MsgTicketPms implements Serializable {
    	/** 省略其他字段 **/
    	
    	@TableField(exist = false)
        @OneToOne(refEntity = MsgStatereport.class)
        private MsgStatereport msgStateReport;
        
        /** 省略 get/set方法 **/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    查询的时候,封装好查询的指定字段、连表关系以及查询条件,最后调用selectJoinList()便可完成数据查询。

    @MPShardingAnno(tableSuffix = "#suffix")
    public List<MsgTicketStat> findTicketPmsStateReportByIdRangeAndPostTime(long beginId, int maxSize, @Param("suffix") String suffix, Date startPostTime, Date endPostTime){
    MPJLambdaWrapper<MsgTicketPms> query = MPJWrappers.lambdaJoin();
    query.select(
            MsgTicketPms::getId, 
            MsgTicketPms::getPackId, 
            MsgTicketPms::getFrameId
    ).select(
            MsgStatereport::getSubmitTime, 
            MsgStatereport::getDoneTime, 
            MsgStatereport::getResult
    ).leftJoin(MsgStatereport.class, on -> on.eq(MsgStatereport::getFrameId, MsgTicketPms::getFrameId).eq(MsgStatereport::getTicketSeqId, MsgTicketPms::getTicketSeqId))
            .gt(MsgTicketPms::getId, beginId)
            .ge(MsgTicketPms::getPostTime, startPostTime)
            .le(MsgTicketPms::getPostTime, endPostTime)
            .orderByAsc(MsgTicketPms::getId)
            .last(initLimit(maxSize));
    List<MsgTicketPms> msgTicketPms = selectJoinList(MsgTicketPms.class, query);
    return msgTicketPms.stream().map(TicketStat::buildPms).collect(Collectors.toList());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    对应Mybatis原生的数据映射关系,解决的是association标签的映射关系。

    <resultMap id="ticketResult" type="com.keduw.statistics.entity.MsgTicketStat">
            <id column="id" property="id" />
            <result column="pack_id" property="packId" />
            <result column="frame_id" property="frameId" />
            /** 省略一些映射 **/
            <association property="stateReportStat" resultMap="stateReportResult" />
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    3.2 一对多查询

    以客户信息和标签绑定信息为例,配置好用户跟客户标签的绑定关系,一对多查询使用@OneToMany,要求传入refEntitytargetEntity两个参数,refEntity关联查询的实体类型,定义是一个数组,可以传入多个实体,targetEntity目标映射的实体类型。

    客户标签的绑定关系分别来源于KdwCustomerTag的value和KdwCustomerTagMeta的name这两个属性,来源于不同的表,所以refEntity传入CustomerTag和CustomerTagMeta。

    @TableName(value ="contacts")
    public class Contacts implements Serializable {
    	/** 省略其他字段 **/
    
    	@TableField(exist = false)
        @OneToMany(refEntity = {CustomerTag.class, CustomerTagMeta.class}, targetEntity = CustomerTagDTO.class)
        private List<CustomerTagDTO> customerTags;
    
    	/** 省略 get/set方法 **/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    查询的时候,封装好查询的指定字段、连表关系以及查询条件,最后也是调用selectJoinList()便可完成数据查询。

    public List<ContactsVo> getContactsAndTags(List<String> codeList) {
        MPJLambdaWrapper<Contacts> lambdaWrapper = MPJWrappers.lambdaJoin();
        lambdaWrapper.select(
                Contacts::getId,
                Contacts::getCode,
                Contacts::getName,
                Contacts::getGroupIds
        )
                .selectAs(CustomerTag::getValue, CustomerTag::getValue)
                .selectAs(CustomerTagMeta::getName, CustomerTag::getMetaName)
                .leftJoin(CustomerTagBind.class, CustomerTagBind::getCustomerId, Contacts::getId)
                .leftJoin(CustomerTag.class, on -> on.eq(CustomerTag::getId, CustomerTagBind::getTagId).eq(CustomerTag::getRemove, RemoveEnum.NO.getCode()))
                .leftJoin(CustomerTagMeta.class, CustomerTagMeta::getId, CustomerTag::getMetaId)
                .eq(Contacts::getState, UserStateEnum.ON.getCode())
                .in(Contacts::getCode, codeList);
        List<Contacts> ContactsList = selectJoinList(Contacts.class, lambdaWrapper);
        return ContactsList.stream().map(ContactsVo::buildAll).collect(Collectors.toList());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    对应Mybatis原生的数据映射关系,解决collection标签映射关系。

    <resultMap id="contacts" type="com.keduw.common.entity.contacts.Contacts">
            <id property="id" column="id" />
            <result property="code" column="code" />
            /** 省略一些映射 **/
            <collection property="tags" ofType="com.keduw.common.entity.contacts.CustomerTag">
                <result property="metaName" column="meta_name"/>
                <result property="value" column="tag_value"/>
            collection>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当查询映射结果是基本数据类型的时候,@OneToMany还需要指定refField声明查询的字段。以查询客户绑定标签id为例,再原有Contacts的基础上加上标签绑定的映射关系:

    @TableName(value ="contacts")
    public class Contacts implements Serializable {
    	/** 省略其他字段 **/
    	
    	/**
         * 绑定标签id
         */
        @TableField(exist = false)
        @OneToMany(refEntity = CustomerTag.class, targetEntity = Long.class, refField = "tagId")
        private List<Long> tagIdList;
    
    	@TableField(exist = false)
        @OneToMany(refEntity = {CustomerTag.class, CustomerTagMeta.class}, targetEntity = CustomerTagDTO.class)
        private List<CustomerTagDTO> customerTags;
    
    	/** 省略 get/set方法 **/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    使用方法跟上面连表查询的结果是一样的,最后也是调用selectJoinList方法

    public List<CustomerTagBind> findCustomerTagBind(List<String> codeList){
            MPJLambdaWrapper<Contacts> query = MPJWrappers.lambdaJoin();
            query.select(
                    Contacts::getId,
                    Contacts::getCode
            ).select(
                    CustomerTagBind::getTagId
            ).innerJoin(CustomerTagBind.class, CustomerTagBind::getCustomerId, Contacts::getId)
                    .in(Contacts::getCode, codeList);
            List<Contacts> contacts = selectJoinList(Contacts.class, query);
            return contacts.stream().map(CustomerTagBind::build).collect(Collectors.toList());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对应Mybatis原生的数据映射关系,解决单一属性映射问题。

    <resultMap id="customerTagBind" type="com.keduw.statistics.entity.CustomerTagBind">
        <result column="customer_code" property="customerCode"/>
        <collection property="tagIdList" ofType="java.lang.Integer">
            <result column="tag_id" property="tagId"/>
        collection>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    selectJoinList()的基础上还提供了selectJoin(),返回连表查询的对象,具体用法跟上面的操作是一样的。

    整体说明如下:

    方法/注解说明
    selectJoin()查询结果返回一个实体
    selectJoinList()查询结果返回一个集合
    @OneToMany声明实体的映射关系为一对多
    其中:
    targetEntity:声明目标映射实体类型
    refEntity:声明关联查询实体类型
    refField:非必填,指定查询的字段,只有在单一字段查询的时候用到
    @OneToOne声明实体的映射关系为一对一
    refEntity:声明关联查询实体的类型

    性能处理方面,在700w客户数据、2.5亿标签数据的基础上,对比Mybatis-Plus-Join、Mybatis和封装查询的方法,整体查询效率跟原生Mybatis接近,比Mybatis-Plus-Join提升了2.5倍。

    在这里插入图片描述

    四、结尾

    使用selectJoin()selectJoinList(),本质是调用了baseMapper#selectMaps(),查询得到所有数据后在内存里做聚合和数据填充,聚合的维度是Mybatis-Plus-Join中的selectColumn。本文主要分享的是封装接口的使用,后续将会继续分享接口内部数据聚合和填充的实现细节。

  • 相关阅读:
    数据预处理大全
    月入3w的大二学生告诉你:副业真的没有那么难搞
    【C语言】【牛客刷题】【BC115】 小乐乐与欧几里得
    【Python、Qt】使用QItemDelegate实现单元格的富文本显示+复选框功能
    2024年6月15日 (周六) 叶子游戏新闻
    21.3 Python 使用DPKT分析数据包
    NIO知识总结三
    Linux内核源码分析 (B.9)深度解读 Linux 内核级通用内存池 —— kmalloc 体系
    【JavaEE】Spring核心与设计思想(控制反转式程序演示、IoC、DI)
    Android 多渠道配置
  • 原文地址:https://blog.csdn.net/hsf15768615284/article/details/126694954