当前位置: 代码网 > it编程>编程语言>Java > MyBatis中关联查询的四种写法速览

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

2026年09月05日 Java 我要评论
先说一个最常见的业务场景,后面所有写法都围绕它:表:user(用户)、dept(部门)、role(角色)、user_role(用户角色中间表)需求:分页查用户列表,每行带出部门名称,以及该用户的角色列

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

  • 表:user(用户)、dept(部门)、role(角色)、user_role(用户角色中间表)
  • 需求:分页查用户列表,每行带出部门名称,以及该用户的角色列表(多对多)

一对一、多对一、多对多各占一个,标准 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>

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

  • resultmap 是纯手工活。主键列必须标 <id>,mybatis 靠它对 join 结果去重,把一个用户的多行折叠回一个对象;列名冲突全靠别名,user.namedept.namerole.name 三列同名,不起别名直接互相覆盖。关联每多一层,resultmap 再长一截,而且是从头到尾的手工配置。
  • 一对多加分页,结果是错的。分页插件把 limit 追加在这条 join sql 上,而 limit 限制的是 join 之后的行数。每页 10 行、每个用户平均挂 2.5 个角色,这一页实际只有 4 个用户;count 统计的也是 join 行数而不是用户数。页面上的表现就是"每页条数忽多忽少",数据一多才发现。
  • 结果集膨胀。用户 × 角色的行数在网络传输和内存里都乘了个系数,字段一多,传输量相当可观。

于是有了第二种写法。

二、写法二:分步查询,用 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() 的那一刻,但"推迟"本身又带来新的不确定性:

  • 接口返回 user 直接序列化成 json,序列化器访问 rolelist,懒加载在序列化阶段才触发,接口耗时抖动、行数不可预期;
  • 跨线程或异步任务里访问关联属性,sqlsession 已关闭,直接抛异常;
  • 给子查询传多个参数要用 column="{userid=id, deptid=dept_id}" 这种 map 语法,第一次见的人都会愣一下。

三、写法三:@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);

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

  • 角色还要嵌套菜单(多级关联)?注解套注解,一行写到三百字符;
  • 需要列别名、需要 <id> 去重?注解的表达能力都有,但堆在一起已经没法排版,也没法 review;
  • 大多数注解流的最终结局,是方法上出现 @resultmap("com.example.mapper.usermapper.userwithrelationsmap"),绕了一圈,指回了 xml。

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

最后一派干脆不用 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,没有行膨胀,分页正确。很多团队认真权衡之后,就停在这里。

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

  • 这六步样板,每个关联、每个接口都要重写一遍。用户带角色写了,角色带菜单再写,订单带明细又写;
  • sql 的条数和顺序靠人肉编排,漏掉一步,就是页面上悄悄空着的字段。不报错,只是空着;
  • 装配逻辑散在 service,两段查询之间的数据一致性靠自觉;
  • 十个人能写出十种组装风格,三年后没人敢动。

说白了,写法四是用 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关联查询的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com