深入理解MyBatis:collection集合封装的底层原理与实现细节

发布时间:2026/6/16 18:00:03
深入理解MyBatis:collection集合封装的底层原理与实现细节 相信很多人在使用 MyBatis 做一对多关联查询时都会用到resultMap中的collection标签能轻松把数据库中扁平化的多行数据封装成包含嵌套集合的 Java 对象。但你有没有好奇过MyBatis 底层到底是怎么完成这个 “数据重组” 的为什么同样的主键数据不会重复创建对象今天我就结合实际案例带大家一步步拆解collection集合封装的完整流程和核心机制。一、先看一个最典型的一对多场景我们先从最常见的 “用户 - 订单” 关系入手这也是理解一对多封装的最佳案例。假设我们执行一条关联查询 SQL从数据库中查出了以下 3 条原始数据idusernameorder_idorder_name1李四1001洗衣机1李四1002空调2张三1003手机很明显这 3 条数据对应的业务逻辑是用户id1李四有 2 个订单1001、1002用户id2张三有 1 个订单1003我们的目标不是得到 3 个独立的行数据而是封装成 2 个User对象每个User对象内部包含一个ListOrder集合存储该用户的所有订单。这正是collection标签要解决的核心问题。二、常规的 MyBatis 映射写法要实现上述效果我们首先会在 Mapper XML 中定义对应的resultMap通过collection标签指定集合属性的映射规则!-- 订单对象的映射 --resultMapidOrderResultMaptypecom.example.entity.OrderidpropertyorderIdcolumnorder_id/resultpropertyorderNamecolumnorder_name//resultMap!-- 用户对象的映射包含订单集合 --resultMapidUserWithOrdersResultMaptypecom.example.entity.User!-- 主键必须用id标签指定这是底层去重的关键 --idpropertyidcolumnid/resultpropertyusernamecolumnusername/!-- 集合属性orders对应Order类型 --collectionpropertyordersofTypecom.example.entity.OrderresultMapOrderResultMap//resultMap!-- 关联查询SQL --selectidselectUserWithOrdersresultMapUserWithOrdersResultMapSELECT u.id, u.username, o.order_id, o.order_name FROM user u LEFT JOIN order o ON u.id o.user_id/select对应的 Java 实体类结构如下publicclassUser{privateLongid;privateStringusername;privateListOrderorders;// 一对多集合属性// getter、setter省略}publicclassOrder{privateLongorderId;privateStringorderName;// getter、setter省略}写好这些后调用 Mapper 方法就能直接得到ListUser每个 User 都带着自己的订单集合。但 MyBatis 到底是怎么把 3 行数据变成 2 个 User 对象的这就需要深入底层流程了。三、核心collection 底层封装的完整流程MyBatis 处理collection的核心逻辑本质上是“基于主键的缓存去重 逐行数据填充”。整个过程围绕 “遍历 ResultSet 的每一行数据” 展开我们就以上面的 3 条数据为例一步步拆解第一步处理第 1 行数据id1order_id1001检查主对象缓存MyBatis 会先提取当前行的主键值这里是id1去内部的一个临时缓存中查找是否已经存在主键为 1 的User对象。创建主对象并填充基本属性第一次查询缓存中没有所以创建一个新的User对象将id1、username李四赋值给该对象的对应属性。处理集合属性检查User对象的orders集合是否存在不存在则创建一个空的ArrayList默认实现。创建嵌套对象并加入集合提取当前行的嵌套对象主键order_id1001同样检查缓存嵌套对象也有自己的缓存没有则创建新的Order对象赋值orderId1001、orderName洗衣机然后将这个 Order 对象添加到orders集合中。缓存主对象将创建好的User对象id1放入临时缓存供后续行使用。此时缓存中有 1 个 User 对象其 orders 集合中有 1 个 Order 对象。第二步处理第 2 行数据id1order_id1002检查主对象缓存提取主键id1发现缓存中已经存在对应的 User 对象跳过主对象的创建和基本属性赋值这就是为什么不会重复创建 id1 的 User。直接处理集合属性发现orders集合已经存在不再创建新集合。创建新的嵌套对象提取order_id1002检查嵌套对象缓存没有则创建新的 Order 对象赋值后添加到已有的orders集合中。此时id1 的 User 对象的 orders 集合中已经有 2 个 Order 对象了。第三步处理第 3 行数据id2order_id1003检查主对象缓存提取主键id2缓存中不存在创建新的 User 对象填充id2、username张三。创建新的集合检查orders集合不存在创建新的 ArrayList。创建嵌套对象并加入集合提取order_id1003创建 Order 对象并赋值添加到集合中。缓存新的主对象将 id2 的 User 对象放入缓存。最终结果遍历完所有行数据后MyBatis 将缓存中的所有 User 对象收集起来返回ListUser。最终我们得到的就是1 个 id1 的 Userorders 集合有 2 个元素1 个 id2 的 Userorders 集合有 1 个元素完美符合我们的业务预期。四、关键机制为什么必须指定 id 标签有人可能会问如果我把resultMap中的id标签换成普通的result标签会发生什么答案是会导致主对象重复创建集合数据错乱。这是因为id标签标记的是对象的唯一标识MyBatis 正是通过这个唯一标识来生成 “行 ID”作为临时缓存的 key。如果没有指定idMyBatis 会把当前行的所有字段值拼接起来作为 key这样即使主键相同只要其他字段有一点点差异比如嵌套对象的字段就会被认为是不同的对象从而重复创建主对象。同样的嵌套对象比如上面的 Order也建议指定id标签否则嵌套对象也会出现重复创建的问题。五、使用 collection 的几个重要注意事项必须为主对象和嵌套对象指定主键id 标签这是保证对象不重复创建的核心也是性能优化的关键。SQL 查询必须包含所有映射的字段尤其是主键字段如果查询结果中没有主键值MyBatis 无法进行缓存判断会导致每一行都创建新对象。避免笛卡尔积过大如果一对多的两边数据量都很大关联查询会产生大量的重复数据影响性能。这种情况下建议使用 “分步查询”select属性代替联合查询。集合的默认实现是 ArrayList如果需要使用其他集合类型比如 Set可以通过collection标签的javaType属性指定。六、总结MyBatis 的collection集合封装本质上是一个 “先缓存主对象再逐行填充集合” 的过程。它通过id标签定义的唯一标识来维护一个临时缓存确保相同主键的主对象只会被创建一次后续行数据只会往已有的集合中添加嵌套对象。