如何在Java中将一对多查询结果集映射为对象
嗨,这个问题我太熟了——用JOIN查出来的嵌套关联结果集,手动转对象很容易写出一堆重复代码,确实得找个优雅的路子。我给你分享几个常用的方案,看你场景选:
方案1:手动映射+缓存(轻量无依赖)
如果不想引入额外框架,这是最直接的优化方式。核心思路是用Map缓存已经创建好的父/子对象,避免重复实例化——毕竟JOIN结果里会有大量重复的父记录行。
举个例子,假设你的对象模型是:
class Parent { private Long id; private String name; private List<Child> children = new ArrayList<>(); // getter/setter } class Child { private Long id; private String name; private List<Grandchild> grandchildren = new ArrayList<>(); // getter/setter } class Grandchild { private Long id; private String name; // getter/setter }
然后遍历ResultSet的代码可以这么写:
Map<Long, Parent> parentMap = new HashMap<>(); Map<Long, Child> childMap = new HashMap<>(); while (rs.next()) { // 处理父对象 Long parentId = rs.getLong("parent_id"); Parent parent = parentMap.get(parentId); if (parent == null) { parent = new Parent(); parent.setId(parentId); parent.setName(rs.getString("parent_name")); parentMap.put(parentId, parent); } // 处理子对象 Long childId = rs.getLong("child_id"); if (childId != null && !rs.wasNull()) { // 避免子记录为空的情况 Child child = childMap.get(childId); if (child == null) { child = new Child(); child.setId(childId); child.setName(rs.getString("child_name")); parent.getChildren().add(child); childMap.put(childId, child); } // 处理孙对象 Long grandchildId = rs.getLong("grandchild_id"); if (grandchildId != null && !rs.wasNull()) { Grandchild grandchild = new Grandchild(); grandchild.setId(grandchildId); grandchild.setName(rs.getString("grandchild_name")); child.getGrandchildren().add(grandchild); } } } // 最终得到去重后的父对象列表 List<Parent> result = new ArrayList<>(parentMap.values());
这个方法的关键是用Map缓存已经创建的对象,跳过重复的父/子记录,只处理新的行,避免对象重复创建和重复添加。
方案2:用MyBatis的ResultMap自动映射(优雅省心)
如果你的项目已经在用MyBatis,那这绝对是最优雅的方案。只需要在Mapper里定义好嵌套的ResultMap,写原生SQL就能自动帮你把ResultSet转成嵌套对象。
比如定义ResultMap:
<resultMap id="grandchildMap" type="Grandchild"> <id column="grandchild_id" property="id"/> <result column="grandchild_name" property="name"/> </resultMap> <resultMap id="childMap" type="Child"> <id column="child_id" property="id"/> <result column="child_name" property="name"/> <collection property="grandchildren" resultMap="grandchildMap"/> </resultMap> <resultMap id="parentMap" type="Parent"> <id column="parent_id" property="id"/> <result column="parent_name" property="name"/> <collection property="children" resultMap="childMap"/> </resultMap>
然后Mapper接口里写原生SQL的方法:
@Select("SELECT p.id parent_id, p.name parent_name, " + "c.id child_id, c.name child_name, " + "gc.id grandchild_id, gc.name grandchild_name " + "FROM parent p " + "LEFT JOIN child c ON p.id = c.parent_id " + "LEFT JOIN grandchild gc ON c.id = gc.child_id " + "WHERE p.id = #{parentId}") @ResultMap("parentMap") Parent getParentWithNestedChildren(Long parentId);
MyBatis会自动处理JOIN结果里的重复行,帮你把嵌套的子对象、孙对象都组装好,完全不用手动处理缓存和遍历逻辑,代码非常干净。
方案3:Spring JDBC的自定义RowMapper
如果用的是Spring JDBC,也可以自定义RowMapper来处理嵌套映射,思路和手动映射类似,但可以整合Spring的BeanPropertyRowMapper来简化属性赋值。
比如:
public class ParentRowMapper implements RowMapper<Parent> { private Map<Long, Parent> parentCache = new HashMap<>(); private Map<Long, Child> childCache = new HashMap<>(); @Override public Parent mapRow(ResultSet rs, int rowNum) throws SQLException { Long parentId = rs.getLong("parent_id"); Parent parent = parentCache.get(parentId); if (parent == null) { parent = new Parent(); parent.setId(parentId); parent.setName(rs.getString("parent_name")); parentCache.put(parentId, parent); } Long childId = rs.getLong("child_id"); if (!rs.wasNull()) { Child child = childCache.get(childId); if (child == null) { child = new Child(); child.setId(childId); child.setName(rs.getString("child_name")); parent.getChildren().add(child); childCache.put(childId, child); } Long grandchildId = rs.getLong("grandchild_id"); if (!rs.wasNull()) { Grandchild grandchild = new Grandchild(); grandchild.setId(grandchildId); grandchild.setName(rs.getString("grandchild_name")); child.getGrandchildren().add(grandchild); } } return parent; } // 最后要去重,因为返回的列表里会有重复的父对象 public List<Parent> getUniqueParents(List<Parent> mappedRows) { return new ArrayList<>(new LinkedHashSet<>(mappedRows)); } }
使用的时候:
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); ParentRowMapper rowMapper = new ParentRowMapper(); List<Parent> rawResult = jdbcTemplate.query(sql, rowMapper); List<Parent> finalResult = rowMapper.getUniqueParents(rawResult);
总结
- 如果项目无ORM依赖,选手动映射+缓存,轻量可控;
- 如果已经用MyBatis,直接用嵌套ResultMap,最优雅省心;
- 如果用Spring JDBC,自定义RowMapper整合缓存逻辑即可。
内容的提问来源于stack exchange,提问作者johnny-b-goode
相关产品推荐
相关产品推荐

