MyBatis + MySQL 实现 ResultMap + Collection 一对多查询
ResultMap + collection 是 MyBatis 中一对多关联查询的核心用法(比如:1 个用户对应多个订单、1 个班级对应多个学生)。
核心场景:主表一条数据,关联从表多条数据 ,用 collection 标签将从表的多条数据封装成集合,存入主表实体类中。
本文以 用户表 (user) ↔ 订单表 (order) 经典一对多场景为例,完整演示用法。
一、准备工作
1. 新建 MySQL 表
-- 用户表(主表:一的一方)
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(20) NOT NULL,
age INT
);
-- 订单表(从表:多的一方)
CREATE TABLE `order` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL, -- 订单编号
user_id BIGINT NOT NULL, -- 关联用户ID
total_price DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES user(id)
);
-- 插入测试数据
INSERT INTO user(username, age) VALUES ('张三', 20), ('李四', 25);
INSERT INTO `order`(order_no, user_id, total_price) VALUES
('ORDER_001', 1, 99.99),
('ORDER_002', 1, 199.99),
('ORDER_003', 2, 299.99);
2. 实体类
1. 订单实体(从表)
import lombok.Data;
import java.math.BigDecimal;
@Data // Lombok 自动生成get/set/toString
public class Order {
private Long id;
private String orderNo; // 订单编号
private Long userId; // 关联用户ID
private BigDecimal totalPrice; // 订单总价
}
2. 用户实体(主表,包含订单集合)
import lombok.Data;
import java.util.List;
@Data
public class User {
private Long id;
private String username;
private Integer age;
// 一对多:一个用户对应多个订单
private List<Order> orderList;
}
二、核心实现:嵌套结果映射(推荐 ✅)
最优方案 :MySQL 联表查询(LEFT JOIN)+ MyBatis 一次性映射结果,无 N+1 查询问题,性能最好。
1. UserMapper 接口
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
// 查询所有用户 + 每个用户的订单列表
List<User> selectAllUserWithOrders();
// 根据用户ID查询用户 + 订单列表
User selectUserWithOrdersById(@Param("userId") Long userId);
}
2. UserMapper.xml 核心配置(重点)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 1. 定义订单的 ResultMap(复用) -->
<resultMap id="OrderResultMap" type="com.example.entity.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="user_id" property="userId"/>
<result column="total_price" property="totalPrice"/>
</resultMap>
<!-- 2. 核心:用户 ResultMap + collection 一对多映射 -->
<resultMap id="UserWithOrderResultMap" type="com.example.entity.User">
<!-- 主表:用户字段映射 -->
<id column="user_id" property="id"/> <!-- 必须写id!MyBatis靠主键去重 -->
<result column="username" property="username"/>
<result column="age" property="age"/>
<!-- 一对多:collection 标签封装订单集合
property:User实体中 订单集合的属性名
ofType:集合泛型的 全类名
resultMap:引用上方定义的订单ResultMap
-->
<collection
property="orderList"
ofType="com.example.entity.Order"
resultMap="OrderResultMap"/>
</resultMap>
<!-- 3. 联表查询SQL:LEFT JOIN 保证无订单的用户也能查到 -->
<select id="selectAllUserWithOrders" resultMap="UserWithOrderResultMap">
SELECT
u.id AS user_id,
u.username,
u.age,
o.id AS order_id,
o.order_no,
o.user_id,
o.total_price
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
</select>
<!-- 根据ID查询 -->
<select id="selectUserWithOrdersById" resultMap="UserWithOrderResultMap">
SELECT
u.id AS user_id,
u.username,
u.age,
o.id AS order_id,
o.order_no,
o.user_id,
o.total_price
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.id = #{userId}
</select>
</mapper>
还可以这么简写
我直接把非必须的映射全部删掉,这是最精简的可用版本:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserWithOrderResultMap" type="com.example.entity.User">
<!-- 🔴 必写:用户主键(唯一必须映射的主表字段) -->
<id column="user_id" property="id"/>
<!-- 👇 普通字段:想映射就写,不想写直接删! -->
<result column="username" property="username"/>
<!-- <result column="age" property="age"/> 不需要就注释/删除 -->
<!-- 🔴 必写:集合 + 订单主键(唯一必须映射的从表字段) -->
<collection property="orderList" ofType="com.example.entity.Order">
<id column="order_id" property="id"/>
<!-- 👇 普通字段:想映射就写,不想写直接删! -->
<!-- <result column="order_no" property="orderNo"/> -->
<!-- <result column="total_price" property="totalPrice"/> -->
</collection>
</resultMap>
<!-- SQL 不变 -->
<select id="selectAllUserWithOrders" resultMap="UserWithOrderResultMap">
SELECT
u.id AS user_id,
u.username,
u.age,
o.id AS order_id,
o.order_no,
o.total_price
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
</select>
</mapper>
三、补充:嵌套查询(不推荐 ❌)
原理:先查主表,再循环查从表 ,会产生 N+1 性能问题(1 条主表 SQL + N 条从表 SQL),仅作了解。
修改 UserMapper.xml
<!-- 嵌套查询:先查用户,再查订单 -->
<resultMap id="UserWithOrderSelectMap" type="com.example.entity.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="age" property="age"/>
<!-- collection:select指定从表查询方法,column指定传递的参数 -->
<collection
property="orderList"
ofType="com.example.entity.Order"
select="com.example.mapper.OrderMapper.selectOrderByUserId"
column="id"/>
</resultMap>
<select id="selectUserWithOrdersById" resultMap="UserWithOrderSelectMap">
SELECT id, username, age FROM user WHERE id = #{userId}
</select>
<!-- OrderMapper.xml -->
<select id="selectOrderByUserId" resultType="com.example.entity.Order">
SELECT * FROM `order` WHERE user_id = #{userId}
</select>