天天看点

在Spring中使用GraphQL Java

作者:java柚子茶

这篇博客文章将介绍如何在Spring应用程序中使用GraphQLJava,该应用程序公开了供客户端发送查询的端点。

GraphQL Java是我发现的最流行的用于Java的GraphQL服务器端实现之一(在编写本文时有超过5k的星星)。如果您计划从Java或JVM应用程序公开GraphQLAPI,那么这是一个很好的开始使用的库。

这篇博客文章将介绍如何在Spring应用程序中使用GraphQLJava,该应用程序公开了供客户端发送查询的端点。GraphQL Java确实有自己涉及这一主题的正式文件然而,我发现它有点过于简单化,这使我很难把我的头围绕在它上面。我希望你不要对这篇文章的内容有同样的想法,尽管我想你可以写你自己的比我的例子更复杂的文章!

我将根据以下假设编写这篇文章:你了解GraphQL的一些基础知识。我不会涉及任何非常复杂的事情,所以基础知识将给你一个很好的基础,这篇博客文章的内容。了解如何创建模式类型和没有任何花哨功能的查询将是你所需要的。你可以从The official那里找到这个信息Graphql.org现场。

另外,这里有一个提示,我已经用Kotlin编写了我的示例,尽管我试图保持它对Java读者的友好。

相依性

下面是本文中使用的Spring和GraphQL相关依赖项:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.1</version>
</parent>

<dependencies>
  <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java</artifactId>
    <version>16.2</version>
  </dependency>
  <dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-spring-boot-starter-webmvc</artifactId>
    <version>2.0</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.6.1</version>
  </dependency>
</dependencies>           

我们将使用的模式

下面是我们将在本文中使用的GraphQL模式:

type Query {
  people: [Person]
  peopleByFirstName(firstName: String): [Person]
  personById(id: ID): Person
}

type Person {
  id: ID,
  firstName: String,
  lastName: String
  relationships: [Relationship]
}

type Relationship {
  relation: Person,
  relationship: String
}           

稍后,我将介绍如何注册该模式并与其交互;了解模式中类型的形状将设置以下部分。

用DataFetcher获取数据

GraphQL Java使用DataFetcherS获取要包含在查询结果中的数据。更具体地说,aDataFetcher在执行查询时检索单个字段的数据。

每个字段都有指定的DataFetcher。当接收到传入的GraphQL查询时,库将调用已注册的DataFetcher对于查询中的每个字段。

现在值得指出的是,它给我带来了很多困惑,首先,一个“字段”可能意味着两件事:

  • 查询的名称。
  • 架构类型中的属性/字段。

这很重要,因为它意味着DataFetcher可以链接到查询。事实上,每一个查询必有关联DataFetcher。不这样做将导致GraphQL查询请求失败,因为没有入口点开始处理查询。

我一直提到DataFetcherS,但是它们在代码方面到底是什么呢?下面是DataFetcher接口:

public interface DataFetcher<T> {

  T get(DataFetchingEnvironment environment) throws Exception;
}           

所以当我说DataFetcher,我说的是DataFetcher接口。

让我们看一个例子DataFetcher将响应people查询:

type Query {
    people: [Person]
}           

这个DataFetcher对于此查询:

@Component
class PeopleDataFetcher(private val personRepository: PersonRepository) : DataFetcher<List<PersonDTO>> {

  override fun get(environment: DataFetchingEnvironment): List<PersonDTO> {
    return personRepository.findAll().map { person -> PersonDTO(
        person.id,
        person.firstName,
        person.lastName
      ) 
    }
  }
}           

这个PeopleDataFetcher返回List<PersonDTO>对应于[Person]指定为GraphQL查询的转折类型。这个PersonDTO中的相同字段。PersonGraphQL类型:

data class PersonDTO(val id: UUID, val firstName: String, val lastName: String)
           

什么时候PeopleDataFetcher.get执行,它查询数据库,将结果映射到PersonDTOS并归还它们。

当接收到传入的GraphQL查询时,如下所示:

query {
    people {
        firstName
        lastName
        id
    }
}           

在GraphQLJava调用PeopleDataFetcher:

{
  "data": {
    "people": [
      {
        "firstName": "John",
        "lastName": "Doe",
        "id": "00a0d4f2-637f-469c-9ecf-ba8839307996"
      },
      {
        "firstName": "Dan",
        "lastName": "Newton",
        "id": "27a08c14-d0ad-476c-ba09-9edad3e4c8f9"
      }
    ]
  }
}           

