java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis关联查询

MyBatis中关联查询的四种写法速览

作者:落木萧萧825

本文将详细拆解MyBatis关联查询的四种写法,分别是JOIN嵌套resultMap,分步查询,注解和内存组装,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下

先说一个最常见的业务场景,后面所有写法都围绕它:

一对一、多对一、多对多各占一个,标准 RBAC 结构,每个后台系统都有。

MyBatis 能做关联查询吗?能,至少有四种写法。但每一种都有它疼的地方。这篇把四种写法挨个过一遍,最后说说为什么四条路走到的都是同一个地方。

一、写法一:JOIN + 嵌套 resultMap,教科书答案

<resultMap id="userWithRelationsMap" type="User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <association property="dept" javaType="Dept">
        <id column="dept_id" property="id"/>
        <result column="dept_name" property="name"/>
    </association>
    <collection property="roleList" ofType="Role">
        <id column="role_id" property="id"/>
        <result column="role_name" property="name"/>
    </collection>
</resultMap>
<select id="selectUserPage" resultMap="userWithRelationsMap">
    select u.id,
           u.name,
           d.id   as dept_id,
           d.name as dept_name,
           r.id   as role_id,
           r.name as role_name
    from user u
    left join dept d       on u.dept_id = d.id
    left join user_role ur on u.id = ur.user_id
    left join role r       on ur.role_id = r.id
    order by u.id
</select>

能跑,文档里也是这么教的。但三个坑,用过的人都熟:

于是有了第二种写法。

二、写法二:分步查询,用 N+1 换 resultMap

把关联拆成子查询,主查询只查 user,关联按需触发:

<resultMap id="userMap" type="User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <association property="dept"
                 column="dept_id"
                 select="com.example.mapper.DeptMapper.selectById"/>
    <collection property="roleList"
                column="id"
                select="selectRolesByUserId"
                fetchType="lazy"/>
</resultMap>
<select id="selectUserPage" resultMap="userMap">
    select id, name, dept_id from user order by id
</select>
<select id="selectRolesByUserId" resultType="Role">
    select r.id, r.name
    from role r
    join user_role ur on ur.role_id = r.id
    where ur.user_id = #{userId}
</select>

resultMap 瘦了,分页也对了(LIMIT 作用在 user 表上)。代价转移到了 SQL 数量上:

查一页 100 个用户,就是 1 条主查询加 100 条部门加 100 条角色,201 条 SQL。这就是 N+1。

fetchType="lazy" 能把触发时机推迟到真正访问 getRoleList() 的那一刻,但"推迟"本身又带来新的不确定性:

三、写法三:@One / @Many 注解,上限来得比想象中早

注解版,试图连 XML 都省掉:

@Select("select id, name, dept_id from user where id = #{id}")
@Results({
    @Result(column = "id", property = "id", id = true),
    @Result(column = "name", property = "name"),
    @Result(column = "dept_id", property = "dept",
            one = @One(select = "com.example.mapper.DeptMapper.selectById")),
    @Result(column = "id", property = "roleList",
            many = @Many(select = "com.example.mapper.UserMapper.selectRolesByUserId"))
})
User selectById(Long id);

只有一个关联时,注解确实干净。再多就不行了:

四、写法四:内存组装,用业务代码补偿框架

最后一派干脆不用 MyBatis 的关联能力:查询拆开,Java 里自己拼。

public List<UserVo> listUserWithRelations(int page, int size) {
    List<User> users = userMapper.selectPage(page, size);          // 第 1 条
    if (users.isEmpty()) {
        return Collections.emptyList();
    }

    Set<Long> deptIds = users.stream()
            .map(User::getDeptId).collect(Collectors.toSet());
    Map<Long, Dept> deptMap = deptMapper.selectBatchIds(deptIds)   // 第 2 条
            .stream().collect(Collectors.toMap(Dept::getId, d -> d));

    List<Long> userIds = users.stream()
            .map(User::getId).collect(Collectors.toList());
    Map<Long, List<Role>> roleMap = roleMapper.selectByUserIds(userIds)   // 第 3 条
            .stream().collect(Collectors.groupingBy(RoleVo::getUserId));

    return users.stream().map(user -> {
        UserVo vo = new UserVo(user);
        vo.setDept(deptMap.get(user.getDeptId()));
        vo.setRoleList(roleMap.getOrDefault(user.getId(), Collections.emptyList()));
        return vo;
    }).collect(Collectors.toList());
}

平心而论,这是四种写法里 SQL 形态最健康的:三条批量语句,没有 N+1,没有行膨胀,分页正确。很多团队认真权衡之后,就停在这里。

但代价原样转移到了业务代码上:

说白了,写法四是用 Java 代码手工补上了 MyBatis 缺失的那个关联装配层。

五、为什么殊途同归

把四种写法的账摆在一起:

写法XML 量SQL 形态主要代价
JOIN + 嵌套 resultMap最多1 条 join手配映射、列别名、分页错乱、行膨胀
分步查询1 + NN+1;懒加载触发时机不可控
@One / @Many 注解初期为零1 + N复杂后无法排版,最终指回 XML
内存组装2~3 条批量装配样板 × 每个关联,散落业务代码

看起来是四个选项,其实背后是同一个缺口。关联本来是对象结构上的事,"用户有哪些角色"是 User 自己的结构属性,声明一次就该管所有查询。MyBatis 却把它做成了每次查询时的一次性手工配置。

resultMap、column 传参、内存组装,本质都是"每个查询重新手工表达一遍关联"。表达关联的劳动没有消失,只是在 XML、注解、Java 三个地方轮流转,转到最后还是 XML 最能打,所以"最后都变回了手写 XML"。

MyBatis-Plus 的态度更干脆:只做单表增强,关联查询请回 XML。于是 MP 项目也一样,单表用 Wrapper,一到 join,殊途同归。

(MyBatis-Flex 后来补了 @RelationOneToMany 这类关联注解,方向对了,但也从侧面印证了:这个缺口是真的,而且是框架级的。)

六、另一种思路:关联声明一次,处处生效

后来我换了一种思路:关联是实体上的声明,不是每次查询的配置。

@Entity
@Table(name = "user")
public class User {

    @Id
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "dept_id")
    @Fetch(FetchMode.BATCH)
    private Dept dept;

    @ManyToMany(mappedBy = "userList", fetch = FetchType.LAZY)
    @Fetch(FetchMode.BATCH)
    private List<Role> roleList;
}

声明一次,之后 userDao.findById(id) 直接返回装配好的对象,没有 resultMap,也没有组装代码。

抓取策略是个枚举,按场景换值:SIMPLE(逐条抓取,存在 N+1)、JOIN(联表,N+1 变 1+1)、BATCH(先查主表,再用 IN 批量抓关联,N+1 变 1+M)、NONE(这个查询不抓)。前四种写法各自的痛点,映射手配、N+1、注解排版、装配样板,到这里都退化成换一个枚举值的问题。

这套关联体系我陆陆续续做了一年多:一对一、一对多、多对多、抓取策略,还有那个绕不开的 N+1,都攒成了比较完整的机制。

结语

关联查询这件事,四种写法没有一种是"错的",选哪种都能交付。真正的问题是表达关联的劳动被框架留在了每一次查询里,于是一代代项目在同一个地方反复手工劳动。

以上就是MyBatis中关联查询的四种写法速览的详细内容,更多关于MyBatis关联查询的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文