65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Spring Boot 创建基于 GraphQL 的 REST API 应用程序

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (1投票)

2022 年 7 月 14 日

MIT

15分钟阅读

viewsIcon

12141

downloadIcon

121

本教程将讨论如何创建一个支持 GraphQL 查询的 REST 服务。

引言

经过漫长的停滞期,Hanbo 带着爆炸性的回归!最近,我终于拾起了放下的地方,决定做一个关于 GraphQL 的教程。我想构建一个可以利用 GraphQL 查询和检索数据对象的示例 Web 应用程序。说实话,我对 GraphQL 了解不多。我只知道可以使用 Spring Boot 来创建支持 GraphQL 的应用程序。我认为现在是学习和评估这个新概念的最佳时机,并看看

  • 创建这样的应用程序有多难
  • 这个新概念有多强大
  • 这对我的未来项目有多大用处

刚开始的时候很糟糕。我以为我会找到一些示例并复制过程会给我一些启示。在我找到的两个示例中,一个是无法编译的,另一个只有代码的截图。这使得学习有些困难。整个过程是值得的。最终的产品效果相当不错。我将在一个非常详细的教程中分享我学到的东西,我保证代码将按预期编译和运行。

为了免责声明,我不是这方面的专家。本教程仅作为我追求更多知识的起点。本教程不能保证能解决读者可能遇到的具体问题。当然,本教程是相当高级的。请尽力跟上。

整体架构

示例应用程序是一个基于 RESTFul API 的应用程序。它将处理两种不同的请求。两种请求都必须是 HTTP POST 请求,并带有请求体。请求体包含纯文本内容,指定了可以被示例应用程序解析的 GraphQL 查询,并返回响应。

使用 Spring Boot 创建示例应用程序非常简单。难点在于将 GraphQL 功能集成到此类应用程序中。与其他概念或技术一样,与 Spring Boot 的集成通过 GraphQL 可以轻松实现。有用于 GraphQL 的 MVC 入门库可以使用。这不是难点。

难点在于使此应用程序正常工作所需的所有配置和代码更改。首先是用于数据类型和查询语法的 GraphQL 定义。它将是一个打包在 jar 文件中的资源。然后我需要三个不同的类,一个用于处理客户端调用的控制器,一个用于创建 GraphQL 数据检索服务的服务层对象。以及用于创建服务对象使用的数据获取器工厂。

我将首先解释我必须在 Maven POM 文件中包含的附加 jar,以便为我的应用程序赋予 GraphQL 功能。这一步将在下一节讨论。

Maven POM 文件

我采用了一个标准的 Maven POM 文件用于 RESTFul 项目,并对其进行了修改以适应这个新应用程序。为了使其支持 GraphQL 查询,我只需要在 dependencies 部分添加一些依赖 jar。这些是我添加的附加 jar

<dependencies>
   <dependency>
      <groupId>com.graphql-java</groupId>
      <artifactId>graphql-java</artifactId>
      <version>18.2</version>
   </dependency>
   <dependency>
      <groupId>com.graphql-java</groupId>
      <artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
      <version>2021-10-25T04-50-54-fbc162f</version>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
</dependencies> 

组 ID 为 "com.graphql-java" 的两个 jar 是我这个项目需要的新 jar。

接下来,我将讨论查询类型定义文件,以及它是如何使用的。

GraphQL 查询类型定义文件

GraphQL 查询类型定义服务于两个不同的目的

  • 它将 Java 中定义的数据对象类型(POJO 类型)映射到 GraphQL 可以理解的类型。这允许 GraphQL 操作查询结果。
  • 它定义了查询的语法,以便 GrapQL 可以解析查询并执行,然后检索和操作结果以获得所需的字段。

我只需要在 <project folder>/src/main/java/resources 目录下创建一个名为 "schema.graphql" 的文件。您可以选择任何您想要的文件名,因为此文件将在 Java 代码中使用,以便定位和加载,只要您知道文件名,就可以将其传递给代码进行处理。

这是我的 "schema.graphql" 文件内容

type Query {
   userProfiles: [ UserProfile ]
   userProfilesByAge(age: Int): [ UserProfile ]
}

