12. Mutation
10m

概述

中只是一个方面。我们已经看到了特定且表达能力强的查询的强大功能,这些查询可以让我们一次性检索到我们想要的确切数据。

但是,当我们想要实际更改、插入或删除数据时,我们需要使用一个新的工具:

在本课中,我们将

  • 通过更改数据的功能来增强我们的 Schema
  • 探索 语法
  • 了解 input 类型
  • 了解 响应最佳实践
  • 创建一个 Java 记录以保存不可变的响应数据

Airlock 中的 Mutation

继续我们的 Airlock API 中的下一个功能:创建新的房源列表。

A mockup showing a view that lets us create a new listing by specifying its pertinent properties

我们的 REST API 配备了一个端点,允许我们创建新的房源列表: POST /listings

此方法接受我们的请求主体上的 listing 属性。它应该包含与房源列表相关的所有属性,包括其便利设施。房源列表在数据库中创建后,此端点会将新创建的房源列表返回给我们。

好的,现在我们如何在 中启用此功能?

设计 Mutation

Query 类型类似, Mutation 类型充当我们 Schema 的入口点。它遵循我们一直在使用的 的语法。

我们使用 type 关键字声明 Mutation 类型,然后是名称 Mutation。在大括号内,我们有我们的入口点,即我们将用来改变数据的

让我们打开 schema.graphqls 并添加一个新的 Mutation 类型。

schema.graphqls
type Mutation {
}

对于 Mutation,我们建议从一个动词开始,该动词描述我们更新 的具体操作(例如 adddeletecreate),然后是 所作用的数据。

我们将探索如何 创建 一个新的房源列表,因此我们将把这个 称为 createListing

type Mutation {
"Creates a new listing"
createListing: #TODO
}

对于 createListing 的返回值类型 ,我们可以返回 Listing 类型;它是我们希望 作用的对象类型。但是,我们建议遵循一致的 Response 类型来处理 响应。让我们看看在新的类型中它是什么样子的。

注意: 在本课程中,我们在单个 schema.graphqls 文件中定义了我们所有的类型。这并不是必需的;DGS 框架从它在 schema 文件夹中找到的所有 .graphqls 文件构建最终的 。这意味着随着你的 Schema 越来越大,你可以在需要时选择将它拆分成多个文件。

The Mutation 响应类型

对于 Mutation,返回值类型通常从 的名称开始,然后是 PayloadResponse。不要忘记类型名称应该使用 PascalCase 格式!

遵循惯例,我们将我们的类型命名为 CreateListingResponse

type CreateListingResponse {
}

我们应该返回我们正在改变的 (在本例中为 Listing),以便客户端可以访问更新后的对象。

type CreateListingResponse {
listing: Listing
}

注意: 虽然我们的 作用于单个 Listing 对象,但 也可能一次更改并返回多个对象。

请注意, listing 返回一个 Listing 类型,该类型 可以null,因为我们的 可能失败。

为了解释可能发生的任何部分错误并向客户端返回有用的信息,我们可以将一些额外的 包含在响应类型中。

  • code:一个 Int,它指的是响应的状态,类似于 HTTP 状态代码。

  • success:一个 Boolean 标志,指示 负责的所有更新是否成功。

  • message:一个 String,用于在客户端显示有关 结果的信息。如果 mutation 仅部分成功,并且通用错误消息无法说明全部情况,这将特别有用。

让我们也为这些 添加注释,以便使我们的 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
}

最后,我们可以将我们 的返回值类型设置为这个新的 CreateListingResponse 类型,并将其设置为不可为空。以下是 createListing 现在应该的样子:

type Mutation {
"Creates a new listing"
createListing: CreateListingResponse!
}

The Mutation 输入

为了创建新的房源列表,我们的 需要接收一些输入。

让我们来思考一下这个 createListing 会期望什么样的输入。我们需要关于房源本身的所有细节,例如 titlecostPerNightnumOfBedsamenities 等等。

我们之前在 Query.listing 中使用过一个 : 我们传递了一个名为 id 的单个

type Query {
listing(id: ID!): Listing
}

createListing 接受不止一个 。我们可以通过一种方法来解决这个问题,那就是将每个参数逐一添加到我们的 createListing 中。但这种方法会变得笨拙且难以理解。相反,使用 输入类型作为 对于一个 来说是一个好习惯。

探索 input 类型

