Mybatis-Plus-Join对于连表查询映射成一个对象能够完美支持,但是对于一对一,一对多上面的方式就不支持,与之对应的Mybatis-Plus-Join提供了@EntityMapping 和 @FieldMapping通过注解的方式处理,但是本质是将关联关系拆分成了多条SQL语句去执行,在查询效率上会有损耗。
针对上面连表的查询场景的问题,回归到我们使用xml配置文件的方式,Mybatis对其的处理本质是将数据库的查询数据在内存里进行聚合和填充。
参考Mybatis的设计和约定大于配置的思想,在Service自定了selectJoin()和selectJoinList()用来支持连表查询,底部是调用baseMapper#selectMaps()查出所有的数据,再参考JPA的设计,通过自定义的@OneToMany和@OneToOne注解声明的对象映射关系,可以获取到的连表查询的元数据信息,再通过反射便可完成对象的数据聚合和填充。
以查询发送信息和发送状态的业务场景为例,先配置好数据对应的映射关系,一对一查询使用@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方法 **/
}
查询的时候,封装好查询的指定字段、连表关系以及查询条件,最后调用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());
}
对应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>
以客户信息和标签绑定信息为例,配置好用户跟客户标签的绑定关系,一对多查询使用@OneToMany,要求传入refEntity和targetEntity两个参数,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方法 **/
}
查询的时候,封装好查询的指定字段、连表关系以及查询条件,最后也是调用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());
}
对应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>
当查询映射结果是基本数据类型的时候,@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方法 **/
}
使用方法跟上面连表查询的结果是一样的,最后也是调用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());
}
对应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>
在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。本文主要分享的是封装接口的使用,后续将会继续分享接口内部数据聚合和填充的实现细节。