DrillLab
第 06 / 17 节LESSON 06 / 17约 16 分钟~16 min

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.

3 个练习3 exercisesFederation · 第 2 部分Federation · Part 2
这一页有什么On this page6
学完这节你会After this lesson you can
  • 用一句话解释 @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
这在考试里考什么What the exam does with this

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.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
graphql-federation-practice/node-subgraph/src/schema.graphql@key 与 @external 的真实用法How @key and @external are actually used
GraphQL SDLschema.graphql源项目From source
1extend schema
2 @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable", "@external"])
3
4type User @key(fields: "id") {
5 id: ID! @external
6 orders: [Order!]!
7}
8
9type Order {
10 id: ID!
11 userId: ID!
12 status: OrderStatus!
13 totalAmount: Float!
14 items: [OrderItem!]!
15 createdAt: String!
16 shippingInfo: ShippingInfo
17}
18
19type OrderItem {
20 productId: ID!
21 quantity: Int!
22 price: Float!
23}
24
25type ShippingInfo {
26 status: String!
27 estimatedDelivery: String
28 trackingNumber: String
29}
30
31enum OrderStatus {
32 PENDING
33 PROCESSING
34 SHIPPED
35 DELIVERED
36 CANCELLED
37}
38
39type Query {
40 order(id: ID!): Order
41 orders(userId: ID!): [Order!]!
42}
43
44type Mutation {
45 createOrder(userId: ID!, items: [OrderItemInput!]!): Order!
46}
47
48input OrderItemInput {
49 productId: ID!
50 quantity: Int!
51}
Source: graphql-federation-practice/node-subgraph/src/schema.graphql
graphql-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.

JavaScriptorderResolvers.js源项目From source
1import DataLoader from 'dataloader';
2import { GraphQLError } from 'graphql';
3
4// Custom error codes
5const ErrorCodes = {
6 ORDER_NOT_FOUND: 'ORDER_NOT_FOUND',
7 INVALID_INPUT: 'INVALID_INPUT',
8 INVENTORY_ERROR: 'INVENTORY_ERROR',
9 SERVICE_ERROR: 'SERVICE_ERROR'
10};
11
12// DataLoader for batching shipping info requests
13function createShippingInfoLoader(shippingDataSource) {
14 return new DataLoader(async orderIds => {
15 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
16
17 const shippingInfos = await Promise.all(
18 orderIds.map(id => shippingDataSource.getShippingInfo(id))
19 );
20
21 return shippingInfos;
22 });
23}
24
25// DataLoader for batching order requests
26function createOrderLoader(orderDataSource) {
27 return new DataLoader(async orderIds => {
28 console.log(`[DataLoader] Batching ${orderIds.length} order requests`);
29
30 const orders = await Promise.all(
31 orderIds.map(id => orderDataSource.getOrderById(id))
32 );
33
34 return orders;
35 });
36}
37
38export const resolvers = {
39 User: {
40 // Reference Resolver - extend User entity from Accounts subgraph
41 __resolveReference(user, { dataSources, loaders }) {
42 return { id: user.id };
43 },
44
45 // Field resolver with caching
46 async orders(user, _, { dataSources, loaders, correlationId }) {
47 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
48 return [];
49 }
50 },
51
52 Order: {
53 // Field resolver for shipping info with DataLoader
54 async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
55 // TODO: Implement shipping info resolver using DataLoader to prevent N+1 queries
56 return null;
57 }
58 },
59
60 Query: {
61 async order(_, { id }, { dataSources, loaders, correlationId }) {
62 // TODO: Implement order query using DataLoader with structured error handling
63 return null;
64 },
65
66 async orders(_, { userId }, { dataSources, correlationId }) {
67 // TODO: Implement orders query with error handling and correlation ID logging
68 return [];
69 }
70 },
71
72 Mutation: {
73 // Mutation provided for reference - candidates focus on Query resolvers
74 async createOrder(_, { userId, items }, { dataSources, correlationId }) {
75 try {
76 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
77
78 if (!userId || !items || items.length === 0) {
79 throw new GraphQLError('Invalid order input', {
80 extensions: {
81 code: ErrorCodes.INVALID_INPUT,
82 correlationId
83 }
84 });
85 }
86
87 const order = await dataSources.orderAPI.createOrder({ userId, items });
88 console.log(`[${correlationId}] Order created: ${order.id}`);
89
90 return order;
91 } catch (error) {
92 console.error(`[${correlationId}] Error creating order:`, error.message);
93 throw new GraphQLError('Failed to create order', {
94 extensions: {
95 code: ErrorCodes.SERVICE_ERROR,
96 correlationId,
97 originalError: error.message
98 }
99 });
100 }
101 }
102 }
103};
104
105export { createShippingInfoLoader, createOrderLoader };
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
§01

entity:可以被多个服务共同描述的类型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:OrderOrderItemShippingInfo没有 @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 SDL源项目From source
1type User @key(fields: "id") { # ← entity:靠 id 认人
2 id: ID! @external
3 orders: [Order!]!
4}
5
6type Order { # ← 不是 entity:没有 @key
7 id: ID!
8 ...
9}
1type User @key(fields: "id") { # ← an entity: id is how you identify it
2 id: ID! @external
3 orders: [Order!]!
4}
5
6type Order { # ← not an entity: no @key
7 id: ID!
8 ...
9}
Source: graphql-federation-practice/node-subgraph/src/schema.graphql
§02

