一、序言

Mybatis-Plus在设计之初是为了扩展而不是替代Mybatis,所以对于连表查询官方并没有给出解决方法,还是依托Mybatis通过XML配置文件中写SQL语句的方式。但是在多数据源适配上,还是想要消除掉XML以屏蔽不同数据库类型的查询(新增加一个数据库,不需要新增加一个XML配置)。

最后采用第三方开源工具Mybatis-Plus-Join实现连表查询,开源地址:https://github.com/yulichang/mybatis-plus-join,支持一对一、一对多的操作。

二、具体实现

引入依赖

<dependency>
    <groupId>com.github.yulichang</groupId>
    <artifactId>mybatis-plus-join</artifactId>
    <version>1.2.4</version>
    <exclusions>
        <exclusion>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </exclusion>
    </exclusions>
</dependency>

之前已经分享过关于Mybatis-Plus代码分层和改造,所以修改XwMapper继承MPJBaseMapper,整体对于原由的结构没有影响。Mybatis-Plus-Join提供MPJLambdaWrapperMPJQueryWrapper实现连表查询。MPJLambdaWrapper支持Lambda表达式查询。

使用示例:

List<UserDTO> list = baseMapper.selectJoinList(UserDTO.class,
    new MPJLambdaWrapper<UserDO>()
            .selectAll(UserDO.class)
            .select(UserAddressDO::getTel)
            .selectAs(UserAddressDO::getAddress, UserDTO::getUserAddress)
            .select(AreaDO::getProvince, AreaDO::getCity)
            .leftJoin(UserAddressDO.class, UserAddressDO::getUserId, UserDO::getId)
            .leftJoin(AreaDO.class, AreaDO::getId, UserAddressDO::getAreaId)
            .eq(UserDO::getId, 1)
            .like(UserAddressDO::getTel, "1")
            .gt(UserDO::getId, 5));

对应的SQL语句:

SELECT t.id, t.name, t.sex, t.head_img, t1.tel, t1.address AS userAddress, t2.province, t2.city 
FROM user t
LEFT JOIN user_address t1 ON t1.user_id = t.id 
LEFT JOIN area t2 ON t2.id = t1.area_id 
WHERE ( t.id = ? AND t1.tel LIKE ? AND t.id > ?)

对应字段说明:

字段

含义

UserDTO

查询返回的结果,对应ResultType

UserDO

查询对应的Entity

selectAll

查询指定实体类的全部字段

select

查询指定实体的指定字段

leftJoin

第一个参数参与连表的实体类class;

第二个参数连表的ON字段,这个属性必须是第一个参数实体类的属性;

第三个参数: 参与连表的ON的另一个实体类属性

默认主表别名是t,其他的表别名以先后调用的顺序使用t1、t2、3;同时也支持分页查询。

IPage<UserDTO> list = baseMapper.selectJoinPage(new Page<>(2, 10), UserDTO.class,
    new MPJLambdaWrapper<UserDO>()
            .selectAll(UserDO.class)
            .select(UserAddressDO::getTel)
            .selectAs(UserAddressDO::getAddress, UserDTO::getUserAddress)
            .select(AreaDO::getProvince, AreaDO::getCity)
            .leftJoin(UserAddressDO.class, UserAddressDO::getUserId, UserDO::getId)
            .leftJoin(AreaDO.class, AreaDO::getId, UserAddressDO::getAreaId)
            .eq(UserDO::getId, 1)
            .like(UserAddressDO::getTel, "1")
            .gt(UserDO::getId, 5));

对应的SQL语句:

SELECT t.id, t.name, t.sex, t.head_img, t1.tel, t1.address AS userAddress, t2.province, t2.city 
FROM user t
LEFT JOIN user_address t1 ON t1.user_id = t.id 
LEFT JOIN area t2 ON t2.id = t1.area_id 
WHERE ( t.id = ? AND t1.tel LIKE ? AND t.id > ?)
LIMIT 2,10

条件查询,可以查询主表以及关联表的所有字段,调用Mybatis-Plus原生的方法,不会有SQL注入风险,但是上面的字段适合查询结果映射成单一属性的对象,当对象中包含对象或者对象中包含集合的时候,上面的方式就不适合了。

Mybatis-Plus-Join提供了@EntityMapping@FieldMapping 注解用来解决非单一属性对象连表查询的处理。通过在Entity中配置好对应的映射关系,再通过baseMapper调用Deep结尾的方法即可处理,例如:查询集合使用baseMapper#selectListDeep、分页查询baseMapper#selectPageDeep。

举个栗子:

@TableName("user")
public class UserDO {

    @TableId
    private Integer id;
    private Integer pid;

    /**
     * 查询上级 一对一
     */
    @TableField(exist = false)
    @EntityMapping(thisField = "pid", joinField = "id")
    private UserDO pUser;

    /**
     * 查询下级 一对多
     */
    @TableField(exist = false)
    @EntityMapping(thisField = "id", joinField = "pid")
    private List<UserDO> childUser;

    /**
     * 带条件的查询下级 一对多
     */
    @TableField(exist = false)
    @EntityMapping(thisField = "id", joinField = "pid",
            condition = {
                    @MPJMappingCondition(column = "sex", value = "0"),//sex = '0' 默认条件是等于
                    @MPJMappingCondition(column = "name", value = "张三", keyWord = SqlKeyword.LIKE)//name like '%a%'
            },
            apply = @MPJMappingApply(value = "id between 1 and 20"))//拼接sql 同 wrapper.apply()
    private List<UserDO> childUserCondition;

    /**
     * 查询地址 (一对多)
     */
    @TableField(exist = false)
    @EntityMapping(thisField = "id", joinField = "userId")
    private List<UserAddressDO> addressList;

    /**
     * 绑定字段 (一对多)
     */
    @TableField(exist = false)
    @FieldMapping(tag = UserDO.class, thisField = "id", joinField = "pid", select = "id")
    private List<Integer> childIds;
}

使用示例:

@Test
void test1() {
    UserDO deep = userMapper.selectByIdDeep(2);
    System.out.println(deep);
}

@Test
void test2() {
    List<UserDO> list = userMapper.selectListDeep(Wrappers.emptyWrapper());
    list.forEach(System.out::println);
}

@Test
void test3() {
    Page<UserDO> page = userMapper.selectPageDeep(new Page<>(2, 2), Wrappers.emptyWrapper());
    page.getRecords().forEach(System.out::println);
}

当查询映射的是实体,则使用@EntityMapping,而单一字段结果映射则使用 @FieldMapping,使用文档也给出了详细的说明,具体可以查看:MAPPING.md

三、结尾

Mybatis-Plus-Join连表查询对于单一属性对象的连表查询能很好的支持,查看源码是通过Mybatis-Plus,抽离成模板,通过自定义SQL注入器的方式实现。而对于非单一属性对象的查询是提供了两个注解,但底层是分成多条SQL语句采用IN查询得到结果后再聚合。

所以原先一条语句被分成了两条甚至多条语句去处理,中间多了网络开销。在1000w数据量下,对比过使用Mybatis-Plus-Join注解的方式和使用XML的方式,Mybatis-Plus-Join注解的方式在查询效率上会慢一倍,所以对于QPS要求比较高的系统来说,对于非单一属性对象的查询使用注解的这种方式并非是最优的。