Skip to content

数据回显


在日常开发中,我们常常要查一张“主表”信息的同时,顺手把它关联的“多条”数据一并拉回来。
比如说:一头狼,不止猎过一次猎物,它的狩猎记录自然也不止一条。

需求场景就像这样 👇

根据狼的 ID,查询这头狼的基本信息,同时把它所有的狩猎记录也查出来。

对应的数据表设计很简单,只有两张表:

  • wolf:狼的基本信息(名字、性别、所属狼群……)
  • hunt_record:狩猎记录表(一头狼可能有多条记录)

两种常见的查询策略

这种“一对多”的关系,在项目里非常常见,有两条路可以走:

  • 策略一:分开查
    先查 wolf 表,拿到狼的基本信息;
    再用 wolf_idhunt_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 给补上了:主键标识 + 集合声明 + 明确别名,三件事到位,就能稳定产出“一只狼 + 多条狩猎记录”。

评论