@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.

§03

__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.

JavaScriptsrc/resolvers/orderResolvers.js(User 部分)src/resolvers/orderResolvers.js (the User part)源项目From source
1User: {
2 // Reference Resolver - extend User entity from Accounts subgraph
3 __resolveReference(user, { dataSources, loaders }) {
4 return { id: user.id };
5 },
6
7 // Field resolver with caching
8 async orders(user, _, { dataSources, loaders, correlationId }) {
9 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
10 return [];
11 }
12},
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
JavaScript测试怎么调它How the test calls it源项目From source
1it('should return orders for a user', async () => {
2 const user = { id: '123' }; // ← 就是这么简单
3 const orders = await resolvers.User.orders(user, {}, context);
4
5 expect(orders).toBeDefined();
6 expect(Array.isArray(orders)).toBe(true);
7 expect(orders.length).toBeGreaterThan(0);
8 expect(orders[0]).toHaveProperty('userId', '123');
9});
1it('should return orders for a user', async () => {
2 const user = { id: '123' }; // ← that is all it takes
3 const orders = await resolvers.User.orders(user, {}, context);
4
5 expect(orders).toBeDefined();
6 expect(Array.isArray(orders)).toBe(true);
7 expect(orders.length).toBeGreaterThan(0);
8 expect(orders[0]).toHaveProperty('userId', '123');
9});
Source: graphql-federation-practice/node-subgraph/__tests__/resolvers.test.js
测试直接调 resolver 函数,手工构造 parent 和 context。这意味着:你的 resolver 只要参数用法正确就能过测试,不需要整个 GraphQL 服务器跑起来。The test calls the resolver function directly and builds parent and context by hand. That means your resolver passes as long as it uses its arguments correctly; you do not need the whole GraphQL server running.
§04

完整链路:从客户端一句话到两个服务The full path: one client query, two services

把上面所有东西串起来。这张图六步走完 Router 的实体解析, 其中第 5 步就是你要写的代码被调用的地方:

Router 怎么把两个 subgraph 的数据缝在一起第 1 / 6 步Step 1 of 6
客户端
查 user + 他的 orders
{ user(id:"123") { name orders { id status } } }
Router
拆查询计划
subgraph A
Accounts(不在本仓库)
Router
拿到 User 的引用
subgraph B
Orders(就是本项目)
Router
合并成一个响应
客户端眼里只有一张图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:

Router 怎么把两个 subgraph 的数据缝在一起第 1 / 6 步Step 1 of 6
客户端
查 user + 他的 orders
{ user(id:"123") { name orders { id status } } }
Router
拆查询计划
subgraph A
Accounts(不在本仓库)
Router
拿到 User 的引用
subgraph B
Orders(就是本项目)
Router
合并成一个响应
客户端眼里只有一张图user 上就是有 orders 字段。 它完全不知道背后有两个服务。
练习Practice

动手做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.

L1认出来Spot it@key 在声明什么What @key declares

type User @key(fields: "id") 最准确的含义是?

What does type User @key(fields: "id") mean, most precisely?

先选一个选项Pick an option first
L1认出来Spot itUser.orders 里的 user 参数上有什么What the user argument of User.orders carries

__resolveReference 返回 { id: user.id }。 那么 User.orders(user, ...) 里的 user上有哪些属性?

__resolveReference returns { id: user.id }. So which properties does user have inside User.orders(user, ...)?

先选一个选项Pick an option first
L2填空Fill the blanks补全 entity 声明与引用解析Fill in the entity declaration and the reference resolver

三个空。第一个是 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.

GRAPHQLschema.graphql + orderResolvers.js3 个空3 blanks
1# schema.graphql
2type User (fields: "id") {
3 id: ID!
4 orders: [Order!]!
5}
6
7# orderResolvers.js
8# User: {
9# __resolveReference(user, { dataSources, loaders }) {
10# return { id: };
11# },
12# ...
13# }
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
迁移Transfer

换一道题也能用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.

看到 @keyYou see @key
先问「别的服务用哪个字段找到这个对象」First ask which field another service uses to find this object
「我要给别人的类型加字段」You want to add a field to a type owned by another service
声明 @key + 把借来的字段标 @externalDeclare @key and mark the borrowed field @external
字段 resolver 拿不到某个属性A field resolver cannot see a property it expected
看 __resolveReference 返回了什么Check what __resolveReference returned
想验证 entity 解析You want to check entity resolution
查 _entities,representation 要带 __typenameQuery _entities; each representation must carry __typename
这节的要点What to take away
  1. entity = 多个 subgraph 共同描述的类型;@key 声明「靠哪个字段跨服务认人」。An entity is a type that several subgraphs describe together. @key declares which field identifies it across services.
  2. @key 和数据库主键无关,可以复合、可以有多个。@key has nothing to do with a database primary key. It can cover several fields, and one type can have more than one.
  3. @external 表示「这个字段是别人的,我只借来做身份识别」。@external means the field belongs to another service and you only borrow it to identify the object.
  4. __resolveReference 把 representation 变成本地对象,它的返回值就是下游 parent。__resolveReference turns a representation into a local object, and its return value becomes the parent for the fields below.
  5. 本项目的 __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.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises3 个,就在这一页上面 —— 别攒着最后一起做3 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lessonN+1 问题与 DataLoaderThe N+1 problem and DataLoader
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: subgraph 是怎么跑起来的How a subgraph starts up