type UserCredential {
   userId: ID!
   userCredential: String!
}

type UserAddress {
   userId: ID!
   userAddressLine1: String!
   userAddressLine2: String
   userCity: String!
   userState: String!
   userZipCode: String!
}

type UserDetail {
   userId: ID!
   userAge: Int!
   userFirstName: String!
   userLastName: String!
   userGender: String
}

type UserProfile {
   userId: ID!
   userName: String!
   userEmail: String!
   userActive: Boolean!
   userDetail: UserDetail
   userCredential: UserCredential
   userAddress: UserAddress
}

内容不难理解。第一个 "type" 称为 Query,其中包含两个不同的查询。一个不接受任何输入参数,它将返回集合中的所有对象。第二个接受一个名为 "age" 的输入参数。它将返回集合中具有相同年龄的任何对象。我们的集合是一个用户配置文件列表,并存储在内存中。每个配置文件都有用户 ID、用户名、用户电子邮件和一个表示用户是否活动的标志。然后有三个子类型,每个类型包含有关这些用户的更多详细信息。这些就是该文件中的所有其他类型。它们与 Java 类型完全匹配。

例如,上面显示的 "UserCredential" 类型定义与 Java 类 org.hanbo.boot.rest.models.UserCredential 完全匹配。这是 GraphQL 代码

type UserCredential {
   userId: ID!
   userCredential: String!
}

这是同一类的 Java 代码

package org.hanbo.boot.rest.models;

public class UserCredential
{
   private String userId;
   
   private String userCredential;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserCredential()
   {
      return userCredential;
   }

   public void setUserCredential(String userCredential)
   {
      this.userCredential = userCredential;
   }
}

这是 "UserAddress" 类型的 GraphQL 类型定义

type UserAddress {
   userId: ID!
   userAddressLine1: String!
   userAddressLine2: String
   userCity: String!
   userState: String!
   userZipCode: String!
}

这是同一类型的 Java 类

package org.hanbo.boot.rest.models;

public class UserAddress
{
   private String userId;
   
   private String userAddressLine1;
   
   private String userAddressLine2;
   
   private String userCity;
   
   private String userState;
   
   private String userZipCode;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserAddressLine1()
   {
      return userAddressLine1;
   }

   public void setUserAddressLine1(String userAddressLine1)
   {
      this.userAddressLine1 = userAddressLine1;
   }

   public String getUserAddressLine2()
   {
      return userAddressLine2;
   }

   public void setUserAddressLine2(String userAddressLine2)
   {
      this.userAddressLine2 = userAddressLine2;
   }

   public String getUserCity()
   {
      return userCity;
   }

   public void setUserCity(String userCity)
   {
      this.userCity = userCity;
   }

   public String getUserState()
   {
      return userState;
   }

   public void setUserState(String userState)
   {
      this.userState = userState;
   }

   public String getUserZipCode()
   {
      return userZipCode;
   }

   public void setUserZipCode(String userZipCode)
   {
      this.userZipCode = userZipCode;
   }
}

这是 "UserDetail" 类型的 GraphQL 类型定义

type UserDetail {
   userId: ID!
   userAge: Int!
   userFirstName: String!
   userLastName: String!
   userGender: String
}

这是同一类型的 Java 类

package org.hanbo.boot.rest.models;

public class UserDetail
{
   private String userId;
   
   private String userFirstName;
   
   private String userLastName;
   
   private String userGender;
   
   private int userAge;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserFirstName()
   {
      return userFirstName;
   }

   public void setUserFirstName(String userFirstName)
   {
      this.userFirstName = userFirstName;
   }

   public String getUserLastName()
   {
      return userLastName;
   }

   public void setUserLastName(String userLastName)
   {
      this.userLastName = userLastName;
   }

   public String getUserGender()
   {
      return userGender;
   }

   public void setUserGender(String userGender)
   {
      this.userGender = userGender;
   }

   public int getUserAge()
   {
      return userAge;
   }

   public void setUserAge(int userAge)
   {
      this.userAge = userAge;
   }
}

最后,我有了 UserProfile 数据类型,它将所有其他三种类型组合成一个复合数据类型。这是 GraphQL 的类型定义

