MyBatis基础入门《七》ResultMap 高级映射:一对一 & 一对多关联查询

前情提要

在 《MyBatis基础入门《六》动态SQL》 中,我们学会了让 SQL "活"起来。

但真实业务中,数据往往分散在多张表中------比如"订单属于用户"、"用户拥有多个地址"。

问题来了:如何将多表查询结果,自动映射成嵌套的 Java 对象?

答案 :使用 <resultMap>association(一对一) 和 collection(一对多)

本文将通过完整案例,手把手教你实现复杂对象映射。


一、场景设计:用户与订单(一对多)

假设数据库结构如下:

复制代码
-- 用户表
CREATE TABLE tbl_user (
    id INT PRIMARY KEY,
    username VARCHAR(50)
);

-- 订单表
CREATE TABLE tbl_order (
    id INT PRIMARY KEY,
    order_no VARCHAR(100),
    user_id INT,
    FOREIGN KEY (user_id) REFERENCES tbl_user(id)
);

Java 实体类:

复制代码
// User.java
public class User {
    private Integer id;
    private String username;
    private List<Order> orders; // 一个用户有多个订单
    // getter / setter
}

// Order.java
public class Order {
    private Integer id;
    private String orderNo;
    private Integer userId;
    // getter / setter
}

目标:查询用户及其所有订单,返回 User 对象(内含 orders 列表)。


二、方式一:嵌套 Select 查询(N+1 问题需注意)

1. Mapper 接口

复制代码
public interface UserMapper {
    // 查询所有用户(含订单)
    List<User> getUsersWithOrders();
    
    // 根据用户ID查订单(供嵌套调用)
    List<Order> getOrdersByUserId(Integer userId);
}

2. XML 映射

复制代码
<!-- UserMapper.xml -->
<resultMap id="userWithOrders" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    
    <!-- 一对多:collection -->
    <collection property="orders" 
                ofType="Order"
                select="com.charles.dao.UserMapper.getOrdersByUserId"
                column="id" />
</resultMap>

<select id="getUsersWithOrders" resultMap="userWithOrders">
    SELECT id, username FROM tbl_user
</select>

<select id="getOrdersByUserId" resultType="Order">
    SELECT id, order_no AS orderNo, user_id AS userId 
    FROM tbl_order 
    WHERE user_id = #{userId}
</select>

🔍 原理:

先查所有用户(1 次 SQL),再对每个用户 ID 调用 getOrdersByUserId(N 次 SQL)→ 共 N+1 次查询

✅ 优点:逻辑清晰,缓存友好

❌ 缺点:性能差(N+1 问题),不适用于大数据量


三、方式二:单条 SQL + 嵌套结果映射(推荐)

1. Mapper 接口(仅一个方法)

复制代码
List<User> getUsersWithOrdersByJoin();

2. XML 映射(使用 JOIN 一次查出所有数据)

复制代码
<resultMap id="userWithOrdersByJoin" type="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    
    <!-- collection 指定如何从结果集中提取 Order 子集 -->
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
        <result property="userId" column="user_id"/>
    </collection>
</resultMap>

<select id="getUsersWithOrdersByJoin" resultMap="userWithOrdersByJoin">
    SELECT 
        u.id AS user_id,
        u.username,
        o.id AS order_id,
        o.order_no,
        o.user_id
    FROM tbl_user u
    LEFT JOIN tbl_order o ON u.id = o.user_id
</select>

关键机制

MyBatis 会根据 <id>(主键)自动 去重并分组 ,将同一用户的多条订单合并到 orders 列表中。

✅ 优点:仅 1 次 SQL ,性能高

✅ 适用:绝大多数生产环境场景


四、扩展:一对一关联(如 用户-身份证)

假设 tbl_id_card 表存储身份证信息,一个用户只有一张身份证。

复制代码
public class User {
    private Integer id;
    private String username;
    private IdCard idCard; // 一对一
}

