场景假设
- 你有一个
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));
详细解释:
-
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,确保下一步查询的用户列表中没有重复数据,提高效率。
-
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是唯一的。
- Key: 是
- 它会根据
经过第 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));
});
详细解释:
pictureVOList.forEach(...): 遍历需要返回给前端的PictureVO列表。Long userId = pictureVo.getUserId();: 从当前PictureVO中获取userId。if (userIdUserListMap.containsKey(userId)): 检查我们在上一步创建的 “查找表”userIdUserListMap中是否包含这个userId。user = userIdUserListMap.get(userId).get(0);:userIdUserListMap.get(userId): 通过userId从Map中获取对应的List<User>。这个操作的时间复杂度是 O(1),非常快。.get(0): 从List中取出第一个元素。如前所述,这个List应该只有一个User对象。
pictureVo.setUser(userService.getUserVo(user));:- 调用
userService.getUserVo(user)将User实体对象转换为UserVO视图对象(这是为了隐藏敏感信息,只返回前端需要的数据)。 - 将转换后的
UserVO设置到PictureVO的user字段中。
- 调用
经过第 2 步,所有 PictureVO 对象都已经填充好了完整的用户信息。
总结
| 步骤 | 操作 | 数据库查询次数 | 目的 |
|---|---|---|---|
| 1.1 | stream().map().collect() | 0 | 从图片列表中提取出所有不重复的 userId。 |
| 1.2 | userService.listByIds().stream().collect() | 1 次 | 批量查询所有用户,并构建一个 userId -> User 的快速查找 Map。 |
| 2 | forEach(...) | 0 | 遍历结果列表,利用 Map 快速填充用户信息。 |
最终效果:将数据库查询次数从 O (N) 降低到了 O (1)(固定的 2 次),极大地提升了系统性能和响应速度。这是处理 “一对多” 数据填充问题的标准和最佳实践。
47

被折叠的 条评论
为什么被折叠?



