深浅模式
在日常开发中,我们常常要查一张“主表”信息的同时,顺手把它关联的“多条”数据一并拉回来。
比如说:一头狼,不止猎过一次猎物,它的狩猎记录自然也不止一条。
需求场景就像这样 👇
根据狼的 ID,查询这头狼的基本信息,同时把它所有的狩猎记录也查出来。
对应的数据表设计很简单,只有两张表:
wolf:狼的基本信息(名字、性别、所属狼群……)hunt_record:狩猎记录表(一头狼可能有多条记录)
两种常见的查询策略
这种“一对多”的关系,在项目里非常常见,有两条路可以走:
- 策略一:分开查
先查wolf表,拿到狼的基本信息;
再用wolf_id去hunt_record表里把这头狼的所有狩猎记录捞出来;
最后在 service 层手动拼装成一个完整的对象返回。 - 策略二:一次性查完(联表查询)
使用LEFT JOIN这样的联表 SQL,
一次就把“狼的基本信息 + 所有狩猎记录”查出来,交给 MyBatis 封装。
这节我们就走第二条路:一次性查询 + MyBatis 手动封装结果。
联表查询与路径参数
要查一头狼的狩猎史,最直接的方式就是联表。我们把“狼”的基本信息和它的“狩猎记录”放在两张表里,一张 wolf,一张 hunt_record。
查询时,不用两次来回折腾,也不需要额外封装,只要一条 SQL:
sql
SELECT
w.*,
hr.id AS hr_id,
hr.prey AS hr_prey,
hr.place AS hr_place,
hr.hunt_at AS hr_hunt_at
FROM wolf w
LEFT JOIN hunt_record hr ON w.id = hr.wolf_id
WHERE w.id = #{id};这条语句把狼的信息和它的狩猎记录一次性拉出来。
这里用的是 LEFT JOIN,有两个要点:
- 即便这头狼还没打过猎,也能查到它自己。
- 如果有多次狩猎,每一次都会对应一行结果。
所以结果集里同一只狼可能会被“重复”多次,每一行搭配一条狩猎记录。这不是错误,而是联表查询的特性。
Controller 接口设计
这一层的活儿其实很轻。URL 里直接带上狼的 ID,用 @PathVariable 接住,然后交给 Service:
java
@GetMapping("/wolves/{id}")
public WolfDTO getWolfWithHunts(@PathVariable Long id) {
return wolfService.getById(id);
}Controller 不去处理复杂的封装逻辑,它只是个分发口:
拿到路径参数 → 丢给 Service → 等着 Mapper 查完回结果。
最终期望的返回是一只狼对象,其中自带一组 huntList,也就是狩猎记录的集合。
注解式 @Select + resultType 的局限
最开始,很多人会想偷个懒,直接在 Mapper 上加一条:
java
@Select("SELECT ... LEFT JOIN ... WHERE w.id = #{id}")
@ResultType(WolfDTO.class)
WolfDTO getById(Long id);但这一套遇到联表就会翻车。原因很简单:
- 联表返回的是多行,而
resultType只懂“一行一对象”; - 它不会自动把多行聚合进
List<HuntRecord>; - 结果要么报错,要么出现重复狼对象,要么字段不完整。
resultType 擅长的是单表、字段一一对应的平铺结构。但要实现“一头狼 + 多条记录”的聚合,必须换一种方式告诉 MyBatis“哪些字段归主对象,哪些字段归列表”。
也就是我们接下来要用的——resultMap。
它能精确地定义映射关系,让 MyBatis 在处理多行数据时,聪明地把它们收拢成一个结构完整的对象。
使用 ResultMap 实现一对多映射
1)实体模型
我们希望返回“一只狼 + 它的所有狩猎记录”。主对象是 Wolf,集合是 List<HuntRecord>。字段名采用驼峰,数据库是下划线(建议全局开启 map-underscore-to-camel-case 以减少逐一映射的噪音)。
java
// com.wreckloud.wolfpack.domain.Wolf
public class Wolf {
private Long id;
private String name;
private Integer gender; // 1公 2母
private Long packId; // 所属狼群
private List<HuntRecord> huntList; // 一对多
// getter/setter
}
// com.wreckloud.wolfpack.domain.HuntRecord
public class HuntRecord {
private Long id;
private Long wolfId;
private String prey; // 猎物
private String place; // 地点
private java.time.LocalDateTime huntAt; // 时间
// getter/setter
}这里的关键是 Wolf.huntList,它对应的是“多条记录”的集合,等会儿用 <collection> 来收拢。
2)resultMap:主对象 + 集合的手动封装
resultMap 的职责是告诉 MyBatis:哪些列映射到狼,哪些列映射到狩猎记录集合。由于联表会把“同一只狼”重复多行铺开,我们必须标出主对象和子对象的主键 <id>,这样 MyBatis 才能把多行聚合回同一个 Wolf,并把每一行的“子部分”收入 huntList。
xml
<!-- resources/mapper/WolfMapper.xml -->
<resultMap id="wolfWithHuntsMap" type="com.wreckloud.wolfpack.domain.Wolf">
<!-- 主对象:狼 -->
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="gender" property="gender"/>
<result column="pack_id" property="packId"/>
<!-- 一对多集合:huntList -->
<collection property="huntList" ofType="com.wreckloud.wolfpack.domain.HuntRecord">
<!-- 子对象主键一定要标出,便于去重聚合 -->
<id column="hr_id" property="id"/>
<result column="hr_wolf_id" property="wolfId"/>
<result column="hr_prey" property="prey"/>
<result column="hr_place" property="place"/>
<result column="hr_hunt_at" property="huntAt"/>
</collection>
</resultMap>要点很直白:主对象用 id/result 接住基础字段;集合用 <collection> 声明类型,并为子对象字段使用列别名(如 hr_prey)。这样一来,联表产生的多行数据会被“拢成”一个 Wolf 实例,huntList 填入多条记录;如果这只狼没有任何狩猎记录,huntList 就是空集合,但 Wolf 仍然能被查出来。
3)Mapper 查询(配合列别名)
SQL 一次搞定:把“狼的列”与“狩猎列”明确起别名,避免字段名撞车,同时让 resultMap 好识别。联表仍然用 LEFT JOIN,保证“无记录也能查到狼”。
xml
<select id="getById" resultMap="wolfWithHuntsMap">
SELECT
w.id,
w.name,
w.gender,
w.pack_id,
hr.id AS hr_id,
hr.wolf_id AS hr_wolf_id,
hr.prey AS hr_prey,
hr.place AS hr_place,
hr.hunt_at AS hr_hunt_at
FROM wolf w
LEFT JOIN hunt_record hr ON w.id = hr.wolf_id
WHERE w.id = #{id}
</select>这套组合解决的不是“能不能查多行”,而是“怎么把多行聚合到一个对象的集合属性里”。resultType 做不到的聚合,resultMap 给补上了:主键标识 + 集合声明 + 明确别名,三件事到位,就能稳定产出“一只狼 + 多条狩猎记录”。

评论