后端笔记之MyBatis 通过 collection 标签实现树形结构自动递归查询

简单来说就是:MyBatis 会自动帮你一层层查父子节点,最终在内存中拼出完整的树,无需手动写循环组装。

核心逻辑:"有子节点就查,无子节点就停"

递归查询的终止条件是:当 selectOtherList 执行后返回空结果(即当前节点没有子节点),此时不再继续向下查询。

举例查询次数拆解:

主查询 selectTreeData:查询 1 次(获取一级节点 id=1);

第一次递归:对 id=1 调用 selectOtherList,查询 1 次(获取二级节点 id=2);

第二次递归:对 id=2 调用 selectOtherList,查询 1 次(获取三级节点 id=3);

第三次递归:对 id=3 调用 selectOtherList,返回空结果,终止递归。

如果调用selectTreeData 方法,核心是以 level=1 为起点,通过递归查询自动关联所有子层级节点,最终返回的是包含完整层级关系的树形数据,而非仅 level=1 的节点。这也是 MyBatis 这种配置实现树形查询的核心价值。

原始代码

用例子拆解全过程:

假设数据库表 a_student 中的数据是一棵三级树:

1. 核心配置:

要实现自动递归,需要在 MyBatis 映射文件中定义一个包含 collection 标签的 resultMap,例如:

xml 复制代码
<!-- 定义树形结果映射 -->
<resultMap id="studentTreeResultMap" type="org.jit.sose.entity.test.Student">
   <!-- 映射基础字段(id、name、lastId等) -->
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="lastId" column="last_id"/>
    <!-- 其他字段映射... -->

    <!-- 关键:通过collection自动查询子节点 -->
    <collection
            property="childList"  <!-- 对应实体类中的childList属性 -->
    ofType="org.jit.sose.entity.assessment.Student"  <!-- 子节点类型 -->
    select="selectOtherList"  <!-- 查询子节点的方法ID -->
    column="id"  <!-- 传递当前节点的id作为子节点的lastId参数 -->
    />
</resultMap>

<!-- 主查询:获取一级指标(level=1) -->
<select id="selectTreeData" resultMap="studentTreeResultMap">
    SELECT id, last_id, name, level FROM a_student
    WHERE level = 1 AND state = 'A' AND plan_id = #{planId}
</select>

<!-- 子查询:根据父节点id查询子节点(last_id=父id) -->
<select id="selectOtherList" resultMap="studentTreeResultMap" parameterType="int">
    SELECT id, last_id, name, level FROM a_student
    WHERE last_id = #{id} AND state = 'A'
</select>

2. 执行流程(自动递归查询):

第一步:执行主查询 selectTreeData

作用:查询所有一级指标(level=1,last_id=null)。

结果:得到 id=1(一级指标 A)和 id=4(一级指标 B)两个对象,此时它们的 childList 暂时为空。

第二步:MyBatis 自动触发子查询 selectOtherList(第一次递归)

对主查询结果中的每个一级指标,MyBatis 会读取其 id(如 id=1),作为参数调用 selectOtherList。

selectOtherList 的逻辑:查询 last_id=1 的记录(即一级指标 A 的子节点)。

结果:查询到 id=2(二级指标 A1),MyBatis 会将这个节点存入一级指标 A 的 childList 中。

第三步:递归查询更深层级(第二次递归)

由于 selectOtherList 的 resultMap 也是 studentTreeResultMap(自身),MyBatis 会对刚查询到的二级指标 A1(id=2)再次触发 selectOtherList。

此时参数是 id=2,查询 last_id=2 的记录,得到 id=3(三级指标 A1-1)。

结果:将三级指标 A1-1 存入二级指标 A1 的 childList 中。

第四步:终止递归

当查询 selectOtherList 时,如果没有符合条件的记录(例如三级指标 A1-1 没有子节点,last_id=3 无结果),递归终止。

对一级指标 B(id=4)执行同样逻辑:如果它没有子节点,childList 会保持空集合。