这包括了第一次看什么DataFetcher是的。我们将在下面的部分中对它们进行扩展,以提高您对GraphQLJava的理解。

默认的PropertyDataFatcher

如上一节所述,每个字段都必须有一个指定的DataFetcher。这意味着Person类型:

type Person {
  id: ID,
  firstName: String,
  lastName: String
  relationships: [Relationship]
}           

你需要把DataFetchers致:

  • Person.id
  • Person.firstName
  • Person.lastName
  • Person.relationships

这似乎有点麻烦,特别是当您可以一次检索所有这些数据时。例如,从数据库中检索数据可能是1 SQL查询与4之间的区别。

若要解决此问题,请在没有指定DataFetcher使用PropertyDataFetcher默认情况下。

这个PropertyDataFetcher使用各种方法(例如,映射中的getter或键)从父字段中提取字段值(可以是查询或模式类型)。

为了提供一个具体的示例,PeopleDataFetcher我们以前看到的是用来响应peopleGraphQL查询(查询、键入和PeopleDataFetcher列于下):

type Query {
  people: [Person]
}

type Person {
  id: ID,
  firstName: String,
  lastName: String
  relationships: [Relationship]
}           
@Component
class PeopleDataFetcher(private val personRepository: PersonRepository) : DataFetcher<List<PersonDTO>> {

  override fun get(environment: DataFetchingEnvironment): List<PersonDTO> {
    return personRepository.findAll().map { person -> PersonDTO(
        person.id,
        person.firstName,
        person.lastName
      ) 
    }
  }
}           

这个PeopleDataFetcher返回PersonDTO每人Person它会找到对顶级查询的响应。这可以被认为是“父域”。

然后,GraphQL库将向下移动以获取Person例如,firstName和lastName。使用PropertyDataFetcher,它访问每个PersonDTO由父字段的DataFetcher (PeopleDataFetcher)并使用它们的getter提取值。

具体而言,这意味着:

  • Person.id->提供的价值PersonDTO.id.
  • Person.firstName->提供的价值PersonDTO.firstName.
  • Person.lastName->提供的价值PersonDTO.lastName.
  • Person.relationships->空,因为没有提供任何值。

你可能需要在你的脑子里重复几遍,这样才有意义。只有当我的代码不能正常工作时,我才在调试库之后才正确地理解了这一点。

为架构类型字段编写DataFetcher

这个PeopleDataFetcher我们在这篇文章中看到了对查询的响应。现在让我们来看看一个自定义DataFetcher应该与架构类型的字段相关联。

这个PersonRelationshipsDataFetcher获取数据。Person.relationships字段:

@Component
class PersonRelationshipsDataFetcher(
  private val relationshipRepository: RelationshipRepository
) : DataFetcher<List<RelationshipDTO>> {

  override fun get(environment: DataFetchingEnvironment): List<RelationshipDTO> {
    // Gets the object wrapping the [relationships] field
    // In this case a [PersonDTO] object.
    val source = environment.getSource<PersonDTO>()
    return relationshipRepository.findAllByPersonId(source.id).map { relationship ->
      RelationshipDTO(
        relation = relationship.relatedPerson.toDTO(),
        relationship = relationship.relationship
      )
    }
  }
}           

它看起来类似于PeopleDataFetcher我们之前看到过,除了新的电话DataFetchingEnvironment.getSource。此方法允许DataFetcher对象返回的对象。DataFetcher与父字段关联。访问此对象后,将从中提取信息(PersonDTO.id)执行的SQL查询中使用。PersonRelationshipsDataFetcher.

为包含参数的查询编写DataFetcher

当您可以将参数传递给查询时,查询就变得更有价值了。

接受以下查询:

type Query {
  peopleByFirstName(firstName: String): [Person]
}           

为了处理这件事,你需要一个DataFetcher如下所示:

@Component
class PeopleByFirstNameDataFetcher(private val personRepository: PersonRepository) : DataFetcher<List<PersonDTO>> {

  override fun get(environment: DataFetchingEnvironment): List<PersonDTO> {
    // The argument is extracted from the GraphQL query
    val firstName = environment.getArgument<String>("firstName")
    return personRepository.findAllByFirstName(firstName)
      .map { person -> PersonDTO(person.id, person.firstName, person.lastName) }
  }
}           

