发布于2026-07-11 阅读(0)
扫一扫,手机访问
如何正确地将扁平化的JDBC结果集,映射成一个包含完整上下级关系的 Employee 集合?这看起来是个小问题,但实操中却藏着不少坑。本文将介绍一套可靠的两阶段映射法,彻底告别数据顺序带来的不确定性。
在实际的业务开发中,处理员工-经理这种自关联关系的数据,是一个很常见的场景。但不少人在用JDBC映射时,会习惯性地在遍历ResultSet的过程中,直接就把Manager的引用给填上。这么做,说白了,就是赌数据库返回数据的顺序一定是经理记录在前、下属记录在后。
问题是,现实世界里的SQL查询,如果不加ORDER BY,其返回顺序从来都不是一个可靠的约定。不同的数据库、不同的执行计划,甚至同一条SQL在不同时间跑,结果顺序都可能不一样。一旦经理记录晚于下属出现,那就只能得到一个manager == null的静默错误,排查起来相当头疼。
解决思路其实很简单:把“创建对象”和“建立关系”这两个步骤拆开来做,各司其职,互不干扰。
如果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是一个不可变对象,比如用了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结果集顺序的任何幻象。编写健壮的数据访问代码,确定性远比巧合更重要。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8