type UserProfile {
   userId: ID!
   userName: String!
   userEmail: String!
   userActive: Boolean!
   userDetail: UserDetail
   userCredential: UserCredential
   userAddress: UserAddress
}

前四个属性是基本类型,最后三个是我上面已经展示过的类型。而这个等效的 Java 类类型显示为

package org.hanbo.boot.rest.models;

public class UserProfile
{
   private String userId;
   
   private String userName;
   
   private String userEmail;
   
   private boolean userActive;
   
   private UserAddress userAddress;
   
   private UserCredential userCredential;
   
   private UserDetail userDetail;

   public String getUserId()
   {
      return userId;
   }

   public void setUserId(String userId)
   {
      this.userId = userId;
   }

   public String getUserName()
   {
      return userName;
   }

   public void setUserName(String userName)
   {
      this.userName = userName;
   }

   public String getUserEmail()
   {
      return userEmail;
   }

   public void setUserEmail(String userEmail)
   {
      this.userEmail = userEmail;
   }

   public boolean isUserActive()
   {
      return userActive;
   }

   public void setUserActive(boolean userActive)
   {
      this.userActive = userActive;
   }

   public UserAddress getUserAddress()
   {
      return userAddress;
   }

   public void setUserAddress(UserAddress userAddress)
   {
      this.userAddress = userAddress;
   }

   public UserCredential getUserCredential()
   {
      return userCredential;
   }

   public void setUserCredential(UserCredential userCredential)
   {
      this.userCredential = userCredential;
   }

   public UserDetail getUserDetail()
   {
      return userDetail;
   }

   public void setUserDetail(UserDetail userDetail)
   {
      this.userDetail = userDetail;
   }
}

现在我们知道了如何使用 GraphQL 定义查询类型,文件放在哪里,以及如何映射 GraphQL 和 POJOs 之间的数据类型,是时候深入研究整合 GraphQL 和 Spring Boot RESTFul API 到一种新型 Web 应用程序的设计了。

Java 应用程序设计

在接下来的三个子节中,我将解释我如何将整合 GraphQL 到我的 RESTFul API 应用程序所需的各个部分组合在一起。为什么是三个部分?很容易将应用程序设计分解成三个不同的组件。每个组件都有特定的用途。我将按自底向上的顺序介绍这三个组件。第一个是一个工厂类,它将提供两个独立的 DataFetcher 对象。数据获取器对象是执行请求中的查询并返回匹配数据元素的集合的对象。

数据获取器工厂

GraphQL 不知道应用程序应该执行哪种检索,因此它提供了一个接口,像我这样的开发人员可以实现该接口来指定确切的逻辑。我可以通过实现 DataFetcher 接口来做到这一点。对于这个示例应用程序,我定义了两个 DataFetcher 对象。一个用于检索集合的所有元素。另一个用于查找所有年龄等于查询中年龄值的用户配置文件。

我将两个 DataFetcher 对象的创建打包到一个工厂类中。让我们看一下这个类的完整源代码,然后我们可以深入了解它们是如何创建的。这是类的定义

package org.hanbo.boot.rest.repos;

import java.util.List;
import java.util.stream.Collectors;

import org.hanbo.boot.rest.models.UserProfile;
import org.springframework.stereotype.Component;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;

@Component
public class AllUserProfilesFetchersFactory
{
   private DummyDataBank _dataBank;
   
   public AllUserProfilesFetchersFactory(DummyDataBank dataBank)
   {
      _dataBank = dataBank;
   }
   
   public DataFetcher<List<UserProfile>> dataFetcherForAllUserProfiles()
   {
      DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
         @Override
         public List<UserProfile> get(DataFetchingEnvironment environment) {
            System.out.println("data fetcher for all user profiles retrieval.");
            return _dataBank.getAllUserProfiles();
         }
     };
     
     return retVal;
   }
   
   public DataFetcher<List<UserProfile>> dataFetcherForUserProfilesByAge()
   {
      DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
         @Override
         public List<UserProfile> get(DataFetchingEnvironment environment) {
            int expectedAge = (int)environment.getArgument("age");
            System.out.println("data fetcher for user profiles 
                                based by age retrieval. Age: " + expectedAge);
            List<UserProfile> retVal = 
            _dataBank.getAllUserProfiles()
                     .stream()
                     .filter( x -> x.getUserDetail() != null && 
                              x.getUserDetail().getUserAge() == expectedAge)
                     .collect(Collectors.toList());
            return retVal;
         }
     };
     
     return retVal;
   }
}

