结果集映射
结果映射既要把一行列值填进 Java Bean,又要在多行结果中合并同一个父对象、创建子集合,并避免嵌套查询无限递归。
先给答案:结果映射是在“数据库列名”和“对象属性图”之间补一层可解释的契约
Section titled “先给答案:结果映射是在“数据库列名”和“对象属性图”之间补一层可解释的契约”数据库返回的是行和列,Java 代码需要的是对象、嵌套对象或集合。MyBatis 不把这两种结构硬编码成一一对应,而是先构建 ResultMap,再根据列值、属性类型和嵌套映射规则逐步装配对象。
映射越灵活,运行时决策越多:自动映射可能带来便利,也可能把列名拼写错误变成静默的 null;嵌套查询可以延迟加载,却会把一次查询放大成 N+1。阅读结果映射时不能只看“字段有没有填上”,还要追踪对象何时创建、嵌套查询何时触发、列为空时选择了哪条分支。
ResultSet -> ResultSetWrapper -> DefaultResultSetHandler |- ResultMap / Discriminator |- createResultObject |- applyPropertyMappings `- nestedResultObjects + CacheKeyDefaultResultSetHandler#handleResultSets遍历多个 result set,并按 result maps 处理(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:211-250)。handleRowValues根据是否有 nested result map 选择简单或复杂路径(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:329-357)。getRowValue创建结果对象,再处理属性映射和 lazy loader(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:461-482)。createResultObject根据 constructor mappings、type handler 和默认构造器选择实例化路径(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:719-820)。applyPropertyMappings先处理显式ResultMapping,再交给自动映射补齐(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:545-675)。createAutomaticMappings根据列名和前缀推导 property,并缓存自动映射(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:644-719)。nestedResultObjects按CacheKey暂存对象,避免一对多 join 每行创建父对象(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:81-101)。createRowKey将 result map id 与标识列值组合成 row key(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:1490-1525)。applyNestedResultMappings生成 combined key,复用或创建子对象(mybatis-3/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java:1150-1338)。- nested select 可创建
ResultLoader,在属性访问时再加载(mybatis-3/src/main/java/org/apache/ibatis/executor/loader/ResultLoader.java:59-91)。
为什么嵌套结果需要 CacheKey
Section titled “为什么嵌套结果需要 CacheKey”替代方案:按 ResultSet 行直接创建完整对象树。
为什么不行:一对多 join 会重复返回父列,直接创建会产生重复父对象和重复子集合。
证据:applyNestedResultMappings 使用 combinedKey 查询 nestedResultObjects,并把新对象放回同一 Map(DefaultResultSetHandler.java:1297-1338)。
| 场景 | 现象 | 原因 | 规避 |
|---|---|---|---|
| join 没有稳定 id | 父对象重复 | row key 无法识别同一父对象 | 为 nested result map 配置 <id> |
| 下划线与驼峰不一致 | 属性为 null | 自动映射规则未转换 | 开启驼峰或显式 result |
| lazy loading 关闭 | 关联 SQL 立即执行 | nested select 按配置加载 | 统一 lazyLoading/fetchType |
| 一对多结果过大 | 内存和 GC 飙升 | 结果树在内存合并 | 分页、游标或拆分查询 |
“父 key + 子 key + pending object”是把扁平事件流还原为树结构的通用算法,适合 CSV join、消息聚合和批量导入。
面试锚点
- MyBatis 如何处理一对多 join 的父对象重复?
- 自动映射和显式
ResultMap的优先级是什么?- 嵌套查询为什么可能造成 N+1?