GraphQL Java是我发现的最流行的用于Java的GraphQL服务器端实现之一(在编写本文时有超过5k的星星)。如果您计划从Java或JVM应用程序公开GraphQLAPI,那么这是一个很好的开始使用的库。
这篇博客文章将介绍如何在Spring应用程序中使用GraphQLJava,该应用程序公开了供客户端发送查询的端点。GraphQL Java确实有自己涉及这一主题的正式文件然而,我发现它有点过于简单化,这使我很难把我的头围绕在它上面。我希望你不要对这篇文章的内容有同样的想法,尽管我想你可以写你自己的比我的例子更复杂的文章!
我将根据以下假设编写这篇文章:你了解GraphQL的一些基础知识。我不会涉及任何非常复杂的事情,所以基础知识将给你一个很好的基础,这篇博客文章的内容。了解如何创建模式类型和没有任何花哨功能的查询将是你所需要的。你可以从The official那里找到这个信息Graphql.org现场。
另外,这里有一个提示,我已经用Kotlin编写了我的示例,尽管我试图保持它对Java读者的友好。
下面是本文中使用的Spring和GraphQL相关依赖项:
org.springframework.boot
spring-boot-starter-parent
2.6.1
com.graphql-java
graphql-java
16.2
com.graphql-java
graphql-java-spring-boot-starter-webmvc
2.0
org.springframework.boot
spring-boot-starter-web
2.6.1
下面是我们将在本文中使用的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
}
稍后,我将介绍如何注册该模式并与其交互;了解模式中类型的形状将设置以下部分。
GraphQL Java使用DataFetcherS获取要包含在查询结果中的数据。更具体地说,aDataFetcher在执行查询时检索单个字段的数据。
每个字段都有指定的DataFetcher。当接收到传入的GraphQL查询时,库将调用已注册的DataFetcher对于查询中的每个字段。
现在值得指出的是,它给我带来了很多困惑,首先,一个“字段”可能意味着两件事:
这很重要,因为它意味着DataFetcher可以链接到查询。事实上,每一个查询必有关联DataFetcher。不这样做将导致GraphQL查询请求失败,因为没有入口点开始处理查询。
我一直提到DataFetcherS,但是它们在代码方面到底是什么呢?下面是DataFetcher接口:
public interface DataFetcher {
T get(DataFetchingEnvironment environment) throws Exception;
}
所以当我说DataFetcher,我说的是DataFetcher接口。
让我们看一个例子DataFetcher将响应people查询:
type Query {
people: [Person]
}
这个DataFetcher对于此查询:
@Component
class PeopleDataFetcher(private val personRepository: PersonRepository) : DataFetcher> {
override fun get(environment: DataFetchingEnvironment): List {
return personRepository.findAll().map { person -> PersonDTO(
person.id,
person.firstName,
person.lastName
)
}
}
}
这个PeopleDataFetcher返回List
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的理解。
如上一节所述,每个字段都必须有一个指定的DataFetcher。这意味着Person类型:
type Person {
id: ID,
firstName: String,
lastName: String
relationships: [Relationship]
}
你需要把DataFetchers致:
这似乎有点麻烦,特别是当您可以一次检索所有这些数据时。例如,从数据库中检索数据可能是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> {
override fun get(environment: DataFetchingEnvironment): List {
return personRepository.findAll().map { person -> PersonDTO(
person.id,
person.firstName,
person.lastName
)
}
}
}
这个PeopleDataFetcher返回PersonDTO每人Person它会找到对顶级查询的响应。这可以被认为是“父域”。
然后,GraphQL库将向下移动以获取Person例如,firstName和lastName。使用PropertyDataFetcher,它访问每个PersonDTO由父字段的DataFetcher (PeopleDataFetcher)并使用它们的getter提取值。
具体而言,这意味着:
你可能需要在你的脑子里重复几遍,这样才有意义。只有当我的代码不能正常工作时,我才在调试库之后才正确地理解了这一点。
这个PeopleDataFetcher我们在这篇文章中看到了对查询的响应。现在让我们来看看一个自定义DataFetcher应该与架构类型的字段相关联。
这个
PersonRelationshipsDataFetcher获取数据。Person.relationships字段:
@Component
class PersonRelationshipsDataFetcher(
private val relationshipRepository: RelationshipRepository
) : DataFetcher> {
override fun get(environment: DataFetchingEnvironment): List {
// Gets the object wrapping the [relationships] field
// In this case a [PersonDTO] object.
val source = environment.getSource()
return relationshipRepository.findAllByPersonId(source.id).map { relationship ->
RelationshipDTO(
relation = relationship.relatedPerson.toDTO(),
relationship = relationship.relationship
)
}
}
}
它看起来类似于PeopleDataFetcher我们之前看到过,除了新的电话
DataFetchingEnvironment.getSource。此方法允许DataFetcher对象返回的对象。DataFetcher与父字段关联。访问此对象后,将从中提取信息(PersonDTO.id)执行的SQL查询中使用。PersonRelationshipsDataFetcher.
当您可以将参数传递给查询时,查询就变得更有价值了。
接受以下查询:
type Query {
peopleByFirstName(firstName: String): [Person]
}
为了处理这件事,你需要一个DataFetcher如下所示:
@Component
class PeopleByFirstNameDataFetcher(private val personRepository: PersonRepository) : DataFetcher> {
override fun get(environment: DataFetchingEnvironment): List {
// The argument is extracted from the GraphQL query
val firstName = environment.getArgument("firstName")
return personRepository.findAllByFirstName(firstName)
.map { person -> PersonDTO(person.id, person.firstName, person.lastName) }
}
}
这里的重要方法调用是
DataFetchingEnvironment.getArgument,它按照它的话做,并从传入的GraphQL查询中提取一个参数。轻松地,getArgument允许您指定参数应该是什么类型(因此不必自己转换)。
你已经看到了如何写一些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项基本投入:
注册后DataFetcher,RuntimeWiring实例将使用build.
最后,TypeDefinitionRegistry和RuntimeWiring以前创建的是通过一个SchemaGenerator然后进入GraphQL.newGraphQL检索功能齐全的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]
注册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 : DataFetcher {
/**
* 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>) {
// 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包含应用程序的主要功能。