8. 为实体贡献字段
15m

概述

尽管我们已经定义了Listing 在我们的 reviews 中,并为其贡献了 ,但我们还没有告诉它如何解析这些字段。

在本课中,我们将

  • 创建一个引用
  • 用数据填充 reviewsoverallRating

引用解析器

当我们 特定的列表及其评论数据时,我们需要一种方法来告诉 reviews 哪个 列表是我们正在谈论的。然后 reviews 将理解要提供哪些数据!

为了做到这一点, 传递了我们在上一课中谈到的 表示: reviews 为了拼凑出它正在获取数据的列表,所需的最小信息。

示例列表实体表示
{
"__typename": "Listing",
"id": "listing-3"
}

但在 reviews 中,这个 表示实际上 了哪里呢?

这正是我们缺少的:引用 。引用解析器是一种特殊的方法,可以接收 表示,来自 ,创建一个新的 Listing 实例,并将其返回。

让我们看看代码中的样子,并逐步完成这个过程。

添加 __resolveReference

回到 reviews 中,我们需要告诉我们的服务器当它接收到特定列表的 表示时该怎么办。我们将通过在 Listing 地图中的新条目下定义一个名为 __resolveReference 的特殊函数来实现。

打开 reviews/src/resolvers.ts。让我们添加 Listing 键和引用 ,如下所示。

reviews/src/resolvers.ts
// Query, Mutation entries above
Listing: {
__resolveReference: () => {},
}

您将在我们定义的这个新函数上看到一个错误。这是因为 __resolveReference 不是我们如在 reviews 模式文件中所述的 Listing 类型上的已知属性。

Object literal may only specify known properties, and '__resolveReference'
does not exist in type 'ListingResolvers<any, Listing>'.

我们可以通过在我们的 codegen.ts 中添加一个额外的属性来解决这个问题。在 config 对象中,我们可以添加一个名为 federation 的新键,并将它的值设置为 true。这使我们的 codegen 过程能够考虑我们 的一些特定于联合的要求:例如解析 表示,从 接收!

codegen.ts
config: {
contextType: "./context#DataSourceContext",
federation: true
},

您可能在您的 resolvers.ts 文件中看不到任何变化;但这次,TypeScript 对我们的函数还没有返回任何东西感到很不安。接下来让我们处理这个问题。

我们将接收 表示作为我们 __resolveReference 函数中的一个参数,名为 representation。然后,我们将把它记录下来,并返回它。

reviews/src/resolvers.ts
__resolveReference: (representation) => {
console.log(representation)
return representation;
},

即使有了这个变化,我们的函数仍然有另一个错误。当我们悬停在 __resolveReference 上时,我们会看到什么。

Property 'reviews' is missing in type '{ __typename: "Listing"; }
& GraphQLRecursivePick<Listing, { id: true; }>' but required in type 'Listing'.

这个很长的错误告诉我们一个很清楚的事情: __resolveReference 函数接收的 "listing"(来自 表示)缺少我们在模式文件中所说的它应该具有的某些基本属性。从本质上讲,我们的 codegen 过程 期望 我们在 resolvers.ts 文件中使用的所有 Listing 对象具有以下所有

type Listing @key(fields: "id") {
id: ID!
"The submitted reviews for this listing"
reviews: [Review!]!
"The overall calculated rating for a listing"
overallRating: Float
}

然而, 表示仅包含两个属性: __typenameid

我们需要修复 codegen 过程对 Listing 的理解,当它作为 表示进入 __resolveReference 函数时。为此,我们将使用 模型

介绍模型

中,定义了 之间的关系以及我们如何从一个对象到另一个对象。通过从一个对象到另一个对象,我们可以构建详细的查询,这些查询在单个客户端请求中获取我们所需的一切。

这是 函数的任务,使这种魔法成为可能:它们需要自由地接收可能与我们模式中的类型不匹配的数据,并执行将数据返回到我们 期望 的类型的逻辑。

A diagram showing data with different shapes entering a resolver, and data that conforms to the GraphqL schema leaving the resolver

为了保持类型安全,我们需要澄清我们的数据类型在解析器接收到的数据(来自,或者来自,就像我们的表示)和解析器返回给客户端的数据之间的区别。

在我们的例子中,我们的__resolveReference 认为它将接收一个看起来像我们Listing 类型的对象;所以我们需要使用一个模型来重新定义它的预期。

添加一个ListingEntityRepresentation 模型

我们将创建一个新的模型,让我们更准确地描述列表表示所采用的形状。然后我们将该模型集成到我们的代码生成流程中,并解决我们的类型错误。

reviews/src目录中,创建一个名为models.ts的文件。

📦 src
┣ 📂 datasources
┣ 📂 sequelize
┣ 📄 context.ts
┣ 📄 graphql.d.ts
┣ 📄 index.ts
┣ 📄 models.ts
┣ 📄 resolvers.ts
┣ 📄 schema.graphql
┗ 📄 types.ts

在里面,我们将定义一个名为ListingEntityRepresentation的基本模型。我们将赋予它我们关心的单个属性:id

models.ts
export type ListingEntityRepresentation = {
id: string;
};

接下来,我们将这个模型添加到我们的代码生成流程中,在codegen.ts中。

