一、问题场景
在列表查询类需求中,经常需要统计每个主单下包含多少种不同的明细项。例如:
- 每个订单包含多少种不同的商品
- 每个采购单包含多少种不同的物料
- 每个用户拥有多少种不同的标签
直接做法是对每个主单单独查询明细并在内存中去重,但当主单数量较多时会产生大量 SQL,性能不佳。本文介绍一种批量查询 + Set 去重 + 复合键分组的高效写法。
二、核心思路
- 批量查询:一次 SQL 查出当前页所有主单的明细,避免循环查库
- 复合键去重:将判断"是否同种"的多个字段拼接成字符串作为唯一标识
- Set 自动去重 :利用
Set不存储重复元素的特性,size()即为种类数 - Map 分组 :按主单 ID 分组,每个主单对应一个
Set
三、代码实现
1. 批量查询并分组统计
java
/**
* 批量统计当前页每个主单的明细种类数
*
* @param records 当前页主单列表
*/
private void fillDetailKindCount(List<OrderListVo> records) {
Set<Long> orderIds = records.stream()
.map(OrderListVo::getOrderId)
.collect(Collectors.toSet());
// 一次查询所有主单的明细,避免循环查库
List<OrderItem> items = itemMapper.selectList(
Wrappers.<OrderItem>lambdaQuery()
.select(OrderItem::getOrderItemId,
OrderItem::getOrderId,
OrderItem::getSource,
OrderItem::getSourceId,
OrderItem::getName,
OrderItem::getSpec,
OrderItem::getUnit)
.in(OrderItem::getOrderId, orderIds));
// Map<主单ID, Set<去重标识>>
Map<Long, Set<String>> keyMap = new HashMap<>();
for (OrderItem item : items) {
String detailKey = buildDetailKey(item);
keyMap.computeIfAbsent(item.getOrderId(), k -> new HashSet<>()).add(detailKey);
}
// 回填种类数
for (OrderListVo record : records) {
Set<String> keys = keyMap.get(record.getOrderId());
record.setDetailKindCount(keys == null ? 0 : keys.size());
}
}
2. 生成复合去重键
java
/**
* 生成明细种类统计使用的去重标识
*
* @param item 明细项
* @return 去重标识字符串
*/
private String buildDetailKey(OrderItem item) {
// 自定义明细:按名称、规格、单位拼接判断
if (SourceEnum.CUSTOM.matches(item.getSource())) {
return String.join("|", SourceEnum.CUSTOM.getCode(),
StringUtils.trimToEmpty(item.getName()),
StringUtils.trimToEmpty(item.getSpec()),
StringUtils.trimToEmpty(item.getUnit()));
}
// 外部系统明细:按来源 + 来源ID判断,更准确
String sourceKey = item.getSourceId() == null
? "ITEM_" + item.getOrderItemId()
: String.valueOf(item.getSourceId());
return item.getSource() + "|" + sourceKey;
}
四、关键技巧解析
技巧 1:Set 去重统计种类数
凡是需要"统计不重复元素个数"的场景,优先使用 Set。Set 的 add() 方法自动忽略重复元素,最终 size() 即为去重后的数量,无需手动编写重复判断逻辑。
java
Set<String> set = new HashSet<>();
set.add("A");
set.add("A"); // 重复,不会存储
set.add("B");
System.out.println(set.size()); // 输出 2
技巧 2:字符串拼接构建复合键
当一个对象由多个字段共同决定"是否相同"时,可将这些字段用分隔符拼接为字符串作为唯一标识:
- 无需重写
equals和hashCode - 无需新建专门的 Key 类
- 直观易调试
注意 :分隔符必须选用字段值中不会出现的字符,否则会产生歧义。例如字段值若可能包含 |,则应改用 _、# 等其他分隔符。
技巧 3:computeIfAbsent 一行完成分组初始化
Map.computeIfAbsent(key, mappingFunction) 的作用是:若 key 不存在,则通过 mappingFunction 计算一个值并存入,最后返回该值。
java
// 传统写法
Set<String> set = map.get(id);
if (set == null) {
set = new HashSet<>();
map.put(id, set);
}
set.add(key);
// computeIfAbsent 写法
map.computeIfAbsent(id, k -> new HashSet<>()).add(key);
技巧 4:批量 IN 查询替代循环查询
循环中执行数据库查询是常见的性能问题。假设当前页有 10 条主单,循环查询会产生 10 次 SQL,而批量 IN 查询只需 1 次,显著减少数据库往返开销。
五、方案优势与局限性
| 维度 | 说明 |
|---|---|
| 性能 | 批量查询减少 SQL 次数,内存分组效率高 |
| 可读性 | 利用 Set 和 Map 的标准 API,意图清晰 |
| 灵活性 | 去重规则可按数据来源差异化设计 |
| 局限性 | 当单页主单数量极大时,IN 列表过长可能影响 SQL 执行计划,需控制分页大小 |
| 适用场景 | 列表页的关联统计、种类数统计等 |
六、小结
本文介绍的 Set 去重 + 复合键 + computeIfAbsent 分组是一种通用的统计模式,适用于各类"按主单统计明细种类"的场景。核心在于善用 Java 集合的内置能力,用最少的代码解决问题,同时兼顾性能。