目标
N+1 在 schema 里看不见,在数据库日志里却一目了然。改代码前先数每个请求的查询数:解析 N 个实体的列表并读计数器,读到 N+1 就说明每个嵌套字段都在独立取数。DataLoader 的解法是收集同一 tick 内请求的所有 key,发出单条 IN 查询;但只有计数器能证明修复真正落地。
步骤要点
- 在解析器上下文里挂一个请求级查询计数器,SQL 层每次执行自增,跑一次 50 条订单的查询确认日志显示 51 次。
- 按主键写 DataLoader 批处理函数:用 in 条件一次取回全部行,按 id 建 Map,再按输入顺序映射返回(缺失补 null)。
- 把嵌套字段解析器改为 loaders.customer.load(order.customerId),让同响应内的所有 load 排队合并成一次数据库往返。
- 在 context 工厂里为每个请求新建 loader 实例,避免跨请求批处理与缓存串号。
- 重放同一 GraphQL 查询:计数器应从 51 落到恰好 2,并把该断言固化进测试套件。
排错提示
查询数几乎没降,通常是 loader 建在了模块顶层导致跨请求交错与缓存泄漏,应改为每请求在 context 中新建;列表里 null 项消失说明批函数返回数组与输入 id 顺序错位,必须按请求顺序映射;需要特定排序时 DataLoader 只保输入顺序,应在 load 之后于解析器内再排序。
适用边界
只有同一响应内解析大量实体时批处理才有收益,单实体查询无改善;loader 缓存按 key 存在于请求内,同请求内的变更可能读到旧值,需显式清理缓存。
代码示例
const customerLoader = new DataLoader(async (ids) => {
const rows = await db.customers.findMany({ where: { id: { in: ids } } });
const byId = new Map(rows.map((r) => [r.id, r]));
return ids.map((id) => byId.get(id) ?? null);
});