config键下,我们将添加另一个名为mappers的属性。在这里,我们可以指定每个模型的路径,这些模型将用作特定类型的映射器。这意味着,当代码生成器看到我们在我们的中使用特定 GraphQL 对象时,它将使用我们提供的模型作为该对象应该具有的属性的参考。

codegen.ts
config: {
contextType: "./context#DataSourceContext",
federation: true,
mappers: {
Listing: "./models#ListingEntityRepresentation"
}
},

让我们停止运行的reviews服务器并重新启动它,以确保应用了新的代码生成设置。这应该可以解决我们的错误!

现在让我们试一试。让我们回到 Sandbox 并再次尝试我们的

query GetListingAndReviews {
listing(id: "listing-1") {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

当我们运行时,我们仍然不会得到任何与评论相关的数据;但是,当我们检查我们的reviews 的终端时,我们将看到我们的表示已经到达并被打印出来了!

{__typename=Listing, id=listing-1}

我们的reviews 正在成功地从接收列表表示;这意味着我们知道哪些列表要提供数据。现在,我们只需要定义,以用于,即reviewsoverallRating,这是reviews 负责的。

添加Listing.reviews

让我们首先解决reviews方法。

返回到reviews/src/resolvers.ts。让我们清理掉__resolveReference中的console.log语句。然后,在__resolveReference的下方,我们将为添加一个新的,用于Listing.reviews

reviews/src/resolvers.ts
Listing: {
__resolveReference: (representation) => {
return representation;
},
reviews: () => {}
}

此方法应该使用来自表示的id来查找并返回数据库中所有相关的评论。由于此解析实体类型上的,我们知道链中的前一个解析器是__resolveReference ,我们刚刚定义了它。这意味着我们可以使用parent(每个接收的第一个位置)访问它的返回值(Listing实例)。

让我们为parent解构id属性。

reviews/src/resolvers.ts
reviews: ({ id }) => {
// TODO
};

我们的已经可以访问服务器的dataSources上的一个属性,名为reviewsDb,它是一个类,提供了一些对底层评论数据库进行操作的方法。让我们为的第三个位置contextValue解构dataSources属性。

reviews/src/resolvers.ts
reviews: ({ id }, _, { dataSources }) => {
// TODO
};

有了我们的列表id,我们可以查询数据库中所有与该列表关联的评论。该reviewsDb类实例提供了一个名为getReviewsByListing的方法,用于根据特定列表 ID 查询评论。让我们调用此方法,并将从我们的parent 获得的id属性作为参数传递。

reviews/src/resolvers.ts
reviews: ({ id }, _, { dataSources }) => {
return dataSources.reviewsDb.getReviewsByListing(id);
};

The overallRating method

进入overallRating

自己尝试一下。(提示:查看datasources/reviews.ts文件以获取有用的方法!)

准备好了之后,将你的代码与我们完成的进行比较。

reviews/src/resolvers.ts
overallRating: ({ id }, _, { dataSources }) => {
return dataSources.reviewsDb.getOverallRatingForListing(id);
},

运行我们的梦想查询

rover dev进程仍在运行的情况下,让我们在https://127.0.0.1:4002尝试我们的

query GetListingAndReviews {
listing(id: "listing-1") {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

提交查询,然后...我们获得了数据!🎉 我们已经将评论与列表关联,并让我们的梦想查询变为现实!

查看查询计划

让我们查看,看看这些数据是如何整合在一起的。我们将看到,首先,将从listings获取数据,然后使用这些数据来构建对reviews的请求。最后一步是将来自这两个的响应扁平化为Listing类型的单个实例,以用于我们查询的listing

https://127.0.0.1:4002

The operation in Sandbox, with the Query Plan Preview opened, showing a linear path to retrieve data from both subgraphs

仍然看到reviews: null?尝试重启你的reviews服务器!在端口4002上运行我们的rover dev进程会自动刷新。

练习

实体的引用解析器方法应该在哪里定义?

关键要点

  • 任何贡献 需要为该实体定义一个引用 方法。每当 需要从另一个子图访问实体的字段时,就会调用此方法。
  • 一个 表示是一个对象, 使用它来表示实体的特定实例。它包含实体的类型及其关键
  • __resolveReference 函数从 接收一个特定 类型的 表示。这个表示包含 需要了解如何填充它所贡献的 的所有数据。

接下来

太棒了!我们的 listingsreviews 服务现在正在协同工作,使用相同的 Listing 。每个 贡献自己的 ,而我们的本地 rover dev 进程启动的 会将响应打包给我们。重点是 本地 这个词;为了让我们的更改真正“生效”(至少在 教程 的意义上),我们需要告诉 关于这些更改!

在下节课中,我们将看看 如何 使用 安全可靠地实现这些更改。

上一节

分享您对本课的疑问和评论

本课程目前处于

测试版
.您的反馈有助于我们改进!如果您遇到问题或感到困惑,请告诉我们,我们会帮助您。所有评论都是公开的,必须遵守 Apollo 行为准则。请注意,已解决或已处理的评论可能会被删除。

您需要一个 GitHub 帐户才能在下方发布评论。没有帐户? 请在我们的 Odyssey 论坛中发布评论。