3. 最终内存中的树形结构

经过上述递归查询,childList 会自动填充所有子节点,形成完整的树:

java 复制代码
List<Student> result = [
    // 一级指标A
    Student(
        id=1,
        name="一级指标A",
        lastId=null,
        childList=[  // 二级子节点
            Student(
                id=2,
                name="二级指标A1",
                lastId=1,
                childList=[  // 三级子节点
                    Student(
                        id=3,
                        name="三级指标A1-1",
                        lastId=2,
                        childList=[]  // 无更深层级
                    )
                ]
            )
        ]
    ),
    // 一级指标B(无子女)
    Student(
        id=4,
        name="一级指标B",
        lastId=null,
        childList=[]
    )
]
java 复制代码
@Data
public class Student{

    private Integer id;

    private String name;

    private Integer level;// 等级:级别(1/2/3)

    private Integer lastId; // 上级ID:用于建立指标层级关系

    List<Indicator> childList;

    /**
     * 树节点设置不能选定
     */
    private Boolean disabled;

    private Integer seq; // 序号: 用于排序
}

核心总结

(1)自动递归:通过 collection 标签的 select 属性,MyBatis 会以当前节点 id 为参数,自动调用子查询,层层嵌套查询子节点。

(2)无需手动组装:省去了在 Service 层写循环拼接 childList 的代码,MyBatis 直接在内存中完成树形结构的组装。

(3)childList 的作用:作为树形结构的载体,最终序列化 JSON 时自然体现父子层级关系。

这种方式的优点是配置简单,适合层级不深的树形结构;缺点是可能产生多次数据库查询(每层一次),层级过深时性能会受影响。

javascript 复制代码
[
  {
    "id": 1,
    "name": "一级指标A",
    "lastId": null,
    "childList": [
      {
        "id": 2,
        "name": "二级指标A1",
        "lastId": 1,
        "childList": [
          {
            "id": 3,
            "name": "三级指标A1-1",
            "lastId": 2,
            "childList": []
          }
        ]
      }
    ]
  },
  {
    "id": 4,
    "name": "一级指标B",
    "lastId": null,
    "childList": []
  }
]

优化代码

xml 复制代码
<sql id="Base_Column_List" >
   id, last_id, name, level,seq
</sql>
<resultMap id="studentTreeResultMap" type="org.jit.sose.entity.test.Student">
    <collection property="childList" column="{lastId=id}" ofType="org.jit.sose.entity.test.Student"
                select="selectOtherList"></collection>
</resultMap>


<!-- 结果映射:使用 studentTreeResultMap,意味着查询到的每个一级节点都会触发 <collection> 配置,自动查询其子节点 -->
<select id="selectTreeData" resultMap="studentTreeResultMap" parameterType="java.lang.Integer">
	  SELECT
	  <include refid="Base_Column_List" />
	  FROM a_student
	  WHERE state='A'
	  AND level = 1  
	  order by seq  <!-- 按排序号升序排列 -->
</select>

<!-- 由于其结果映射也是 studentTreeResultMap,因此查询到的每个子节点会再次触发 <collection> 配置,继续查询它的子节点(即 "孙子节点"),以此类推,直到没有更深层级的节点 -->
<select id="selectOtherList" resultMap="studentTreeResultMap" parameterType="org.jit.sose.entity.test.Student">
	  SELECT
	  <include refid="Base_Column_List" />
	  FROM a_student
	  WHERE state='A'
	  AND last_id=#{lastId,jdbcType=INTEGER}  <!-- 按父节点 ID 查询子节点 -->
	  order by seq
</select>

这两段代码都是 MyBatis 中用于查询树形结构的配置,但在子节点查询参数传递、结果映射复用、查询逻辑细节等方面存在差异,具体区别如下:

1. collection 标签的参数传递方式不同

第一段代码:

xml 复制代码
<collection 
    property="childList" 
    ofType="org.jit.sose.entity.assessment.Student" 
    select="selectOtherList" 
    column="id"  <!-- 传递当前节点的id作为参数 -->
/>

column="id" 表示:将当前节点的 id 字段作为参数,传递给子查询 selectOtherList。

子查询接收参数时直接用 #{id}(因为参数是单个值 id):

xml 复制代码
<select id="selectOtherList" parameterType="int">
    WHERE last_id = #{id}  <!-- 直接使用id作为参数 -->
</select>

第二段代码:

xml 复制代码
<collection 
    property="childList"  <!-- 对应 Student 类的 childList 属性(子节点列表) -->
    column="{lastId=id}"  <!-- 传递当前节点的 id 作为子查询的 lastId 参数 -->
    ofType="org.jit.sose.entity.test.Student"  <!-- 子节点类型为 Student -->
    select="selectOtherList"  <!-- 子查询的 ID(用于查询当前节点的子节点) -->
  />

column="{lastId=id}" 表示:将当前节点的 id 字段封装成键值对 {lastId: id} 传递给子查询。

子查询接收参数时需要用 #{lastId}(与键名对应):

xml 复制代码
<select id="selectOtherList" parameterType="org.jit.sose.entity.test.Student">
    WHERE last_id=#{lastId,jdbcType=INTEGER}  <!-- 使用lastId作为参数 -->
</select>

核心差异:参数传递方式不同,第一段是 "单值传递",第二段是 "命名参数传递"(更灵活,支持多参数)。

2. resultMap 的复用与递归逻辑不同

第一段代码:

主查询 selectTreeData和子查询 selectOtherList 共用同一个 resultMap="studentTreeResultMap"。

由于 studentTreeResultMap中包含 collection 标签,子查询的结果会再次触发 collection 配置,实现自动递归查询(无需手动控制层级,直到没有子节点为止)。

第二段代码:

主查询和子查询共用 resultMap="studentTreeResultMap",该 resultMap 同样包含 collection 标签,理论上也能递归,但参数传递是命名参数(lastId),与第一段的单值参数逻辑不同。

此外,第二段明确指定了 parameterType="org.jit.sose.entity.test.Student"(子查询接收一个对象参数),而第一段子查询的 parameterType="int"(接收单个整数)。

核心差异:参数类型和传递逻辑不同,但递归原理一致(均通过 collection 标签实现多层级查询)。

3. 查询字段的定义方式不同

第一段代码:

主查询和子查询的字段是直接写在 SQL 中的(SELECT id, last_id, name, level ...),硬编码在 <select> 标签内。

第二段代码:

通过 <sql id="Base_Column_List"> 定义了公共字段片段(id, last_id, name, level, seq),并在查询中用 <include refid="Base_Column_List" /> 引用。

好处是:如果需要修改查询字段,只需修改 Base_Column_List 片段,无需修改所有 SQL,复用性更高。

核心差异:第二段使用了 SQL 片段复用,第一段是硬编码字段。

4. 子查询的 parameterType 不同

第一段代码:子查询 selectOtherList 的 parameterType="int",表示接收一个整数参数(当前节点的 id)。

第二段代码:子查询 selectOtherList 的 parameterType="org.jit.sose.entity.test.Student",表示接收一个 Student对象参数(通过对象的 lastId 属性取值)。

核心差异:参数类型不同,第一段是基本类型,第二段是对象类型(更适合多参数场景)。

总结:核心区别对比表

实际效果差异

(1)两段代码最终都能查询出树形结构(通过 childList 体现父子关系)。

(2)第二段代码的 SQL 片段复用更符合代码规范,维护性更好。

(3)第二段的命名参数传递({lastId=id})支持传递多个参数(例如 column="{lastId=id, planId=planId}"),灵活性更高,而第一段仅支持单参数。

(4)若字段需要扩展(如新增字段),第二段只需修改 Base_Column_List,第一段需逐个修改 SQL,因此第二段更优。

selectTreeData与selectOtherList使用场景

