entity、@key 与 __resolveReferenceentity, @key and __resolveReference
「另一个服务要用哪个字段找到这个对象?」—— 想清这一句,这三个概念全通。Which field does another service use to find this object? Answer that one question and all three ideas become clear.
这一页有什么On this page6
- 01 entity:可以被多个服务共同描述的类型entity: a type that several services can describe together
- 02 @external:这个字段不是我的@external: this field is not mine
- 03 __resolveReference:把「引用」变成「本地对象」__resolveReference: turning a reference into a local object
- 04 完整链路:从客户端一句话到两个服务The full path: one client query, two services
- 练习 · 动手做Practice
- 迁移模式Transfer
- 用一句话解释 @key 在声明什么Explain in one sentence what @key declares
- 说清 @external 标在什么场合Say when a field should be marked @external
- 读懂 __resolveReference 的输入和输出Read what goes into __resolveReference and what comes out
- 画出 Router 做实体解析的完整链路Draw the full path the Router takes to resolve an entity
User.orders 这个 TODO 就长在这套机制上。不理解 __resolveReference 的返回值会流向哪里,就不知道自己的 orders resolver 里 user.id 从何而来。The User.orders TODO sits on top of this mechanism. If you do not know where the return value of __resolveReference goes, you will not know where user.id inside your orders resolver comes from.
graphql-federation-practice/node-subgraph/src/schema.graphql@key 与 @external 的真实用法How @key and @external are actually used
graphql-federation-practice/node-subgraph/src/schema.graphqlgraphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js__resolveReference 已给好,orders 要你写__resolveReference is given; orders is yours to write
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。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.jsentity:可以被多个服务共同描述的类型entity: a type that several services can describe together
不是所有类型都是 entity。判据是「别的服务需不需要引用它」。Not every type is an entity. The test is whether another service needs to refer to it.
entity(实体)是「同一个东西在多个 subgraph 里 都有一部分字段」的类型。User 就是: Accounts 有它的 name/email,本项目有它的 orders。
要成为 entity,类型必须声明「怎么认出同一个我」—— 这就是 @key:
@key(fields: "id") 读作:「拿 id 这个字段就能唯一定位一个 User」。于是 Router 只要手里有 { __typename: "User", id: "123" }, 就能去任何声明了这个 key 的 subgraph 补齐字段。
@key 可以是复合的:@key(fields: "orgId userId") 表示 要两个字段才能定位。也可以有多个 @key (同一个类型能用几种方式定位)。本项目只用了最简单的单字段形式。
不是 entity 的类型呢?看这份 schema:Order、OrderItem、ShippingInfo 都没有 @key。 因为它们只在本 subgraph 里存在,别的服务不需要引用它们。没必要就不要加 @key —— 加了反而要维护 __resolveReference。
An entity is a type whose fields live partly in one subgraph and partly in another — the same thing described in several places. User is one: Accounts has its name and email, this project has its orders.
To become an entity, a type has to declare how to recognise the same one of me — and that is @key:
Read @key(fields: "id") as “the id field alone pinpoints one User”. So the moment the Router holds { __typename: "User", id: "123" }, it can go to any subgraph that declares this key and fill in more fields.
A @key can be compound: @key(fields: "orgId userId") says it takes two fields to pinpoint one. A type can also have several @keys, so it can be identified in more than one way. This project uses the simplest single-field form.
What about types that are not entities? Look at this schema: Order, OrderItem and ShippingInfo have no @key. They exist only inside this subgraph and no other service needs to reference them. Do not add a @key you do not need — it buys you a __resolveReference to maintain.
graphql-federation-practice/node-subgraph/src/schema.graphql@external:这个字段不是我的@external: this field is not mine
id: ID! @external 的意思是「这个字段由别的 subgraph 定义和提供, 我只是需要它来完成 @key」。
所以本项目不需要为 User.id写 resolver、不需要有用户表。它只是借这个字段做身份识别。
本 subgraph 真正贡献的字段是 orders —— 它没有 @external,说明「这个字段是我的,我负责实现」。
一个诚实的注解:在 Federation 2 里,为一个自己不拥有的 entity 加字段, 标准写法其实是 type User @key(fields: "id")加上普通的 id: ID!(不用 @external)。@external 更多是 Federation 1 的遗留写法。但这个项目就是这么写的,而且buildSubgraphSchema 接受它 —— 审计时实测 SDL 正常生成、_entities 查询正常工作。考试里照着项目已有的写法走,别自己改 schema 风格。
id: ID! @external means “another subgraph defines and provides this field; I only need it to satisfy my @key”.
So this project does not need a resolver for User.id and does not need a user table. It borrows the field purely to tell one user from another.
The field this subgraph really contributes is orders — it has no @external, which says “this field is mine, I implement it”.
One honest footnote: in Federation 2, the standard way to add a field to an entity you do not own is type User @key(fields: "id") plus a plain id: ID!, with no @external. @external is mostly a Federation 1 leftover. But this project writes it that way, and buildSubgraphSchema accepts it — the audit confirmed the SDL is emitted fine and _entities queries work. In the exam, follow the style already in the project; do not rewrite the schema to your own taste.
__resolveReference:把「引用」变成「本地对象」__resolveReference: turning a reference into a local object
它是 entity 解析的入口,也是 User.orders 的上游。It is the entry point for entity resolution, and it runs right before User.orders.
Router 把 { __typename: "User", id: "123" }交给本 subgraph 时,第一个被调用的就是User.__resolveReference。
它的职责:拿到这个「引用」, 返回一个本地能用的对象。这个返回值会成为下游所有字段 resolver 的 parent。
这个项目里它已经写好了,而且非常简单:
为什么这么简单就够了?因为本 subgraph 只贡献 orders 一个字段, 而算 orders 只需要 user.id。 不需要去查用户表 —— 本项目也没有用户表。
这一行是理解 User.orders 的钥匙:__resolveReference 返回 { id: "123" }, 所以你写的 User.orders(user, ...) 里那个user 就是 { id: "123" },user.id 就是 "123"。它上面没有 name、没有 email —— 别指望能拿到那些字段。
测试也是这么模拟的:const user = { id: '123' }, 然后 resolvers.User.orders(user, {}, context)。直接调 resolver 函数,绕过了整个 GraphQL 执行器—— 这就是为什么这些测试跑得那么快(0.16 秒)。
When the Router hands { __typename: "User", id: "123" } to this subgraph, the first thing called is User.__resolveReference.
Its job: take that reference and return an object this service can work with. The return value becomes the parent of every field resolver downstream.
It is already written in this project, and it is very short:
Why is that enough? Because this subgraph contributes one field, orders, and computing orders needs nothing but user.id. No user table to query — this project has none.
That single line is the key to understanding User.orders: __resolveReference returns { id: "123" }, so the user inside the User.orders(user, ...) you write is { id: "123" } and user.id is "123". There is no name on it and no email — do not expect to read those fields.
The tests simulate it the same way: const user = { id: '123' }, then resolvers.User.orders(user, {}, context). They call the resolver function directly and skip the whole GraphQL executor — which is why these tests finish in 0.16 seconds.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.jsgraphql-federation-practice/node-subgraph/__tests__/resolvers.test.js完整链路:从客户端一句话到两个服务The full path: one client query, two services
把上面所有东西串起来。这张图六步走完 Router 的实体解析, 其中第 5 步就是你要写的代码被调用的地方:
user 上就是有 orders 字段。 它完全不知道背后有两个服务。String everything above together. This diagram walks the Router’s entity resolution in six steps, and step 5 is where the code you write gets called:
user 上就是有 orders 字段。 它完全不知道背后有两个服务。动手做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.
type User @key(fields: "id") 最准确的含义是?
What does type User @key(fields: "id") mean, most precisely?
__resolveReference 返回 { id: user.id }。 那么 User.orders(user, ...) 里的 user上有哪些属性?
__resolveReference returns { id: user.id }. So which properties does user have inside User.orders(user, ...)?
三个空。第一个是 directive,第二个是标记「这不是我的字段」, 第三个是引用解析要返回什么。
Three blanks. The first is a directive, the second marks a field as not belonging to this service, and the third is what the reference resolver returns.
换一道题也能用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.
- entity = 多个 subgraph 共同描述的类型;@key 声明「靠哪个字段跨服务认人」。An entity is a type that several subgraphs describe together. @key declares which field identifies it across services.
- @key 和数据库主键无关,可以复合、可以有多个。@key has nothing to do with a database primary key. It can cover several fields, and one type can have more than one.
- @external 表示「这个字段是别人的,我只借来做身份识别」。@external means the field belongs to another service and you only borrow it to identify the object.
- __resolveReference 把 representation 变成本地对象,它的返回值就是下游 parent。__resolveReference turns a representation into a local object, and its return value becomes the parent for the fields below.
- 本项目的 __resolveReference 只返回 { id },所以 User.orders 里只有 user.id 可用。In this project __resolveReference returns only { id }, so inside User.orders the only value you can use is user.id.