这里的重要方法调用是DataFetchingEnvironment.getArgument,它按照它的话做,并从传入的GraphQL查询中提取一个参数。轻松地,getArgument允许您指定参数应该是什么类型(因此不必自己转换)。

设置GraphQL实例

你已经看到了如何写一些DataFetcher在这一点上,我们现在需要通过创建一个GraphQL实例并注册应用程序的DataFetcherS.

这个@Configuration下面的代码就是这样做的:

@Configuration
class GraphQLConfiguration(
  private val peopleByFirstNameDataFetcher: PeopleByFirstNameDataFetcher,
  private val peopleDataFetcher: PeopleDataFetcher,
  private val personByIdDataFetcher: PersonByIdDataFetcher,
  private val personRelationshipsDataFetcher: PersonRelationshipsDataFetcher
) {

  @Bean
  fun graphQL(): GraphQL {
    val typeRegistry: TypeDefinitionRegistry = SchemaParser().parse(readSchema())
    val runtimeWiring: RuntimeWiring = buildWiring()
    val graphQLSchema: GraphQLSchema =  SchemaGenerator().makeExecutableSchema(typeRegistry, runtimeWiring)
    return GraphQL.newGraphQL(graphQLSchema).build()
  }

  private fun schemaFile(): File {
    return this::class.java.classLoader.getResource("schema.graphqls")
      ?.let { url -> File(url.toURI()) }
      ?: throw IllegalStateException("The resource does not exist")
  }

  private fun buildWiring(): RuntimeWiring {
    return RuntimeWiring.newRuntimeWiring()
      .type(newTypeWiring("Query").dataFetcher("peopleByFirstName", peopleByFirstNameDataFetcher))
      .type(newTypeWiring("Query").dataFetcher("people", peopleDataFetcher))
      .type(newTypeWiring("Query").dataFetcher("personById", personByIdDataFetcher))
      .type(newTypeWiring("Person").dataFetcher("relationships", personRelationshipsDataFetcher))
      .build()
  }
}           

目的@Configuration类创建一个GraphQL实例,GraphQLJava使用。进一步的设置是不需要的,因为它将在SpringBoot的自动配置中获得。

创建GraphQL实例需要读取应用程序的GraphQL架构。SchemaParser.parse能接受FileS,InputStreamS,ReaderS或String,它解析它(如类名所示)并返回TypeDefinitionRegistry以后再用。在这个应用程序中,模式定义在一个资源文件中,该文件被输入到SchemaParser.parse。这使GraphQL库能够理解传入的查询以及可以或不能处理的内容。

这个DataFetcherS然后在RuntimeWiring实例(通过RuntimeWiring.Builder归还RuntimeWiring.newRuntimeWiring)。每次我提到“得到DataFetcher“与字段相关联”,这是关联实际发生的地方。既然你看过密码,我就不用一直挥手了。

各DataFetcher在本例中,应用程序被注入到配置类中,并链接到RuntimeWiring实例通过其type方法。各TypeRuntimeWiring.Builder实例(由newTypeWiring)需要3项基本投入:

  • 架构类型的名称("Query"或类型名称)。
  • 字段的名称(查询名称或架构类型字段)。
  • 这个DataFetcher与类型和字段关联。

注册后DataFetcher,RuntimeWiring实例将使用build.

最后,TypeDefinitionRegistry和RuntimeWiring以前创建的是通过一个SchemaGenerator然后进入GraphQL.newGraphQL检索功能齐全的GraphQL举个例子。

向应用程序发送GraphQL查询

安装完成后,应用程序现在公开一个/graphql中的自动配置代码提供的端点。graphql-java-spring-boot-starter-webmvc。此端点是客户端将GraphQL查询发送到的地方。

在本节中,我们将介绍如何使用curl和Postman(具有GraphQL功能)发送查询并查看返回的数据。

我们试图发送的查询

query {
    peopleByFirstName(firstName: "Dan") {
        firstName
        lastName
        id
        relationships {
            relation {
                firstName
                lastName
            }
            relationship
        }
    }
}           

卷曲:

curl 'localhost:8080/graphql/' \
-X POST \
-H 'content-type: application/json' \
--data '{ "query": "query { peopleByFirstName(firstName: \"Dan\") { firstName lastName id relationships { relation { firstName lastName } relationship }}}"}'           