调用 selectOtherList 与调用 selectTreeData 的结果有本质区别,核心差异在于查询的起点和返回的树形结构范围,具体区别如下:

1. 查询起点不同

selectTreeData:以 level=1 的一级节点为起点(根节点),查询条件明确限制 level = 1,因此返回的是完整的树形结构(从一级节点开始,包含所有子层级)。

selectOtherList:没有固定的起点,需要传入一个 lastId 参数(父节点 ID),查询条件是 last_id = #{lastId},因此返回的是以 lastId 对应节点为父节点的子节点树(即从某个节点的子节点开始,包含其所有后代)。

2. 返回结果的范围不同

假设数据库中存在如下树形结构(state='A'):

javascript 复制代码
level=1(id=1)
└── level=2(id=2,last_id=1)
    └── level=3(id=3,last_id=2)
level=1(id=4)

(1)调用 selectTreeData 的结果:

返回以 level=1 为根的完整树,包含所有一级节点及其后代:

bash 复制代码
[
  {
    "id": 1,
    "level": 1,
    "childList": [
      {
        "id": 2,
        "level": 2,
        "childList": [
          {
            "id": 3,
            "level": 3,
            "childList": []
          }
        ]
      }
    ]
  },
  {
    "id": 4,
    "level": 1,
    "childList": []
  }
]

(2)调用 selectOtherList(lastId=1) 的结果:

传入 lastId=1(即父节点为 id=1),返回以 id=1 的子节点为起点的树(仅包含 id=1 的后代,不包含 id=1 本身和其他一级节点):

bash 复制代码
[
  {
    "id": 2,
    "level": 2,
    "childList": [
      {
        "id": 3,
        "level": 3,
        "childList": []
      }
    ]
  }
]

(3)调用 selectOtherList(lastId=2) 的结果:

传入 lastId=2,返回以 id=2 的子节点为起点的树(仅包含 id=2 的后代):

bash 复制代码
[
  {
    "id": 3,
    "level": 3,
    "childList": []
  }
]

(4)调用 selectOtherList(lastId=null) 的结果:

传入 lastId=null,查询 last_id=null 的节点(通常是一级节点),但返回的是这些一级节点的子树(不包含一级节点本身,仅包含它们的后代):

bash 复制代码
[
  {
    "id": 2,  // 一级节点id=1的子节点
    "level": 2,
    "childList": [{"id": 3, ...}]
  }
]

3. 使用场景不同

selectTreeData:用于查询 "完整的树形结构",适合在页面初始化时展示整个树(如左侧导航树、完整组织架构)。

selectOtherList:用于查询 "某个节点的子树",适合动态加载场景(如点击某个节点后,仅加载其下的子节点,减少初始加载数据量)。

总结:核心区别表

简单说:selectTreeData 查 "全树",selectOtherList 查 "某节点的子树",两者是整体与局部的关系。

相关推荐
Achou.Wang3 小时前
Kubernetes 的本质:一个以 API 为中心的“元操作系统”
java·容器·kubernetes
z晨晨3 小时前
Java求职面试实战:从Spring到微服务的全面挑战
java·数据库·spring·微服务·面试·技术栈
麦兜*3 小时前
Redis多租户资源隔离方案:基于ACL的权限控制与管理
java·javascript·spring boot·redis·python·spring·缓存
聪明的笨猪猪4 小时前
Java SE “异常处理 + IO + 序列化”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
毕设源码-赖学姐4 小时前
【开题答辩全过程】以 SpringbootVueUniapp农产品展销平台为例,包含答辩的问题和答案
java·eclipse
User_芊芊君子4 小时前
【Java ArrayList】底层方法的自我实现
java·开发语言·数据结构
敲代码的嘎仔4 小时前
牛客算法基础noob56 BFS
java·开发语言·数据结构·程序人生·算法·宽度优先
GalenZhang8884 小时前
Springboot调用Ollama本地大模式
java·spring boot·后端