Java数据填充(Data Enrichment) 场景, “1 + N 查询优化”。

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.7k人参与

场景假设

  • 你有一个 Picture 列表,每个 Picture 对象都包含一个 userId,但没有完整的用户信息(如用户名、头像等)。
  • 你需要将这个 Picture 列表转换为 PictureVO 列表,其中每个 PictureVO 需要包含发布者的完整 User 信息。
  • userService 提供了通过用户 ID 列表批量查询用户的方法 listByIds(Collection<ID> ids)。

低效的 “反模式”

在解释这段段代码之前,我们先看一下错误的做法,这样能更好地理解优化的价值。

如果没有优化,开发者可能会这样写:

// 1. 收集所有不重复的 userId
Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());

// 2. 一次性批量查询所有用户,并按 userId 分组
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
        .collect(Collectors.groupingBy(User::getId));

问题:如果 pictureVOList 有 100 个元素,这段代码会发起 1 次 查询获取图片列表,然后再发起 100 次 查询来获取每个图片对应的用户,总共 101 次 数据库交互。这会给数据库带来巨大压力,导致系统性能急剧下降。

高效的 “正模式”:

第 1 步:准备数据(发起 2 次数据库查询)
// 1. 收集所有不重复的 userId
Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());

// 2. 一次性批量查询所有用户,并按 userId 分组
Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()
        .collect(Collectors.groupingBy(User::getId));

详细解释:

  1. Set<Long> userIdSet = ...

    • pictureList.stream(): 将 pictureList 转换为一个流(Stream),以便进行后续的链式操作。
    • .map(Picture::getUserId): 这是一个映射操作。它会遍历流中的每一个 Picture 对象,并通过方法引用 Picture::getUserId 提取出 userId。执行完这一步,流中的元素从 Picture 对象变成了 Long 类型的 userId
    • .collect(Collectors.toSet()): 这是一个收集操作。它将流中的所有 userId 收集到一个 Set 集合中。
    • 为什么用 Set Set 集合的特性是元素唯一。这样做可以自动去除 pictureList 中重复的 userId,确保下一步查询的用户列表中没有重复数据,提高效率。
  2. Map<Long, List<User>> userIdUserListMap = ...

    • userService.listByIds(userIdSet): 这是关键的优化点。它调用 userService 的批量查询方法,一次性将上一步收集到的所有 userId 对应的 User 对象从数据库中查询出来,返回一个 List<User>。这只发起了 1 次数据库查询
    • .stream(): 将查询回来的 List<User> 再次转换为流。
    • .collect(Collectors.groupingBy(User::getId)): 这是一个非常强大的分组收集器。
      • 它会根据 User::getId 的返回值(即 userId)对流中的 User 对象进行分组。
      • 最终生成一个 Map,其中:
        • Key: 是 userId ( Long 类型)。
        • Value: 是一个 List<User>,包含了所有具有该 userId 的 User 对象。在正常情况下,这个 List 应该永远只有一个元素,因为 userId 是唯一的。

经过第 1 步,我们得到了一个 Map,它像一个 “用户信息查找表”,可以让我们通过 userId 快速定位到对应的 User 对象。

第 2 步:填充数据(内存操作,无数据库查询)
// 2. 填充信息
pictureVOList.forEach(pictureVo -> {
    Long userId = pictureVo.getUserId();
    User user = null;
    // 使用 Map 进行 O(1) 复杂度的快速查找
    if (userIdUserListMap.containsKey(userId)) {
        user = userIdUserListMap.get(userId).get(0);
    }
    // 将 User 对象转换为 VO 并设置到 PictureVO 中
    pictureVo.setUser(userService.getUserVo(user));
});

详细解释:

  1. pictureVOList.forEach(...): 遍历需要返回给前端的 PictureVO 列表。
  2. Long userId = pictureVo.getUserId();: 从当前 PictureVO 中获取 userId
  3. if (userIdUserListMap.containsKey(userId)): 检查我们在上一步创建的 “查找表” userIdUserListMap 中是否包含这个 userId
  4. user = userIdUserListMap.get(userId).get(0);:
    • userIdUserListMap.get(userId): 通过 userId 从 Map 中获取对应的 List<User>。这个操作的时间复杂度是 O(1),非常快。
    • .get(0): 从 List 中取出第一个元素。如前所述,这个 List 应该只有一个 User 对象。
  5. pictureVo.setUser(userService.getUserVo(user));:
    • 调用 userService.getUserVo(user) 将 User 实体对象转换为 UserVO 视图对象(这是为了隐藏敏感信息,只返回前端需要的数据)。
    • 将转换后的 UserVO 设置到 PictureVO 的 user 字段中。

经过第 2 步,所有 PictureVO 对象都已经填充好了完整的用户信息。

总结

步骤操作数据库查询次数目的
1.1stream().map().collect()0从图片列表中提取出所有不重复的 userId
1.2userService.listByIds().stream().collect()1 次批量查询所有用户,并构建一个 userId -> User 的快速查找 Map
2forEach(...)0遍历结果列表,利用 Map 快速填充用户信息。

最终效果:将数据库查询次数从 O (N) 降低到了 O (1)(固定的 2 次),极大地提升了系统性能和响应速度。这是处理 “一对多” 数据填充问题的标准和最佳实践

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值