前情提要 :
在 《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 缓存机制(一级缓存 & 二级缓存),让你的应用更快更稳!
👍 如果你觉得有帮助,欢迎点赞、收藏、转发!
💬 有任何疑问,欢迎在评论区留言交流!