这个类有两个方法,第一个是创建一个 DataFetcher 对象,GraphQL 可以用它来处理我们正在查询的集合中所有 UserProfile 对象的完整列表。下面是代码

public DataFetcher<List<UserProfile>> dataFetcherForAllUserProfiles()
{
   DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
      @Override
      public List<UserProfile> get(DataFetchingEnvironment environment) {
         System.out.println("data fetcher for all user profiles retrieval.");
         return _dataBank.getAllUserProfiles();
      }
   };
   
   return retVal;
}

在这个方法中,我创建了一个匿名对象,类型为 DataFetcher。它实现了 DataFetcher 接口,我只需要实现 get() 方法。它返回一个 UserProfile 类型的对象列表。对于这个,我喜欢返回我数据银行对象中的所有对象。然后 GraphQL 将处理列表并返回一个对象列表,该列表只包含用户指定要返回的属性。

第二个 DataFetcher 对象与我刚刚审查的那个类似。它有一个额外功能。查询需要一个名为 "age" 的输入参数。这个新的 DataFetcher 对象应该能够检索参数并将其用于实际查询。这是通过使用传递到 get() 方法的 DataFetchingEnvironment 参数来完成的。DataFetchingEnvironment 对象有一个名为 getArgument() 的方法,该方法可以返回查询文本中参数的值。它以 Object 类型存储,因此我需要通过显式转换进行转换。下面是代码