input 类型中 是一种特殊的 ,它将一组 组合在一起,然后可以用作另一个 的参数。使用 input 类型有助于我们对 进行分组和理解,特别是对于

要定义一个输入类型,使用 input 关键字,后面跟着名称和大括号 ({} )。在大括号内,我们像往常一样列出 和类型。请注意,输入类型的字段只能是 、枚举或另一个输入类型。

input CreateListingInput {
}

接下来,我们将添加属性。为了完善即将创建的房源,让我们发送所有相关信息。我们需要:titledescriptionnumOfBedscostPerNightclosedForBookings

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 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 类型的知识,以及其他 类型和功能,请访问 Side Quest: 中级 Schema 设计

使用 input

要在 Schema 中使用 input 类型,我们可以将其设置为 的类型。例如,我们可以更新 createListing 以使用 CreateListingInput 类型,如下所示:

type Mutation {
"Creates a new listing"
createListing(input: CreateListingInput!): CreateListingResponse!
}

请注意,CreateListingInput 是非空类型。要运行此 ,实际上我们需要一些输入!

构建 ListingService 方法

与其他请求一样,我们将构建一个方法来管理此 REST API 调用,以创建一个新的房源。

回到 datasources/ListingService 中,让我们为这个 添加一个新方法。

datasources/ListingService
public void createListingRequest() {
return client
.post()
}

这一次,因为端点使用 POST 方法,我们将链接 .post() 而不是 .get()

接下来,我们将在线创建我们的目标端点。我们将指定一个 uri/listings ,但在调用 .retrieve() 之前,我们将首先调用 .body() 以指定我们的请求主体。

public void createListingRequest() {
return client
.post()
.uri("/listings")
.body()
}

我们将在稍后讨论将什么传递给请求 body ,但现在让我们先完成这个方法。

它接受一个 listing 类型为 CreateListingInput ,所以让我们先在文件开头导入它。

datasources/ListingService
import com.example.listings.generated.types.CreateListingInput;

注意: 如果你没有看到生成 CreateListingInput 类型的有效导入,请尝试停止并重新启动服务器。

接下来,我们将为我们的方法指定一个返回值类型 ListingModel ,并包含一个名为 listingCreateListingInput 参数。

public ListingModel createListingRequest(CreateListingInput listing) {
// ...method body
}

为了完成我们对 /listings 端点的调用的语法,我们将链接 retrievebody 方法。最后,为了将响应转换为 ListingModel 的实例,我们将 ListingModel.class 传递给最后的 body() 方法调用。

public ListingModel createListingRequest(CreateListingInput listing) {
return client
.post()
.uri("/listings")
.body() // TODO!
.retrieve()
.body(ListingModel.class);
}

现在我们可以回到这个问题:如何 将我们的请求数据发送到端点——换句话说,我们需要传递给 第一个 body() 方法!

定义 CreateListingModel 记录

当通过 POST /listings 端点创建新房源时,我们需要记住几个要求。

  1. 我们知道,此端点在请求主体上查找 listing 属性。
  2. 要将数据作为我们请求的一部分发送,我们不能使用普通的 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 可能会提供一些基于类的样板代码,但让我们立即将我们的类更新为记录。

models/CreateListingModel
package com.example.listings.models;
public record CreateListingModel() { }

要使用具有 listing 属性(类型为 CreateListingInput)的记录,我们只需要将属性和类型传递到记录名称后的括号中。(不要忘记导入 CreateListingInput!)

models/CreateListingModel
import com.example.listings.generated.types.CreateListingInput;
public record CreateListingModel(CreateListingInput listing) { }

现在我们能够动态创建新记录——我们可以传递 CreateListingInput,它们会自动创建 listing 属性来保存数据。

让我们回到我们的 ListingService 方法并使用这个新的记录。在文件顶部,导入 CreateListingModel

datasources/ListingService
import com.example.listings.models.CreateListingModel;

然后在我们的 createListingRequest 方法中,在 POST 请求之前,我们将创建一个 CreateListingModel 记录并将我们的方法接收到的 listing 传递进去。

datasources/ListingService
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

datasources/ListingService
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 输入类型来提高清晰度和可维护性。

接下来

在最后一课中,我们将把这些点连接起来,使我们的 能够完全正常工作——并创建一些新的 listings!

上一个

分享您关于本课的问题和评论

本课程目前处于

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

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