TODO 3 & 4 · Query.order 与 Query.ordersTODO 3 & 4 · Query.order and Query.orders
一个用 loader、一个用数据源;一个可空、一个非空。放一起讲差别最清楚。One uses the loader, one uses the data source; one is nullable, one is not. Side by side the difference is clearest.
这一页有什么On this page8
- 01 两个 TODO 的要求对比The two TODOs side by side
- 02 Query.order:用 loader + 找不到要抛错Query.order: use the loader, and throw when nothing is found
- 03 这里最能看出 instanceof 检查为什么必要This is where the instanceof check clearly matters
- 04 Query.orders:用数据源 + 校验参数Query.orders: use the data source and validate the argument
- 05 Query.order 没有测试意味着什么What it means that Query.order has no test
- 练习 · 动手做Practice
- 常见错误Common mistakes
- 迁移模式Transfer
- 独立写出两个 Query resolverWrite both Query resolvers without help
- 说清为什么一个用 loader、一个用数据源Explain why one uses the loader and the other uses the data source
- 写出「找不到」时的结构化错误Write a structured error for the not-found case
- 知道 Query.order 没有测试意味着什么Know what it means that Query.order has no test
Query.orders 有两条测试。Query.order 一条测试都没有,但 TODO 明确要求实现 —— 这种「没测试但有要求」的地方最能区分认真读题的人。Query.orders has two tests. Query.order has none, yet the TODO clearly asks for it. A required part with no test is what separates the people who read the task carefully.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.jsQuery.order 与 Query.ordersQuery.order and Query.orders
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。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两个 TODO 的要求对比The two TODOs side by side
Query.order | Query.orders | |
|---|---|---|
| TODO 原文关键词 | using DataLoader with structured error handling | error handling and correlation ID logging |
| schema 返回类型 | Order(可空) | [Order!]!(双重非空) |
| 数据来源 | loaders.orderLoader.load(id) | dataSources.orderDataSource.getOrdersByUserId(userId) |
| context 解构 | { dataSources, loaders, correlationId } | { dataSources, correlationId }(没有 loaders) |
| 找不到时 | 抛 ORDER_NOT_FOUND | 返回 [] |
| 测试 | ❌ 一条都没有 | ✅ 2 条 |
注意 Query.orders 的签名里没有loaders。starter 代码就是这么写的 —— 这是出题人在提示 「这个字段不用 loader」。参数签名本身就是提示。
为什么 orders 不用 loader?因为 orderLoader 是按 order id批量取单个订单的。而这里要的是「某个 user 的所有订单」—— key 不一样,用不上。想用 loader 就得再造一个ordersByUserLoader,而 index.js里没有它,你也不该改 index.js(PROVIDED)。
Query.order | Query.orders | |
|---|---|---|
| Key words in the TODO | using DataLoader with structured error handling | error handling and correlation ID logging |
| Return type in the schema | Order (nullable) | [Order!]! (non-null twice over) |
| Where the data comes from | loaders.orderLoader.load(id) | dataSources.orderDataSource.getOrdersByUserId(userId) |
| context destructuring | { dataSources, loaders, correlationId } | { dataSources, correlationId } (no loaders) |
| When nothing is found | throws ORDER_NOT_FOUND | returns [] |
| Tests | ❌ not a single one | ✅ 2 of them |
Notice that the signature of Query.orders has no loaders in it. That is how the starter code is written — the examiner hinting that this field does not use a loader. The parameter signature is itself a hint.
Why does orders not use a loader? Because orderLoader batches single orders by order id, and what this field wants is “every order belonging to one user” — a different key, so it does not fit. Using a loader would mean building an ordersByUserLoader, and index.js has no such thing, and you should not be editing index.js anyway (PROVIDED).
Query.order:用 loader + 找不到要抛错Query.order: use the loader, and throw when nothing is found
为什么这里用 loader?这个字段本身只取一条,看起来不需要合并。 但 loader 的另一个作用是同请求内缓存 —— 如果一次查询里多处引用同一个 order (比如 { a: order(id:"1") { ... } b: order(id:"1") { ... } }), loader 只会真的取一次。而且 TODO 原文点名了它。
找不到怎么办?schema 说 order(id: ID!): Order 是可空的, 所以 return null 不违反 schema。 但 TODO 要求 structured error handling, 而 ErrorCodes 里恰好准备了一个ORDER_NOT_FOUND —— 这是明显的暗示。
那个 ErrorCodes 常量表值得注意:四个 code 里有一个(INVENTORY_ERROR) 在参考答案里没用到,ORDER_NOT_FOUND 只有这里用。准备好的常量就是在告诉你「这里该抛什么」。
Why use a loader here? This field fetches a single row, so batching looks pointless. But a loader’s other job is caching inside one request — if a single query references the same order in several places (say { a: order(id:"1") { ... } b: order(id:"1") { ... } }), the loader only fetches once. And the TODO names it outright.
What happens when nothing is found? The schema says order(id: ID!): Order is nullable, so return null breaks no rule. But the TODO asks for structured error handling, and ErrorCodes happens to have an ORDER_NOT_FOUND ready — a fairly loud hint.
That ErrorCodes table repays a close read: one of its four codes (INVENTORY_ERROR) is never used in the reference answer, and ORDER_NOT_FOUND is used only here. A constant somebody prepared for you is telling you what to throw where.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js这里最能看出 instanceof 检查为什么必要This is where the instanceof check clearly matters
同一个 try 块里既抛业务错误又要接系统错误 —— 不判断就必然出错。The same try block both throws a business error and catches system errors. Without the check it will always go wrong.
看 Query.order 的结构:try 块里自己抛了一个ORDER_NOT_FOUND。 而同一个 catch 又要负责接住数据源可能抛的系统异常。
如果 catch 里没有 instanceof 判断:你抛的 ORDER_NOT_FOUND 会被自己的 catch 接住, 然后重新包成 SERVICE_ERROR。 客户端查一个不存在的订单,收到的是「服务器内部错误」—— 它会重试,而重试永远不会成功。
加上那一行之后:ORDER_NOT_FOUND 原样传出去, 客户端知道「这个 id 不存在,别重试了」; 而数据源真的挂了(比如网络超时)时, 才会得到 SERVICE_ERROR。两种情况被正确区分了。
审计时用 order(id: "order-999")实测过,返回的 code 确实是 ORDER_NOT_FOUND。
Look at the shape of Query.order: the try block throws an error itself, an ORDER_NOT_FOUND. And the same catch is also responsible for the system exceptions the data source might throw.
Without the instanceof check in the catch: the ORDER_NOT_FOUND you threw is caught by your own catch and rewrapped as SERVICE_ERROR. A client asking for an order that does not exist is told “internal server error” — so it retries, and the retry can never succeed.
With that one line added: ORDER_NOT_FOUND travels out untouched and the client learns “this id does not exist, stop retrying”; only when the data source genuinely fails (a network timeout, say) do you get a SERVICE_ERROR. The two cases are told apart correctly.
The audit measured this with order(id: "order-999"), and the code that came back really was ORDER_NOT_FOUND.
Query.orders:用数据源 + 校验参数Query.orders: use the data source and validate the argument
和 User.orders 几乎一样,只有两处差别:
- userId 来自 args 而不是 parent。
async orders(_, { userId }, ...)—— 第一个参数是_(顶层 Query 没有有意义的 parent)。 - 要校验 userId。schema 写的是
userId: ID!, GraphQL 会保证它不是 null。 但空字符串""能通过 schema 校验(它是个合法的 ID 值),所以自己再挡一道更稳。 这也是 TODO 里 error handling 的一部分。
返回类型是 [Order!]!,所以照样 ?? []。
Almost identical to User.orders, with two differences:
- userId comes from args, not from parent.
async orders(_, { userId }, ...)— the first parameter is_, because a top-level Query has no meaningful parent. - userId has to be validated. The schema says
userId: ID!, so GraphQL guarantees it is not null. But an empty string""passes schema validation (it is a legal ID value), so a second guard of your own is safer. This is part of the error handling the TODO asks for.
The return type is [Order!]!, so once again ?? [].
Query.order 没有测试意味着什么What it means that Query.order has no test
十个测试里,Query.order 一条都没有。 所以你完全不实现它,npm test 也是全绿。
三种可能的处理方式,以及各自的后果:
- 不实现,留着
return null。测试全绿。但代码里留着一个明晃晃的 TODO 注释, 人工 review 一眼就看到。 - 删掉 TODO 注释但还是 return null。更糟 —— 这看起来像「我以为我做完了」, 比留着 TODO 更容易被判定为疏漏。
- 照 TODO 要求实现。测试不会因此多绿一条,但 TODO 清空、
ORDER_NOT_FOUND这个准备好的错误码被用上了。
选 3。Online Assessment 通常是「自动测试 + 人工 review」双轨的。 自动测试是及格线,人工 review 看的是 「有没有做完、有没有理解设计意图」。一个没被测试覆盖但明确要求的 TODO, 正是拉开差距的地方。
Of the ten tests, not one covers Query.order. So you can skip implementing it entirely and npm test is still all green.
Three ways to handle that, and what each one costs:
- Do not implement it, leave the
return null. All tests green. But a glaring TODO comment stays in the code, and a human reviewer spots it instantly. - Delete the TODO comment but still return null. Worse — it reads as “I thought I was done”, which looks more careless than leaving the TODO in place.
- Implement what the TODO asks. No test turns green because of it, but the TODO list is clear and that prepared
ORDER_NOT_FOUNDcode finally gets used.
Pick 3. An online assessment usually runs on two tracks: automated tests plus a human review. The tests are the pass mark; the review asks whether you finished and whether you understood the design intent. A TODO that no test covers but the brief clearly requires is exactly where candidates pull ahead.
动手做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.
四个空横跨两个 resolver。注意它们数据来源不同、兜底策略不同。
Four blanks across two resolvers. Note they read from different places and need different fallbacks.
两个函数一起写。注意它们的数据来源、兜底策略、 context 解构都不一样。
Write both functions together. They differ in where they read from, what they fall back to, and what they destructure out of context.
- Query.order 用 orderLoader 取数据Query.order reads through orderLoader
- Query.order 找不到时抛带 ORDER_NOT_FOUND code 的 GraphQLErrorWhen Query.order finds nothing, it throws a GraphQLError carrying the ORDER_NOT_FOUND code
- Query.orders 用 orderDataSource.getOrdersByUserId 取数据Query.orders reads through orderDataSource.getOrdersByUserId
- Query.orders 校验 userId,非法时抛 INVALID_INPUTQuery.orders validates userId and throws INVALID_INPUT when it is not valid
- Query.orders 绝不返回 null(schema 是 [Order!]!)Query.orders never returns null (the schema says [Order!]!)
- 两个都用 try/catch,catch 里先放行已有的 GraphQLErrorBoth use try/catch, and the catch lets an existing GraphQLError through first
- 两个都在日志里带上 correlationIdBoth include correlationId in their log line
看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.
初学者常见的几种写法错误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.
orderLoader 的 key 是 order id, 不是 user id。传 "123" 进去会去找id === "123" 的订单 —— 数据源里的 id 长得像 order-456,所以找不到, 返回 undefined。而且返回的是单个对象而不是数组,违反
[Order!]!。提示其实在参数签名里:starter 给的
orders 签名没有解构 loaders。The key of orderLoader is an order id, not a user id. Passing "123" makes it look for the order whose id === "123" — the ids in the data source look like order-456, so nothing matches and it returns undefined.It also returns a single object instead of an array, which breaks
[Order!]!.The hint is in the argument list: the
orders signature in the starter code does not destructure loaders.order 是可空的), 而且没有测试会挂。但 TODO 要求 structured error handling, 而
ErrorCodes.ORDER_NOT_FOUND 明摆着是为这里准备的。给好但没用上的常量,就是没做完的信号。This does not break the schema (order is nullable), and no test fails.But the TODO asks for structured error handling, and
ErrorCodes.ORDER_NOT_FOUND is clearly there for this spot. A constant that is given but never used means the work is not finished.换一道题也能用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.
- Query.order 用 orderLoader(TODO 点名了),Query.orders 用数据源(signature 里没给 loaders)。Query.order uses orderLoader (the TODO names it). Query.orders uses the data source (its signature does not receive loaders).
- Query.order 可空 → 找不到抛 ORDER_NOT_FOUND;Query.orders 双重非空 → 兜底 []。Query.order is nullable, so throw ORDER_NOT_FOUND when nothing is found. Query.orders is non-null at both levels, so fall back to [].
- 同一个 userId,在 User.orders 里来自 parent,在 Query.orders 里来自 args。The same userId comes from parent inside User.orders and from args inside Query.orders.
- catch 第一行的 instanceof 判断在 Query.order 里最关键 —— 同一个 try 里既抛业务错又接系统错。The instanceof check on the first line of catch matters most in Query.order, where one try block both throws a business error and catches system errors.
- Query.order 没有测试但 TODO 明确要求 —— 实现它,这是拉开差距的地方。Query.order has no test but the TODO clearly asks for it. Implement it; this is where answers differ.