public DataFetcher<List<UserProfile>> dataFetcherForUserProfilesByAge()
{
   DataFetcher<List<UserProfile>> retVal = new DataFetcher<List<UserProfile>>() {
      @Override
      public List<UserProfile> get(DataFetchingEnvironment environment) {
         int expectedAge = (int)environment.getArgument("age");
         System.out.println("data fetcher for user profiles 
                             based by age retrieval. Age: " + expectedAge);
         List<UserProfile> retVal = 
         _dataBank.getAllUserProfiles()
            .stream()
            .filter( x -> x.getUserDetail() != null && 
                     x.getUserDetail().getUserAge() == expectedAge)
            .collect(Collectors.toList());
         return retVal;
      }
   };
   
   return retVal;
}

两个类使用的 get() 方法使用流并查询集合。这应该很容易理解。它类似于 C# Linq 语法。

下一节将是本教程最重要的部分。在其中,我将展示如何配置 GraphQL,以便稍后可以由 Rest Controller 使用。

GraphQL 服务

严格来说它不是一个服务,我创建这个服务是因为我习惯这样做。以下代码实际上是初始化 GraphQL 对象,让 GraphQL 解析我创建的模式文件,并配置两个 DataFetcher 对象来处理查询。这是我创建的服务对象类型

package org.hanbo.boot.rest.services;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import javax.annotation.PostConstruct;

import org.hanbo.boot.rest.repos.AllUserProfilesFetchersFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

@Service
public class GraphQLProvider
{
   private GraphQL _graphQL;
   
   private AllUserProfilesFetchersFactory _dataFetcherFactory;
   
   public GraphQLProvider(AllUserProfilesFetchersFactory dataFetcherFactory)
   {
      _dataFetcherFactory = dataFetcherFactory;
   }
   
   @Bean
   public GraphQL graphQL() {
       return _graphQL;
   }

   @PostConstruct
   public void init() throws IOException
   {
      ClassPathResource resLoader = new ClassPathResource("schema.graphql");
      InputStream inStr = resLoader.getInputStream();
      Reader typeReader = new InputStreamReader(inStr);
      
      TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(typeReader);
      RuntimeWiring runtimeWiring = buildWiring();
      SchemaGenerator schemaGenerator = new SchemaGenerator();
      GraphQLSchema schema = schemaGenerator.makeExecutableSchema
                             (typeRegistry, runtimeWiring);
   
      _graphQL = GraphQL.newGraphQL(schema).build();
   }

   private RuntimeWiring buildWiring() {
      return RuntimeWiring.newRuntimeWiring()
            .type("Query", typeWiring->typeWiring.dataFetcher
            ("userProfiles", _dataFetcherFactory.dataFetcherForAllUserProfiles()))
            .type("Query", typeWiring->typeWiring.dataFetcher
            ("userProfilesByAge", _dataFetcherFactory.
              dataFetcherForUserProfilesByAge())).build();
   }
}

让我逐个解释。第一部分是

private AllUserProfilesFetchersFactory _dataFetcherFactory;

public GraphQLProvider(AllUserProfilesFetchersFactory dataFetcherFactory)
{
   _dataFetcherFactory = dataFetcherFactory;
}

我将 AllUserProfilesFetchersFactory 对象声明为该类的一部分。然后构造函数将执行依赖注入来实例化这个对象。它可以在类的后续部分中使用。下一部分是这个 init() 方法

@PostConstruct
public void init() throws IOException
{
   ClassPathResource resLoader = new ClassPathResource("schema.graphql");
   InputStream inStr = resLoader.getInputStream();
   Reader typeReader = new InputStreamReader(inStr);
   
   TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(typeReader);
   RuntimeWiring runtimeWiring = buildWiring();
   SchemaGenerator schemaGenerator = new SchemaGenerator();
   GraphQLSchema schema = schemaGenerator.makeExecutableSchema
                          (typeRegistry, runtimeWiring);

   _graphQL = GraphQL.newGraphQL(schema).build();
}

首先,我使用了 @PostConstruct 注解。这个注解的酷之处在于,这个方法将在构造函数调用后自动调用。这个注解不是 Spring 的一部分,而是 Java 语言的一部分。在这个方法中,我将首先传入 schema.graphql 文件的类路径来构造一个 ClassPathResource 构造函数并实例化资源加载器。然后我创建一个 InputStream 对象和一个 InputStreamReader 对象

ClassPathResource resLoader = new ClassPathResource("schema.graphql");
InputStream inStr = resLoader.getInputStream();
Reader typeReader = new InputStreamReader(inStr);

下一部分是创建一个 GrapQLSchema 对象。首先,我必须创建一个 TypeDefinitionRegistry 类型的对象。然后,我必须创建一个 RuntimeWiring 类型的对象。这是通过调用 buildWiring() 方法完成的。buildWiring() 是我用于将我的两个 DataFetcher 对象与 GraphQL 关联的方法。最后,我创建一个 SchemaGenerator 类型的对象,并使用它来创建一个 GrapQLSchema 对象

TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(typeReader);
RuntimeWiring runtimeWiring = buildWiring();
SchemaGenerator schemaGenerator = new SchemaGenerator();
GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);

这是我的 buildWiring() 方法。这是我将模式与两个 DataFetcher 对象关联的方式。如果检查我的模式,会有一个名为 "Query" 的类型,其中有两个不同的查询。一个名为 "userProfiles";另一个名为 "userProfilesByAge"

private RuntimeWiring buildWiring() {
   return RuntimeWiring.newRuntimeWiring()
         .type("Query", typeWiring->typeWiring.dataFetcher
         ("userProfiles", _dataFetcherFactory.dataFetcherForAllUserProfiles()))
         .type("Query", typeWiring->typeWiring.dataFetcher
         ("userProfilesByAge", 
           _dataFetcherFactory.dataFetcherForUserProfilesByAge())).build();
}

这很简单。但是,通过阅读这两个教程并弄清楚组件如何工作来将所有这些组合在一起有些困难。最后,我将向您展示如何使用此服务对象来处理查询。所有这些都在 Rest API 控制器中完成。我将在下一节中介绍。

Rest API 控制器

现在教程中最重要的部分已经完成,是时候使用了。Rest API 控制器非常简单

package org.hanbo.boot.rest.controllers;

import java.io.IOException;
import java.util.Map;

import org.hanbo.boot.rest.services.GraphQLProvider;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.ObjectMapper;

import graphql.ExecutionResult;

@RestController
public class SampleGraphQLApiController
{
   private GraphQLProvider _queryProvider;
   
