Skip to content

使用 Fetch 解决 "N 1 查询问题"

Lin ZiHao edited this page Jan 19, 2023 · 1 revision

使用 Fetch 解决 "N + 1 查询问题"

N+1 问题简单描述, 在循环中每次都执行数据库查询. 例如

Map<Long, String> usernameMap = new HashMap();

List<Long> userIds = getAllUserIds();
for (Long userId : userIds) {
  User user = getUserById(userId);
  usernameMap.put(userId, user.getName());
}

return usernameMap;

假设 getAllUserIds 是一次数据库查询, getUsernameById 也是一次数据库查询, 一共有 N 个 userId, 总共会执行 N + 1 次查询.

解决 N + 1 查询的常见方式是手动优化查询代码, 例如

Map<Long, String> usernameMap = new HashMap();

List<Long> userIds = getAllUserIds();
List<User> users = getUserByIds(userIds);
for (User user : users) {
  usernameMap.put(user.getId(), user.getName());
}

return usernameMap;

假设 getAllUserIds 是一次数据库查询, getUserByIds 是一次批量数据库查询, 总共会执行 N + 1 次查询.

这种手动优化的解决方案有两个缺点

  1. 查询优化逻辑和业务逻辑混杂在一起
  2. 没有组合性, 无法复用代码

这是个简单的例子, 有经验的开发人员都能马上想起自己做过的类似优化, 当有多个关联数据源, 并且有多层的层级关系时, 手动优化的缺点就会暴露出来.
想象下面这些例子
"查询一个班级中所有的学生信息", 需要先查询班级数据, 批量查询学生数据, 再写如何把班级和学生数据对应组合起来.
"查询一个年级中, 所有班级的所有学生信息", 这里新增了一个层级, 需要先查询年级数据. 这时候无法复用上面的代码, 复用代码需要循环年级中的每个班级, 然后调用"查询一个班级中所有的学生信息". 可以发现, N + 1 问题又回来了. 因此需要再次手动优化, 先查询年级数据, 批量查询班级数据, 批量查询学生数据, 再写如何把三份数据对应组合起来.

如果使用 Fetch, 就不会遇到上述的问题, 代码是这样的.

以下代码会用涉及到 Fetch 一些还未介绍的概念和 API, 读者不需要完全理解每一行代码, 看大致的意思就行.

Map<Long, String> usernameMap = new HashMap();

Fetch<List<Long>> userIds = getAllUserIds();     // (1)
Fetch.traverse(userIds, userId -> {                  // (2)
  Fetch<User> user = getUserById(userId);        // (3)
  user.map(u -> usernameMap.put(userId, user.getName())); // (4)
}).resolve(...)                                  // (5)

return usernameMap;

(1). getAllUserIds 返回的不再是 List, 而是再包了一层 Fetch<...>. 类似 Future<...>, 使用 Fetch 包住表示暂时没有求值. 具体看 [[]].
(2). Fetch.traverse 类似 for 循环, 第一个参数是一个 Fetch<List>, 第二个参数是个函数 A -> {...}. traverse 表示循环第一个参数 List, 并对里面的每个元素, 应用后面的函数.
(3). getUserById 和 (1) 类似, 返回值从 User 包了一层变成 Fetch.
(4). Fetch 的 map 方法类似 Future 的 map 方法, 表示当求值完成之后, 应用后面的函数.
(5). 在调用 resolve 方法之前, 没有任何请求发出. 调用 resolve 时, Fetch 库会自动分析并且优化, 发出优化后的查询请求.

"查询一个班级中所有的学生信息" 是这样的

Fetch<List<Long>> studentIds = getStudentIdsByClassId(classId);
Fetch.traverse(studentIds, studentId -> {
  Fetch<Student> student = getStudentById(studentId);
  student.map(s -> {
    // ... 任意业务逻辑
  })
})

"查询一个年级中, 所有班级的所有学生信息" 是这样的

Fetch<List<Long>> classIds = getClassIdsByGradeId(gradeId);
Fetch.traverse(classIds, classId -> {
  // "查询一个班级中所有的学生信息" 可以封装成函数 getStudentsByClass(classId), 这里直接复用
  getStudentsByClass(classId);
})

从以上的伪代码可以看出, Fetch 解决了 N + 1 问题, 并且提供了可复用性, 优化了性能, 使得用户代码可以专注于业务逻辑.

Clone this wiki locally