TODO 2 · Order.shippingInfoTODO 2 · Order.shippingInfo
两行代码,但选错一行就答不到 N+1 这个考点。Two lines of code. Pick the wrong one and you miss the N+1 point entirely.
这一页有什么On this page9
- 01 这一问在要求什么What this task asks for
- 02 先想再写Think before you write
- 03 两种写法都能过测试,但只有一种答对了Both versions pass the tests, but only one answers the question
- 04 为什么必须显式 ?? nullWhy you must write ?? null explicitly
- 05 完整答案The full answer
- 06 验证合并真的发生了Checking that the calls really were merged
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 独立写出 Order.shippingInfoWrite Order.shippingInfo without help
- 解释为什么必须走 loader 而不是直接调数据源Explain why you must go through the loader instead of calling the data source
- 说清为什么这里要 ?? null 而不是 ?? []Explain why this one needs ?? null and not ?? []
- 知道测试为什么抓不到「绕过 loader」这个错Know why the tests do not catch a solution that skips the loader
TODO 原文点名了 DataLoader。这是全项目唯一明确指定实现手段的一处 —— 说明出题人就是要看你会不会用它。The TODO names DataLoader directly. It is the only place in the project that says how to implement something, which means the exam wants to see whether you can use it.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.jsOrder.shippingInfo
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js这一问在要求什么What this task asks for
TODO 原文:Implement shipping info resolver using DataLoader to prevent N+1 queries。
「using DataLoader」是硬性指定。四个 TODO 里只有这一个规定了实现手段 —— 别的都只说「实现 + 错误处理」。 这说明 N+1 就是这一问的全部考点。
schema 那边:shippingInfo: ShippingInfo,可空。所以「这个订单没有物流信息」 返回 null 是正确行为。
The TODO, word for word: Implement shipping info resolver using DataLoader to prevent N+1 queries.
“using DataLoader” is a hard requirement. Of the four TODOs this is the only one that dictates how to implement it — the others just say “implement it, handle errors”. Which tells you N+1 is the whole point of this question.
From the schema: shippingInfo: ShippingInfo, nullable. So returning null for “this order has no shipping info” is the correct behaviour.
先想再写Think before you write
两种写法都能过测试,但只有一种答对了Both versions pass the tests, but only one answers the question
这是本项目最典型的「测试抓不到」的地方。This is the clearest case in this project of something the tests cannot catch.
对比这两种写法:
测试为什么抓不到区别?因为测试是直接调 resolver 函数的:
它一次只调一个 order,所以「有没有合并」根本体现不出来。 两种写法都返回正确的物流信息,两条测试都过。
那怎么知道自己写对了?看日志。走 loader 的写法会打印[DataLoader] Batching N shipping info requests,N 个订单只打一行。 直接调数据源的写法一行都不打。
所以验证方式是:用真实的 GraphQL 查询(不是单元测试)查一个有多个订单的用户, 然后数日志行数。
Compare the two versions:
Why can the tests not tell them apart? Because the tests call the resolver function directly:
They pass one order at a time, so “did anything get batched” never shows up. Both versions return the right shipping info, and both tests go green.
So how do you know you got it right? Read the logs. The loader version prints [DataLoader] Batching N shipping info requests, and N orders produce a single line. The version that calls the data source directly prints nothing at all.
Which makes the check: run a real GraphQL query (not a unit test) against a user who has several orders, then count the log lines.
graphql-federation-practice/node-subgraph/__tests__/resolvers.test.js为什么必须显式 ?? nullWhy you must write ?? null explicitly
第二个测试用的是 toBeNull(),不是 toBeUndefined()。The second test uses toBeNull(), not toBeUndefined().
ShippingDataSource.getShippingInfo('order-999')的实现是查一个对象字面量,找不到时return shippingData[orderId] || null —— 它已经返回 null 了。
所以严格说 ?? null 是多余的。但还是要写,理由和上一节一样:
- 测试断言的是
toBeNull()。undefined不等于null, 这条断言会失败。 - DataLoader 的 batch 函数如果返回的数组某个位置是
undefined(比如未来数据源换实现),load()就会 resolve 成 undefined。 显式兜底能挡住这种情况。
习惯:可空字段显式 ?? null, 非空列表显式 ?? []。两行都写,不依赖下游实现细节。
ShippingDataSource.getShippingInfo('order-999') looks up an object literal and, on a miss, does return shippingData[orderId] || null — so it already returns null.
Strictly speaking that makes ?? null redundant. Write it anyway, for the same reasons as the last lesson:
- The test asserts
toBeNull().undefinedis notnull, so that assertion would fail. - If a DataLoader batch function ever returns
undefinedat some position (say the data source changes implementation later),load()resolves to undefined. An explicit fallback blocks that.
Make it a habit: nullable fields get an explicit ?? null, non-null lists get an explicit ?? []. Write both lines and stop depending on what the layer below happens to do.
完整答案The full answer
加上 TODO 没明说但一致的错误处理(和另外三个 resolver 保持同样的结构):
With the error handling the TODO does not spell out but consistency wants — the same structure as the other three resolvers:
验证合并真的发生了Checking that the calls really were merged
用真实查询(不是单元测试),查一个有两个订单的用户:
关键是数那行 [DataLoader] Batching。两个订单只出现一行且 N=2, 说明合并生效。如果出现两行 N=1, 说明两次 load() 不在同一个 tick 里 (通常是因为你在 resolver 里加了不必要的 await 把它们错开了)。 如果一行都没有,说明你根本没走 loader。
Use a real query, not a unit test, against a user who has two orders:
The thing to count is that [DataLoader] Batching line. Two orders should produce one line with N=2, which means batching worked. Two lines with N=1 mean the two load() calls did not land in the same tick (usually because an unnecessary await in your resolver pulled them apart). No line at all means you never went through the loader.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
三个空。第 1 个决定你答不答得到 N+1 考点, 第 3 个决定第二条测试过不过。
Three blanks. The first decides whether you answer the N+1 question at all; the third decides whether the second test passes.
dataSources.shippingDataSource.getShippingInfo(parent.id) 能让两条测试都通过。为什么还是错的?
dataSources.shippingDataSource.getShippingInfo(parent.id) makes both tests pass. So why is it still wrong?
初学者常见的几种写法错误Mistakes beginners actually make
下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.
await 会把各个 order 的load() 推到不同的 tick, 于是变成 N 次 batch,每次 N=1。症状:日志出现多行
Batching 1 ... requests。合并没了,但测试还是过的。DataLoader merges calls by collecting every load() made in the same tick. An extra await in between pushes each order's load() into a different tick, so you get N batches of one instead of one batch of N.What you see: several
Batching 1 ... requests lines in the log.The merging is gone, but the tests still pass.orderLoader 取的是 order,不是物流信息。 这会返回那个 order 本身, 然后 GraphQL 试图把它当 ShippingInfo 用 ——status 字段恰好都有(值是 SHIPPED 而不是 IN_TRANSIT),trackingNumber 是 undefined。第一条测试会挂在
toHaveProperty('trackingNumber') 上。context 里有两个 loader,看清名字。orderLoader returns an order, not shipping information. You get the order object back, and GraphQL then reads it as a ShippingInfo: status happens to exist (its value is SHIPPED, not IN_TRANSIT) and trackingNumber is undefined.The first test fails on
toHaveProperty('trackingNumber').There are two loaders in context. Read the names carefully.换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- TODO 点名了 DataLoader —— 这是四个 TODO 里唯一指定实现手段的,考点就在这。The TODO names DataLoader. It is the only one of the four TODOs that says how to implement it, and that is the point being tested.
- 走 loaders.shippingInfoLoader.load(parent.id),不是 dataSources.shippingDataSource。Use loaders.shippingInfoLoader.load(parent.id), not dataSources.shippingDataSource.
- 两种写法都能过测试,因为测试一次只调一个 order —— 抓不到合并与否。Both versions pass, because each test calls only one order, so the tests cannot tell whether the calls were merged.
- 可空字段要显式 ?? null,因为测试断言的是 toBeNull(),undefined 会挂。A nullable field needs an explicit ?? null, because the test asserts toBeNull() and undefined fails it.
- resolver 里别插多余的 await,会把 load() 推到不同 tick,合并失效。Do not add an extra await in the resolver. It pushes load() into a different tick and the merging stops working.