   public SampleGraphQLApiController(GraphQLProvider queryProvider)
   {
      _queryProvider = queryProvider;
   }
   
   @PostMapping(value="/api/allUserProfiles")
   public String allUserProfiles(
      @RequestBody
      String queryVal
   ) throws IOException
   {
      ExecutionResult result = _queryProvider.graphQL().execute(queryVal);
      Map<String, Object> resp = (Map<String, Object>) result.getData();
      String retVal = new ObjectMapper().writeValueAsString(resp);
      
      return retVal;
   }
}

这个类的构造函数用于自动注入我在上一节中展示的服务对象。只有一个其他方法,即处理用户请求的操作方法。在详细介绍之前,我想解释一下请求是什么样的。GraphQL 使用特殊语法。所以它期望的不是 HTML、XML 或 JSON。我选择使用纯文本作为请求介质类型。请求内容是一个简单的字符串。

该方法只有四行代码。第一行是调用我的服务对象返回一个 GraphQL 类型的对象。然后调用其 execute() 方法,该方法将解析 string 并使用其中一个 DataFetcher 对象来实际查询我的 UserProfile 集合。对于每个结果用户配置文件,将进行其他处理以仅选择用户定义的字段返回。因此,用户配置文件对象将不是所有属性的完整列表,并且将大大简化。这些对象是键为 string、值为 ObjectMap 对象。一旦得到这些,我将使用 Jackson 框架将对象转换为 JSON 对象并返回给用户。

以上就是这个示例应用程序的 Java 代码的全部内容。现在我们已经了解了所有的技术细节。是时候进行测试,看看应用程序实际是如何工作的了。

如何运行示例应用程序

首先,请在命令行上使用以下命令构建应用程序

mvn clean install

构建成功后(并且一定会成功),您可以使用以下命令运行应用程序

java -jar <project folder>/target/hanbo-graphql-restapi-1.0.1.jar

这是 RESTFul API 的 URL

https://:8080/api/allUserProfiles

您可以使用 PostMan 或任何其他 RESTFul 测试应用程序进行测试。我使用了 Advanced REST Client (ARC) 进行测试。它不像 PostMan 那么高级,但也是一个不错的替代品。这是我的桌面上运行的 Advanced REST Client (ARC) 的截图

在实际测试查询之前,我想分享一下数据集合。在这个数据集合中,列表中只有一个用户配置文件对象

package org.hanbo.boot.rest.repos;

import java.util.ArrayList;
import java.util.List;

import org.hanbo.boot.rest.models.UserAddress;
import org.hanbo.boot.rest.models.UserCredential;
import org.hanbo.boot.rest.models.UserDetail;
import org.hanbo.boot.rest.models.UserProfile;
import org.springframework.stereotype.Component;

@Component
public class DummyDataBank
{
   private static List<UserProfile> mockUserProfiles;
   
   static
   {
      mockUserProfiles = new ArrayList<UserProfile>();
      
      UserProfile profileToAdd = new UserProfile();
      
      profileToAdd.setUserId("00000001");
      profileToAdd.setUserName("testUser1");
      profileToAdd.setUserEmail("testUser1@test.org");
      profileToAdd.setUserActive(true);

      UserAddress userAddress = new UserAddress();
      userAddress.setUserAddressLine1("123 Main St.");
      userAddress.setUserAddressLine2("");
      userAddress.setUserCity("Kalamazoo");
      userAddress.setUserId("00000001");
      profileToAdd.setUserAddress(userAddress);
      
      UserDetail userDetail = new UserDetail();
      userDetail.setUserAge(35);
      userDetail.setUserFirstName("Lee");
      userDetail.setUserLastName("Junkie");
      userDetail.setUserGender("M");
      userDetail.setUserId("00000001");
      profileToAdd.setUserDetail(userDetail);

      UserCredential userCredential = new UserCredential();
      userCredential.setUserCredential("xxxxyyyyxxxxyyyy");
      userCredential.setUserId("00000001");
      profileToAdd.setUserCredential(userCredential);
      
      mockUserProfiles.add(profileToAdd);
   }
   
   public List<UserProfile> getAllUserProfiles()
   {
      return mockUserProfiles;
   }
}