XML 映射:

复制代码
<resultMap id="userWithIdCard" type="User">
    <id property="id" column="user_id"/>
    <result property="username" column="username"/>
    
    <!-- 一对一:association -->
    <association property="idCard" javaType="IdCard">
        <id property="id" column="card_id"/>
        <result property="cardNumber" column="card_number"/>
    </association>
</resultMap>

<select id="getUserWithIdCard" resultMap="userWithIdCard">
    SELECT 
        u.id AS user_id,
        u.username,
        c.id AS card_id,
        c.card_number
    FROM tbl_user u
    LEFT JOIN tbl_id_card c ON u.id = c.user_id
    WHERE u.id = #{userId}
</select>

💡 association 用于一对一,collection 用于一对多,用法几乎一致。


五、单元测试验证

复制代码
@Test
public void testOneToMany() {
    SqlSession session = MyBatisUtil.getSqlSession();
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    List<User> users = mapper.getUsersWithOrdersByJoin();
    
    for (User user : users) {
        System.out.println("用户: " + user.getUsername());
        if (user.getOrders() != null) {
            user.getOrders().forEach(order -> 
                System.out.println("  └─ 订单: " + order.getOrderNo())
            );
        }
    }
    
    MyBatisUtil.closeSqlSession(session);
}

输出示例:

复制代码
用户: 张三
  └─ 订单: ORD2024001
  └─ 订单: ORD2024002
用户: 李四
  └─ 订单: ORD2024003

六、避坑指南 & 最佳实践

❗ 1. 必须为 <id> 指定主键

  • MyBatis 依靠 <id> 进行对象去重和分组;
  • 若漏写,可能导致数据重复或丢失。

❗ 2. 别名必须唯一且明确

  • u.id AS user_id, o.id AS order_id,避免字段名冲突。

✅ 3. 优先使用 JOIN + 嵌套结果

  • 避免 N+1 查询性能陷阱;
  • 可配合分页插件(如 PageHelper)使用。

🚀 4. 复杂场景可拆分为 DTO

  • 若关联层级过深(如 用户→订单→商品→分类),建议创建专用 DTO,而非强行嵌套实体类。

七、总结对比

方式 SQL 次数 性能 适用场景
嵌套 Select N+1 ❌ 差 小数据量、需独立缓存子查询
JOIN + 嵌套结果 1 ✅ 优 绝大多数生产场景

核心口诀
"一对多用 collection,一对一用 association;
主键别名要清晰,一次 JOIN 性能高!"


本文带你彻底攻克 MyBatis 多表关联映射难题。

下一篇我们将深入 MyBatis 缓存机制(一级缓存 & 二级缓存),让你的应用更快更稳!

👍 如果你觉得有帮助,欢迎点赞、收藏、转发!

💬 有任何疑问,欢迎在评论区留言交流!

相关推荐
qq_5895681018 小时前
mybatis-plus和springboot项目错误记录
spring boot·后端·mybatis
会编程的林俊杰19 小时前
Mapper解析
java·mybatis
Tan_Ying_Y20 小时前
Mybatis的mapper文件中#和$的区别
java·tomcat·mybatis
失足成万古风流人物1 天前
面试官问MyBatis/OpenFeign的原理?我手搓了个MyHttp怼回去!(反八股版)
mybatis·springboot·openfeign·动态代理
fanruitian1 天前
springboot-mybatisplus-demo
spring boot·后端·mybatis·mybatisplus
invicinble2 天前
spring相关系统性理解,企业级应用
java·spring·mybatis
gAlAxy...2 天前
MyBatis 核心配置文件 SqlMapConfig.xml 全解析
xml·mybatis
2501_916766542 天前
【Mybatis】延迟加载与多级缓存
缓存·mybatis
YDS8293 天前
MyBatis-Plus精讲 —— 从快速入门到项目实战
java·后端·spring·mybatis·mybatis-plus