GraphQL 是什么:一份 schema 加一堆 resolverWhat GraphQL is: one schema plus a set of resolvers
读真实的 schema.graphql,把 type / field / Query / Mutation 一次讲清。Read the real schema.graphql and cover type / field / Query / Mutation in one pass.
这一页有什么On this page8
- 01 GraphQL 服务只有两半A GraphQL service has only two halves
- 02 读真实的 schema.graphqlReading the real schema.graphql
- 03 type 和 fieldtype and field
- 04 Query 和 Mutation:两个特殊的入口类型Query and Mutation: the two special entry types
- 05 客户端决定返回形状The client decides the shape of the response
- 06 一次查询的完整执行流程The full execution flow of one query
- 练习 · 动手做Practice
- 迁移模式Transfer
- 说清 schema 在 GraphQL 里的地位Explain what role the schema plays in GraphQL
- 读懂 type / field / 标量 / enum / input 各是什么Read a schema and say what type / field / scalar / enum / input each mean
- 分清 Query 和 MutationTell Query and Mutation apart
- 知道「客户端决定返回形状」意味着什么Know what it means that the client decides the shape of the response
这份 schema 决定了你的 resolver 必须返回什么形状。审计发现有两处细节(双重非空、input 里没有 price)直接决定实现对错 —— 不读 schema 就写 resolver,必错。The schema decides what shape each resolver must return. Two details in it (a doubly non-null type, and an input with no price field) decide whether your code is right or wrong. If you write resolvers without reading the schema, you will get them wrong.
graphql-federation-practice/node-subgraph/src/schema.graphql整个 subgraph 的契约The contract for the whole subgraph
graphql-federation-practice/node-subgraph/src/schema.graphqlGraphQL 服务只有两半A GraphQL service has only two halves
一半是「有什么」,一半是「怎么拿到」。One half says what exists. The other half says how to fetch it.
schema 声明「这个服务提供哪些类型、 每个类型有哪些字段、字段是什么类型、能不能为空」。 它是一份契约,用一种叫SDL(Schema Definition Language)的语法写。
resolver 是一堆函数,负责「某个字段的值实际怎么算出来」。 schema 说 Order 有个 shippingInfo 字段, resolver 负责真的去物流服务把它取回来。
两半必须对得上。schema 里有的字段没有 resolver → 返回 null;resolver 的名字和 schema 里的字段名不一致 → 这个 resolver 永远不会被调用,而且不报错。后者是 GraphQL 最难查的一类 bug,后面 Debug Lab 会专门练。
The schema declares what types this service offers, what fields each type has, what type each field is, and whether it can be null. It is a contract, written in a syntax called SDL (Schema Definition Language).
The resolvers are a pile of functions that say how each field’s value actually gets produced. The schema says Order has a shippingInfo field; a resolver is what really goes to the shipping service and fetches it.
The two halves have to line up. A field that exists in the schema with no resolver returns null; a resolver whose name does not match the schema field name is never called at all — and nothing complains. The second one is the hardest class of GraphQL bug to track down, and a later Debug Lab drills it.
读真实的 schema.graphqlReading the real schema.graphql
先整体看一遍,再逐块拆。Read it once as a whole, then take it apart block by block.
这是 node-subgraph/src/schema.graphql 的全文, 一个字没改。第 1–2 行的 extend schema @link是 Federation 专用的,下一个模块再讲,先跳过:
This is the whole of node-subgraph/src/schema.graphql, not one character changed. Lines 1–2, the extend schema @link part, are Federation-only — the next module covers them, so skip them for now:
graphql-federation-practice/node-subgraph/src/schema.graphqltype 和 fieldtype and field
type Order { ... } 声明了一个对象类型,花括号里每一行是一个字段(field),格式是字段名: 类型。
字段的类型分三种:
- 标量(scalar) —— 内置的叶子类型, 不能再往下展开。GraphQL 内置 5 个:
ID、String、Int、Float、Boolean。 这份 schema 里totalAmount: Float!、quantity: Int!都是。
注意createdAt: String!—— 时间被存成了字符串,不是什么 DateTime 类型。 GraphQL 没有内置日期标量。 - 对象类型 —— 可以继续往下查。
items: [OrderItem!]!、shippingInfo: ShippingInfo都是。 - 枚举(enum) ——
status: OrderStatus!,值只能是PENDING/PROCESSING/SHIPPED/DELIVERED/CANCELLED五个之一。返回一个不在列表里的字符串会报错。
ID 值得单说:它序列化成字符串, 但语义是「这是个标识符,不要拿它做算术」。 所以 getOrdersByUserId("123") 里那个 userId 是字符串 "123" 而不是数字 123 —— 这一点和 React 那门考试里 Note.id 是number 正好相反,别混。
type Order { ... } declares an object type. Every line inside the braces is a field, written as fieldName: Type.
Field types come in three kinds:
- Scalars — the built-in leaf types, nothing to expand further. GraphQL ships five:
ID,String,Int,Float,Boolean. In this schematotalAmount: Float!andquantity: Int!are both scalars.
Look atcreatedAt: String!— the timestamp is stored as a string, not as some DateTime type. GraphQL has no built-in date scalar. - Object types — you can keep querying downward.
items: [OrderItem!]!andshippingInfo: ShippingInfoare both object types. - Enums —
status: OrderStatus!, where the value can only be one ofPENDING/PROCESSING/SHIPPED/DELIVERED/CANCELLED. Return a string that is not on that list and you get an error.
ID deserves its own note: it serialises to a string, but what it means is “this is an identifier, do not do arithmetic on it”. So the userId in getOrdersByUserId("123") is the string "123", not the number 123 — the exact opposite of the React exam, where Note.id is a number. Do not mix the two up.
Query 和 Mutation:两个特殊的入口类型Query and Mutation: the two special entry types
Query 和 Mutation 是两个约定的入口类型。客户端只能从它们的字段开始查。
Query= 读。这份 schema 提供两个入口:order(id: ID!): Order(按 id 取一条,可空 —— 找不到就返回 null)和orders(userId: ID!): [Order!]!(按用户取列表,不可空)。Mutation= 写。这里只有createOrder。GraphQL 不强制你把写操作放 Mutation, 但这是所有人都遵守的约定 —— 而且 Mutation 的多个字段 是串行执行的,Query 的字段是并行的。
圆括号里的是参数(argument):order(id: ID!) 意思是「调这个字段必须给一个非空的 ID」。 参数在 resolver 里通过第二个参数 args 拿到。
input OrderItemInput 是输入类型。 它和 type 的区别是:input 只能当参数用, 字段只能是标量、enum 或别的 input,不能有 resolver。这个 input 后面会成为一个大坑, 下一节专门讲。
Query and Mutation are two entry types fixed by convention. A client can only start from their fields.
Query= read. This schema offers two entries:order(id: ID!): Order(one order by id, nullable — not found means null) andorders(userId: ID!): [Order!]!(a list for one user, not nullable).Mutation= write. There is onlycreateOrderhere. GraphQL does not force you to put writes under Mutation, but everybody follows the convention — and the fields of one Mutation run one after another, while Query fields run in parallel.
What sits inside the parentheses is an argument: order(id: ID!) means “calling this field requires a non-null ID”. A resolver reads arguments from its second parameter, args.
input OrderItemInput is an input type. The difference from type: an input can only be used as an argument, its fields can only be scalars, enums or other inputs, and it has no resolvers. This one input becomes a big trap later — the next lesson is all about it.
客户端决定返回形状The client decides the shape of the response
这是 GraphQL 和 REST 最本质的区别。This is the deepest difference between GraphQL and REST.
REST 里 GET /api/orders/456 返回什么字段, 是服务端定的。GraphQL 里客户端自己写:
这带来两个后果,都跟考试有关:
- 你的 resolver 可能根本不被调用。客户端没查
shippingInfo,那个 resolver 就不执行 —— GraphQL 是按需调用 resolver 的。 - N+1 问题的根源。客户端查了 100 个 order 的 shippingInfo, 执行器就会调 100 次
Order.shippingInfo。 客户端一句话,后端 100 次请求。 这就是为什么这个项目里要用 DataLoader。
In REST, the server decides which fields GET /api/orders/456 returns. In GraphQL the client writes that itself:
Two consequences, and both of them show up in the exam:
- Your resolver may never run. The client did not ask for
shippingInfo, so that resolver is never executed — GraphQL calls resolvers on demand. - This is where N+1 comes from. Ask for the shippingInfo of 100 orders and the executor calls
Order.shippingInfo100 times. One sentence from the client, 100 requests on the backend. That is why this project needs DataLoader.
一次查询的完整执行流程The full execution flow of one query
把上面的知识串起来。下面这张图是第二个查询(带 shippingInfo 的那个) 在服务端的完整旅程,七步:
Now string all of it together. The diagram below is the full server-side journey of that second query (the one that asks for shippingInfo), in seven steps:
动手做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.
看真实 schema 里的 type Order。 下面哪些字段的类型是标量(不能再往下展开)?(多选)
Look at type Order in the real schema. Which of these fields have a scalar type — one that cannot be expanded any further? (Select all that apply.)
graphql-federation-practice/node-subgraph/src/schema.graphql这题是多选。More than one answer is correct.
真实 schema 里 createOrder 放在type Mutation 下。如果把它挪到type Query 下会怎样?
In the real schema, createOrder sits under type Mutation. What happens if you move it under type Query?
照真实 schema.graphql 补全。 三个空分别关系到「入口类型」「枚举」「输入类型」。
Fill this in from the real schema.graphql. The three blanks are the entry type, the enum and the input type.
换一道题也能用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.
- GraphQL = schema(有什么)+ resolver(怎么拿到),两半必须对得上。GraphQL = schema (what exists) + resolver (how to fetch it). The two halves must match.
- 字段类型分标量 / 对象 / enum;ID 序列化成字符串,别当数字用。Field types are scalar, object or enum. ID is serialized as a string, so do not treat it as a number.
- Query 是读入口(字段并行),Mutation 是写入口(字段串行)。Query is the read entry point (its fields run in parallel). Mutation is the write entry point (its fields run one after another).
- input 只能当参数,不能有 resolver —— 而这个项目的 OrderItemInput 里没有 price。An input can only be used as an argument and cannot have resolvers. In this project OrderItemInput has no price.
- 客户端决定返回形状,所以 resolver 是按需调用的,也因此产生 N+1 问题。The client decides the shape of the response, so resolvers are called only when needed. That is also where the N+1 problem comes from.