这个 UserProfile 类型并非极其复杂。在实际示例中,一个复杂对象可以有层层数据,在极端情况下,单个对象的数据大小可能超过 100MB,甚至更大。

当我们检索这样的对象时,不必获取所有数据,只需要我们想要的值。而且我们不想创建很多具有较少属性的子类型。这会将大型复杂对象类型的问题转移到许多较小的对象类型。GraphQL 通过动态地挑选特定对象的属性并创建一个新对象列表来返回,从而解决了这两个问题。这就是 GraphQL 的酷之处。现在我们可以在示例测试运行中看到这一点。

假设我们要返回所有用户配置文件,并且只从中选择几个属性

  • 来自 UserProfile 对象的 userId
  • 来自 UserProfile 中的 UserAddress 对象的 userIduserAddressLine1
  • 来自 UserProfile 中的 UserDetail 对象的 userFirstNameuserAge

要做到这一点,我只需要对 REST API 服务器执行一个 HTTP Post 请求,请求内容,内容类型设置为 "text/plain;charset=UTF-8"。请求体将是这样

{
    userProfiles
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstName
            userAge
        }
    }
}

当应用程序正常运行时,返回的响应将是

{
   "userProfiles": [
      {
         "userId":"00000001",
         "userAddress": {
            "userId":"00000001",
            "userAddressLine1":"123 Main St."
         },
         "userDetail": {
            "userFirstName":"Lee",
            "userAge":35
         }
      }
   ]
}

我们可以尝试第二种情况,即我们只对年龄为 35 的用户配置文件感兴趣。我可以修改我的查询如下

{
    userProfilesByAge(age: 35)
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstName
            userAge
        }
    }
}

当我向服务器发送此请求时,我得到相同的响应。如果我将年龄更改为 34,我会得到这样的响应

{
    "userProfilesByAge":[]
}

这意味着集合中没有与此搜索条件匹配的元素。如果我在查询中拼错了一个属性名,例如,像这样

{
    userProfilesByAge(age: 35)
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstname
            userAge
        }
    }
}

我会得到这样的响应

null

在后端日志中,我会看到如下输出

022-07-12 11:43:55.851  WARN 956647 --- [nio-8080-exec-1] 
    notprivacysafe.graphql.GraphQL           : Query did not validate : '{
    userProfilesByAge(age: 34)
    {
        userId
        userAddress
        {
            userId
            userAddressLine1
        }
        userDetail
        {
            userFirstname
            userAge
        }
    }
}'

如果您编译了代码并能够运行所有这些示例测试,那么您就已经成功设置了您的第一个 GraphQL 示例应用程序。第一次看到它工作并得到预期结果时,我非常高兴。我希望您一切顺利。

摘要

写这篇教程很有趣。我花了一些时间才弄清楚如何设计这样一个应用程序,以及它是如何运行的。第一次看到它工作时,感觉非常棒。我非常感谢我看到的两个教程提供了我需要的所有信息来使其正常工作。

在本教程中,我涵盖了以下信息

  • 如何定义 GraphQL schema 并将 schema 文件添加到 Spring boot 应用程序。
  • 如何创建适用于 GraphQL 的自定义 DataFetcher 对象。
  • 如何配置和创建带有 schema 和 Data Fetcher 对象的 GraphQL 对象。
  • 如何创建 RESTFul API 控制器来接收原始 GraphQL 查询并返回响应。
  • 最后,如何测试示例应用程序。

我还想总结一下我学到的东西。GraphQL 可用于简化从通用复杂数据对象创建许多不同类型子对象的过程。如果您想避免定义太多相似的小对象。这是动态生成这些小对象而无需创建对象类型的绝佳方式。总的来说,这将为我节省大量创建这些较小数据对象类型的时间。我不喜欢我必须为已定义的 Java 对象创建查询 schema 的想法。这就像重复定义同一件事。我还必须定义查询类型并实现查询逻辑。这似乎也是重复的工作。我想这些就是使用 GraphQL 的不便之处,除非我们明确定义行为,否则它不会知道我们想要什么。

请享用!希望这对您的开发有所帮助。感谢您阅读本教程。

历史

  • 2022 年 7 月 13 日 - 初始版本
© . All rights reserved.