如何正确映射数据库结果集为包含完整管理层级关系的 Employee 集合
两阶段映射法将创建对象与建立关系分离,第一阶段遍历结果集构建Employee实例并缓存,第二阶段根据映射关系注入经理引用。该方法不依赖SQL顺序,支持可变对象的setter注入与不可变对象的递归重建,可实现任意深度管理链的正确映射。
如何正确地将扁平化的JDBC结果集,映射成一个包含完整上下级关系的 Employee 集合?这看起来是个小问题,但实操中却藏着不少坑。本文将介绍一套可靠的两阶段映射法,彻底告别数据顺序带来的不确定性。
在实际的业务开发中,处理员工-经理这种自关联关系的数据,是一个很常见的场景。但不少人在用JDBC映射时,会习惯性地在遍历ResultSet的过程中,直接就把Manager的引用给填上。这么做,说白了,就是赌数据库返回数据的顺序一定是经理记录在前、下属记录在后。
问题是,现实世界里的SQL查询,如果不加ORDER BY,其返回顺序从来都不是一个可靠的约定。不同的数据库、不同的执行计划,甚至同一条SQL在不同时间跑,结果顺序都可能不一样。一旦经理记录晚于下属出现,那就只能得到一个manager == null的静默错误,排查起来相当头疼。
✅ 正确方案:两阶段映射法
解决思路其实很简单:把“创建对象”和“建立关系”这两个步骤拆开来做,各司其职,互不干扰。
- 第一阶段(遍历 + 构建):单遍扫描ResultSet,只干两件事。第一,根据每行数据创建Employee实例,此时其manager字段先留空。第二,把这些实例按ID缓存到一个Map里,同时再用另一个Map记录下“谁的经理是谁”这个映射关系。
- 第二阶段(关系注入):遍历那张关系映射表,从缓存中把对应的员工和经理都找出来,然后通过setter方法,或者用新的构造器,完成引用注入。
✅ 场景一:Employee 类支持 setter(简单场景的首选)
如果Employee类允许修改manager属性,那事情就简单多了。具体代码如下,逻辑很清晰:
public SetMapper> employeesSetMapper() { return resultSet -> { Set employees = new HashSet<>(); Map employeeMap = new HashMap<>(); Map employeeToManagerMap = new HashMap<>(); while (resultSet.next()) { BigInteger id = BigInteger.valueOf(resultSet.getInt(1)); FullName fullName = new FullName( resultSet.getString("firstName"), resultSet.getString("lastName"), resultSet.getString("middleName") ); Position position = Position.valueOf(resultSet.getString("position")); LocalDate hired = resultSet.getDate(7).toLocalDate(); BigDecimal salary = resultSet.getBigDecimal("salary"); BigInteger managerId = BigInteger.valueOf(resultSet.getInt(6)); // 注意:这里需要根据业务逻辑过滤掉无效的managerId,比如0或NULL if (managerId != null && !managerId.equals(BigInteger.ZERO)) { employeeToManagerMap.put(id, managerId); } Employee employee = new Employee(id, fullName, position, hired, salary, null); employees.add(employee); employeeMap.put(id, employee); } // 第二阶段:批量注入 manager 引用 employeeToManagerMap.forEach((employeeId, managerId) -> { Employee employee = employeeMap.get(employeeId); Employee manager = employeeMap.get(managerId); if (employee != null && manager != null) { employee.setManager(manager); } }); return employees; };}
⚠️ 几点说明:
- 这里假设
Employee.setManager()是公开的;- 对于managerId的过滤校验,一定要结合实际业务规则,防止把占位符当成真实ID;
- 这套方案完全不依赖
ORDER BY,结果集无论什么顺序都能正确处理。
✅ 场景二:Employee 类不可变(无 setter,仅构造器)
如果Employee是一个不可变对象,比如用了Ja va 14的record,或者所有字段都是final的,那又该怎么办?这时候就需要用递归重建的策略了,确保每一层级的manager都能被正确地注入,包括嵌套的管理链:
public SetMapper> employeesSetMapper() { return resultSet -> { Map employeeMap = new HashMap<>(); Map employeeToManagerMap = new HashMap<>(); // 阶段一:仅构建基础 Employee(manager = null) while (resultSet.next()) { BigInteger id = BigInteger.valueOf(resultSet.getInt(1)); FullName fullName = new FullName( resultSet.getString("firstName"), resultSet.getString("lastName"), resultSet.getString("middleName") ); Position position = Position.valueOf(resultSet.getString("position")); LocalDate hired = resultSet.getDate(7).toLocalDate(); BigDecimal salary = resultSet.getBigDecimal("salary"); BigInteger managerId = BigInteger.valueOf(resultSet.getInt(6)); if (managerId != null && !managerId.equals(BigInteger.ZERO)) { employeeToManagerMap.put(id, managerId); } Employee employee = new Employee(id, fullName, position, hired, salary, null); employeeMap.put(id, employee); } // 阶段二:递归构建含完整 manager 链的 Employee Map enrichedMap = new HashMap<>(); for (BigInteger id : employeeMap.keySet()) { enrichedMap.put(id, getEmployeeWithManager(id, employeeMap, employeeToManagerMap)); } return new HashSet<>(enrichedMap.values()); };}private static Employee getEmployeeWithManager( BigInteger employeeId, Map employeeMap, Map employeeToManagerMap) { Employee base = employeeMap.get(employeeId); BigInteger managerId = employeeToManagerMap.get(employeeId); if (managerId == null || !employeeMap.containsKey(managerId)) { return base; // 无经理或经理不存在 } Employee manager = getEmployeeWithManager(managerId, employeeMap, employeeToManagerMap); return new Employee( base.getId(), base.getFullName(), base.getPosition(), base.getHireDate(), base.getSalary(), manager );}
? 这个方案的好处很明显:
- 它能支持任意深度的管理链,从CEO到普通员工,一路串下来没问题;
- 由于
employeeMap在递归前已完整构建,这天然就能避免循环引用;- 完全符合函数式编程的理念,保持了对象的不变性。
✅ 总结
下面这张表格,可以帮你快速了解不同方案的特点:
| 方案 | 适用条件 | 是否依赖顺序 | 可扩展性 |
|---|---|---|---|
| 单次遍历(原始) | 经理记录严格前置 | ✅ 强依赖 | ❌ 易崩坏 |
| 两阶段 + setter | Employee 可变 | ❌ 无关 | ✅ 简洁高效 |
| 递归重建 | Employee 不可变 | ❌ 无关 | ✅ 支持深层嵌套 |
说白了,不管选择哪种方式,核心原则都是一致的:彻底放弃对SQL结果集顺序的任何幻象。编写健壮的数据访问代码,确定性远比巧合更重要。
Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。
Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。
Photoshop 2026 是 Adobe 推出的专业图像处理与视觉设计软件,支持 Windows、macOS 和 iPad 等平台,广泛应用于摄影修图、电商设计、平面海报、数字绘画及视觉合成等创作场景。
Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。















