概述
查询 在 GraphQL 中只是一个方面。我们已经看到了特定且表达能力强的查询的强大功能,这些查询可以让我们一次性检索到我们想要的确切数据。
但是,当我们想要实际更改、插入或删除数据时,我们需要使用一个新的工具: GraphQL mutation。
在本课中,我们将
- 通过更改数据的功能来增强我们的 Schema
- 探索 mutation 语法
- 了解 GraphQL
input
类型 - 了解 mutation 响应最佳实践
- 创建一个 Java 记录以保存不可变的响应数据
Airlock 中的 Mutation
继续我们的 Airlock API 中的下一个功能:创建新的房源列表。
我们的 REST API 配备了一个端点,允许我们创建新的房源列表: POST /listings
。
此方法接受我们的请求主体上的 listing
属性。它应该包含与房源列表相关的所有属性,包括其便利设施。房源列表在数据库中创建后,此端点会将新创建的房源列表返回给我们。
好的,现在我们如何在 GraphQL 中启用此功能?
设计 Mutation
与 Query
类型类似, Mutation
类型充当我们 Schema 的入口点。它遵循我们一直在使用的 schema 定义语言 或 SDL 的语法。
我们使用 type
关键字声明 Mutation
类型,然后是名称 Mutation
。在大括号内,我们有我们的入口点,即我们将用来改变数据的 字段。
让我们打开 schema.graphqls
并添加一个新的 Mutation
类型。
type Mutation {}
对于 字段 的 Mutation
,我们建议从一个动词开始,该动词描述我们更新 操作 的具体操作(例如 add
、delete
或 create
),然后是 mutation 所作用的数据。
我们将探索如何 创建 一个新的房源列表,因此我们将把这个 mutation 称为 createListing
。
type Mutation {"Creates a new listing"createListing: #TODO}
对于 createListing
的返回值类型 mutation,我们可以返回 Listing
类型;它是我们希望 mutation 作用的对象类型。但是,我们建议遵循一致的 Response
类型来处理 mutation 响应。让我们看看在新的类型中它是什么样子的。
注意: 在本课程中,我们在单个 schema.graphqls
文件中定义了我们所有的类型。这并不是必需的;DGS 框架从它在 schema
文件夹中找到的所有 .graphqls
文件构建最终的 GraphQL schema。这意味着随着你的 Schema 越来越大,你可以在需要时选择将它拆分成多个文件。
The Mutation
响应类型
对于 Mutation
的 字段,返回值类型通常从 mutation 的名称开始,然后是 Payload
或 Response
。不要忘记类型名称应该使用 PascalCase
格式!
遵循惯例,我们将我们的类型命名为 CreateListingResponse
。
type CreateListingResponse {}
我们应该返回我们正在改变的 对象类型(在本例中为 Listing
),以便客户端可以访问更新后的对象。
type CreateListingResponse {listing: Listing}
注意: 虽然我们的 mutation 作用于单个 Listing
对象,但 mutation 也可能一次更改并返回多个对象。
请注意, listing
字段 返回一个 Listing
类型,该类型 可以 为 null
,因为我们的 mutation 可能失败。
为了解释可能发生的任何部分错误并向客户端返回有用的信息,我们可以将一些额外的 字段 包含在响应类型中。
code
:一个Int
,它指的是响应的状态,类似于 HTTP 状态代码。success
:一个Boolean
标志,指示 mutation 负责的所有更新是否成功。message
:一个String
,用于在客户端显示有关 mutation 结果的信息。如果 mutation 仅部分成功,并且通用错误消息无法说明全部情况,这将特别有用。
让我们也为这些 字段 添加注释,以便使我们的 GraphQL API 文档更有用。
type CreateListingResponse {"Similar to HTTP status code, represents the status of the mutation"code: Int!"Indicates whether the mutation was successful"success: Boolean!"Human-readable message for the UI"message: String!"The newly created listing"listing: Listing}
最后,我们可以将我们 mutation 的返回值类型设置为这个新的 CreateListingResponse
类型,并将其设置为不可为空。以下是 createListing
的 mutation 现在应该的样子:
type Mutation {"Creates a new listing"createListing: CreateListingResponse!}
The Mutation
输入
为了创建新的房源列表,我们的 mutation 需要接收一些输入。
让我们来思考一下这个 createListing
mutation 会期望什么样的输入。我们需要关于房源本身的所有细节,例如 title
、costPerNight
、numOfBeds
、amenities
等等。
我们之前在 Query.listing
field 中使用过一个 GraphQL argument : 我们传递了一个名为 id
的单个 argument 。
type Query {listing(id: ID!): Listing}
但 createListing
接受不止一个 argument 。我们可以通过一种方法来解决这个问题,那就是将每个参数逐一添加到我们的 createListing
mutation 中。但这种方法会变得笨拙且难以理解。相反,使用 GraphQL 输入类型作为 arguments 对于一个 field 来说是一个好习惯。
探索 input
类型
在 input
类型中 GraphQL schema 是一种特殊的 object type ,它将一组 arguments 组合在一起,然后可以用作另一个 field 的参数。使用 input
类型有助于我们对 arguments 进行分组和理解,特别是对于 mutations 。
要定义一个输入类型,使用 input
关键字,后面跟着名称和大括号 ({}
)。在大括号内,我们像往常一样列出 fields 和类型。请注意,输入类型的字段只能是 scalar 、枚举或另一个输入类型。
input CreateListingInput {}
接下来,我们将添加属性。为了完善即将创建的房源,让我们发送所有相关信息。我们需要:title
、description
、numOfBeds
、costPerNight
和 closedForBookings
。
input CreateListingInput {"The listing's title"title: String!"The listing's description"description: String!"The number of beds available"numOfBeds: Int!"The cost per night"costPerNight: Float!"Indicates whether listing is closed for bookings (on hiatus)"closedForBookings: Boolean}
我们还需要指定房源提供的 amenities
。请注意,我们在创建新房源的模型中预定义了便利设施选项;我们只能从现有选项中选择。在幕后,每个便利设施都有一个唯一的 ID
标识符。因此,我们将添加一个 amenityIds
field 到 CreateListingInput
中,类型为 [ID!]!
。
input CreateListingInput {"The listing's title"title: String!"The listing's description"description: String!"The number of beds available"numOfBeds: Int!"The cost per night"costPerNight: Float!"Indicates whether listing is closed for bookings (on hiatus)"closedForBookings: Boolean"The Listing's amenities"amenities: [ID!]!}
注意: 你可以了解更多关于 input
类型的知识,以及其他 GraphQL 类型和功能,请访问 Side Quest: 中级 Schema 设计 。
使用 input
要在 Schema 中使用 input
类型,我们可以将其设置为 field argument 的类型。例如,我们可以更新 createListing
mutation 以使用 CreateListingInput
类型,如下所示:
type Mutation {"Creates a new listing"createListing(input: CreateListingInput!): CreateListingResponse!}
请注意,CreateListingInput
是非空类型。要运行此 mutation ,实际上我们需要一些输入!
构建 ListingService
方法
与其他请求一样,我们将构建一个方法来管理此 REST API 调用,以创建一个新的房源。
回到 datasources/ListingService
中,让我们为这个 mutation 添加一个新方法。
public void createListingRequest() {return client.post()}
这一次,因为端点使用 POST
方法,我们将链接 .post()
而不是 .get()
。
接下来,我们将在线创建我们的目标端点。我们将指定一个 uri
为 /listings
,但在调用 .retrieve()
之前,我们将首先调用 .body()
以指定我们的请求主体。
public void createListingRequest() {return client.post().uri("/listings").body()}
我们将在稍后讨论将什么传递给请求 body
,但现在让我们先完成这个方法。
它接受一个 listing
argument 类型为 CreateListingInput
,所以让我们先在文件开头导入它。
import com.example.listings.generated.types.CreateListingInput;
注意: 如果你没有看到生成 CreateListingInput
类型的有效导入,请尝试停止并重新启动服务器。
接下来,我们将为我们的方法指定一个返回值类型 ListingModel
,并包含一个名为 listing
的 CreateListingInput
参数。
public ListingModel createListingRequest(CreateListingInput listing) {// ...method body}
为了完成我们对 /listings
端点的调用的语法,我们将链接 retrieve
和 body
方法。最后,为了将响应转换为 ListingModel
的实例,我们将 ListingModel.class
传递给最后的 body()
方法调用。
public ListingModel createListingRequest(CreateListingInput listing) {return client.post().uri("/listings").body() // TODO!.retrieve().body(ListingModel.class);}
现在我们可以回到这个问题:如何 将我们的请求数据发送到端点——换句话说,我们需要传递给 第一个 body()
方法!
定义 CreateListingModel
记录
当通过 POST /listings
端点创建新房源时,我们需要记住几个要求。
- 我们知道,此端点在请求主体上查找
listing
属性。 - 要将数据作为我们请求的一部分发送,我们不能使用普通的 Java 类;我们需要将其序列化以便作为 HTTP 请求的一部分发送。
让我们考虑第一个要求。我们的方法接收一个 CreateListingInput
类型,其中包含要创建的房源的所有详细信息;这意味着它包含发布到 /listings
端点所需的所有信息,但我们需要 所有 这些数据都包含在请求主体上名为 listing
的属性中。
我们可以创建一个新类,为其赋予一个 listing
属性,并设置一个 setter 方法,如下所示。
public class CreateListing {CreateListingInput listing;public void setListing(CreateListingInput listing) {this.listing = listing;}}
但这比我们需要的多一些样板代码;毕竟,我们真正想要做的只是 设置 一个 listing
属性一次,将其用作请求主体的一部分,并且不再更改它。这意味着我们可以节省一些时间和代码,并使用 Java 记录来代替!
在 models
目录中,创建一个名为 CreateListingModel
的新文件。
📂 com.example.listings┣ 📂 datafetchers┣ 📂 models┃ ┣ 📄 CreateListingModel┃ ┗ 📄 ListingModel┣ 📄 ListingsApplication┣ 📄 WebConfiguration
默认情况下,您的 IDE 可能会提供一些基于类的样板代码,但让我们立即将我们的类更新为记录。
package com.example.listings.models;public record CreateListingModel() { }
要使用具有 listing
属性(类型为 CreateListingInput
)的记录,我们只需要将属性和类型传递到记录名称后的括号中。(不要忘记导入 CreateListingInput
!)
import com.example.listings.generated.types.CreateListingInput;public record CreateListingModel(CreateListingInput listing) { }
现在我们能够动态创建新记录——我们可以传递 CreateListingInput
,它们会自动创建 listing
属性来保存数据。
让我们回到我们的 ListingService
方法并使用这个新的记录。在文件顶部,导入 CreateListingModel
。
import com.example.listings.models.CreateListingModel;
然后在我们的 createListingRequest
方法中,在 POST
请求之前,我们将创建一个 CreateListingModel
记录并将我们的方法接收到的 listing
传递进去。
public ListingModel createListingRequest(CreateListingInput listing) {new CreateListingModel(listing);// "/listings" post request}
现在,我们可以考虑我们的第二个要求。为了将新记录的内容与我们的请求一起作为其主体发送,我们需要先序列化它。序列化将 Java 类或记录准备为 HTTP 请求的一部分。
序列化我们的 listing 输入
我们已经使用了 Jackson 的 ObjectMapper
将 JSON 对象转换为 Java 类;现在我们需要从 Java 类转换为 JSON 对象。Spring 为我们提供了一个名为 MappingJacksonValue
的实用程序来实现这一点。
在文件顶部,从 Spring Framework 导入 MappingJacksonValue
。
import org.springframework.http.converter.json.MappingJacksonValue;
在 createListingRequest
方法中,我们将创建一个新的 MappingJacksonValue
实例,并将我们的记录传递给它。我们将结果称为 serializedListing
。
MappingJacksonValue serializedListing = new MappingJacksonValue(new CreateListingModel(listing));
好了——这个方法的最后一步!我们将把这个 serializedListing
作为 POST 请求的主体传递。
public ListingModel createListingRequest(CreateListingInput listing) {MappingJacksonValue serializedListing = new MappingJacksonValue(new CreateListingModel(listing));return client.post().uri("/listings").body(serializedListing).retrieve().body(ListingModel.class);
到此为止,我们就完成了对端点的调用——现在是时候在我们的数据提取器中使用它了。
练习
CreateListingResponse
)中,为什么修改后的对象的返回类型(Listing
)是可空的?input
类型?主要要点
- 变异 是用于修改数据的写 操作。
- 命名 变异 通常以描述动作的动词开头,例如 "add"、"delete" 或 "create"。
- 一个常见的约定是为 变异 响应创建一致的响应类型。
- 变异 在 GraphQL 中通常需要多个 参数 来执行操作。为了将参数分组在一起,我们使用 GraphQL 输入类型来提高清晰度和可维护性。
接下来
在最后一课中,我们将把这些点连接起来,使我们的 变异 操作 能够完全正常工作——并创建一些新的 listings!
分享您关于本课的问题和评论
本课程目前处于
您需要一个 GitHub 帐户才能在下方发帖。没有吗? 请在我们的 Odyssey 论坛上发帖。