这两种方法都返回相同的数据

{
  "data": {
    "peopleByFirstName": [
      {
        "firstName": "Dan",
        "lastName": "Newton",
        "id": "27a08c14-d0ad-476c-ba09-9edad3e4c8f9",
        "relationships": [
          {
            "relation": {
              "firstName": "Laura",
              "lastName": "So"
            },
            "relationship": "Wife"
          },
          {
            "relation": {
              "firstName": "Random",
              "lastName": "Person"
            },
            "relationship": "Friend"
          }
        ]
      },
      {
        "firstName": "Dan",
        "lastName": "Doe",
        "id": "3c07b717-8b9c-4d88-926f-c892be38ee85",
        "relationships": []
      },
    ]
  }
}           

这里最关键的因素是POST请求被使用。GraphQLAPI使用的标准POST获取和变异数据的请求。这使我有一段时间感到不舒服,因为我从/graphql端点并不使我相信这是由于使用了错误的HTTP动词。

为了清晰起见,使用错误的HTTP动词(例如GET)通过邮递员获得以下回复和日志:

{
    "timestamp": "2022-01-03T16:50:58.376+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/graphql"
}           
Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'query' for method parameter type String is not present]           

改进DataFetcher的注册

注册DataFetcherS可以改进,因为它们现在是通过注入DataFetcher的名字GraphQLConfiguration类并手动将每个类与类型和字段名关联。这并不是太糟糕,因为应用程序是小的;如果您认为它看起来已经不可靠,那么我会同意您的意见。

为了进一步提高代码的可维护性和可扩展性,我们可以引入一种新的结构来定义DataFetcher和注册他们。

我们可以通过定义一个新接口来实现这一点,该接口指定类型和字段名为DataFetcher应与:

/**
 * [TypedDataFetcher] is an instance of a [DataFetcher] that specifies the schema type 
 * and field it processes.
 *
 * Instances of [TypedDataFetcher] are registered into an instance of [RuntimeWiring] 
 * after being picked up by Spring (the instances must be annotated with @[Component]
 * or a similar annotated to be injected).
 */
interface TypedDataFetcher<T> : DataFetcher<T> {

  /**
   * The type that the [TypedDataFetcher] handles.
   *
   * Use `Query` if the [TypedDataFetcher] responds to incoming queries.
   *
   * Use a schema type name if the [TypedDataFetcher] fetches data for a single field
   * in the specified type.
   */
  val typeName: String

  /**
   * The field that the [TypedDataFetcher] should apply to.
   *
   * If the [typeName] is `Query`, then [fieldName] will be the name of the query the 
   * TypedDataFetcher] handles.
   *
   * If the [typeName] is a schema type, then [fieldName] should be the name of a single
   * field in [typeName].
   */
  val fieldName: String
}           

用TypedDataFetcher,您可以在Spring允许注入的情况下检索其所有实现。ListS包含实现接口的所有实例。

将此接口与对DataFetcher注册代码GraphQLConfiguration把这一切结合在一起:

@Configuration
class GraphQLConfiguration(private val dataFetchers: List<TypedDataFetcher<*>>) {

  // Create the GraphQL instance (no changes from the previous example).

  /**
   * Loops through all injected [TypedDataFetcher] instances and includes them in the output [RuntimeWiring] instance.
   */
  private fun buildWiring(): RuntimeWiring {
    val wiring = RuntimeWiring.newRuntimeWiring()
    for (dataFetcher in dataFetchers) {
      wiring.type(newTypeWiring(dataFetcher.typeName).dataFetcher(dataFetcher.fieldName, dataFetcher))
    }
    return wiring.build()
  }
}
           

注册现在使用typeName和fieldName由每个人提供TypedDataFetcher,打破了DataFetcher实现和它们表示的类型、字段或查询。添加新TypedDataFetcherS在进行此更改后变得非常简单;您可以创建一个新的实现,然后定义typeName和fieldName,用它注释@Component其余的都由你来处理。

摘要

GraphQLJava允许您执行名称建议的操作,支持Java(或其他JVM语言)中的GraphQL查询。

它是通过联想DataFetcherS到你以自己的方式编写和指向传入查询的类型和字段。使用处理查询解析的库,您可以将精力集中在实现DataFetcherS包含应用程序的主要功能。

在Spring中使用GraphQL Java