使用graqhql-java-tools(加上kotlin)来实现GraphQL服务器尝试

这篇文章是关于什么的?

使用Kotlin + graphql-java-tools来实现GraphQL服务器并返回响应的过程,我将整理如下。

使用本文的信息,在此文章中提到的内容是:
– 构建一个简单的GraphQL服务器。

这篇文章没有包含的内容:
– N+1问题的解决办法
– 翻页功能的实现

关于graphql-java-tools

此文章将使用Ktor作为任意的框架或库,将提供作为GraphQL服务器的功能。在内部,称为graphql-spring-boot的工具还使用graphql-java-tools。GraphQL服务器本身不具备作为Web应用程序服务器的功能。

实施的过程

    1. 对于graphql文件,我们需要定义一个schema。

 

    1. 我们需要实现Root Resolver。

 

    1. 我们需要实现Resolver。

 

    我们需要实现一个处理GraphQL请求的Handler。

关于RootResolver和Resolver的区别

graphql-java-tools有两种不同的解析器概念。(解析器类似于MVC中的控制器)

我們考慮實現以下的 GraphQL Schema。
這是一個包含父項目 Parent 及其子項目 Child 的數據。

type Query {
  parent: Parent!
}

type Parent {
  id: ID!
  name: String!
  child: Child!
}

type Child {
  id: ID!
  name: String!
}

要返回具有Has A关系的数据,请使用graphql-java-tools,在这种情况下,需要实现RootResolver和Resolver这两种类型。它们各自的作用如下:

RootResolver:
实现了当父节点被引用时的处理逻辑。
当请求达到端点时,将触发并调用该处理逻辑。

解析器:
实现了当Parent.Child被引用时的处理。
注册的父类被调用的时机作为触发器,并被调用。

由于难以理解,我将通过举例来进行说明。

假设我们将example.graphql中定义的类型转换成Kotlin类后,结果如下所示。


data class Parent(
    val id: Int, 
    val name: String, 
    val childId: Int
)

data class Child(
    val id: Int,
    val name: String
)

实际的Class中不存在Parent.child。
为了将Parent.child作为响应返回,需要使用Parent.childId来关联Child。
为此,需要使用Resolver。
当指定的类作为响应返回时,Resolver可以接收该对象。
因此,通过ParentResolver接收Parent以引用childId,可以获取相关联的Child。

概括起来就是:
当被查询定义的终点被引用时的处理方法 -> RootResolver
当终点返回的类型属性被引用时的处理方法 -> Resolver

1. 定义模式

我们来看看实际上实施服务器的步骤。

首先,我们要定义GraphQL的模式。
本文中没有解释GraphQL的语法,所以请参考另一篇文章了解语法。

请在这里实现一个简单的 Query 处理。
请在任意目录下创建一个名为 sample.graphql 的文件。
在本次创建的服务器中,将加载该文件的定义。

我尝试定义了一个名为”samples”的端点,该端点以列表形式返回”Sample”类型的数据。

以下是示例GraphQL文件的释义。

type Query {
  samples: [Sample!]!
}

type Sample {
  id: ID
  name: String
  user: User
}

type User {
  id: ID
  sampleId: ID
}

我們也會在Kotlin中定義對應的資料類。


data class Sample(
  val id: Int,
  val name: String,
  val userId: Int
)

data class User(
  val id: Int,
  val email: String
)

2. 实现 RootResolver

(Note: This translation assumes that “RootResolver” refers to a specific term or concept in the context of programming or technology. If there is more context or specific meaning behind “RootResolver,” please provide more details for a more accurate translation.)

我们将实现 RootReslover。
我们将实现当调用 samples 端点时的处理。
通常情况下,我们会调用 UseCase 等,但是这次我们将制作一个返回相同列表的函数。

根样本解析程序.kt

class RootSampleResolver: GraphQLQueryResolver {
  fun samples(): List<Sample> {
    return listOf(
      Sample(id = 1, name = "sample1", userId = 1),
      Sample(id = 2, name = "sample2", userId = 2),
      Sample(id = 3, name = "sample3", userId = 3)
    )
  }
}

以下是漢語的原生釋義:

重點有兩個:
1. 繼承GraphQLQueryResolver。
因為我們這次是實現Query的操作,所以應該繼承GraphQLQueryResolver。
如果想實現Mutation的操作,則應該繼承GraphQLMutationResolver。
如果需要實現兩者的操作,也可以在一個RootResolver中同時繼承GraphQLQueryResolver和GraphQLMutationResolver。

    1. 准备一个与在GraphQL中定义的查询相同名称的方法。

 

    1. 在graphql/sample.graphql中,定义了名为samples的查询节点,因此在RootReposolver中也需要定义samples方法。

 

    此时,如果在GraphQL端指定了参数,那么RootReposolver的方法必须接收与之匹配的参数类型。

实现Resolver

我們將實現 SampleResolver。
我們將實現當調用 Sample.user 的屬性時的處理。

这个SampleResolver将在返回Sample类作为响应的时候触发。
与往常一样,UseCase不会被调用,而是从固定的List中返回与id匹配的对象。

SampleResolver.kt 的中文本地化处理

class SampleResolver: GraphQLResolver<Sample> {

  private val users = listOf(
    User(id = 1, email = "user1@hoge.com"),
    User(id = 2, email = "user2@hoge.com"),
    User(id = 3, email = "user3@hoge.com")
  )

  fun user(input: Sample): User? {
    return users.find { it.id == input.userId }
  }

}

重点有两个。

    1. 继承GraphQLResolver类。

 

    1. 通过为Resolver指定要接收的对象类型的泛型参数,在这个例子中继承GraphqlResolver。

 

    1. 准备一个与在graphql中定义的属性同名的方法。

 

    由于要实现对在GraphQL中定义的Sample.user属性的引用处理,因此需要实现一个名为user的方法。

4. 实现Handler

我们将前面定义的 sample.graphql 文件和 Resolver 注册到 Handler 中。
这样一来,我们就能在 GraphQL 服务器上处理请求了。

GraphqlHander.kt的汉语本地化为:Graphql处理器.kt

class GraphQLHander {

  /**
  * GraphQLのスキーマをビルドします。
  * filePathに定義されているスキーマとResolverを登録します。
  **/
  fun init(): GraphQL {
    val filePath = "graphql/sample.graphql" 
    val schema = SchemaParser.newParser()
      // GraphQLの定義を読み込む
      .file(file)
      // Resolverを読み込ませて、GraphQLのエンドポイントと紐づける
      .resolvers(listOf(RootSampleResolver(), SampleResolver())) 
      .build()
      .makeExecutableSchema()
  }

  /**
  * GraphQLのリクエストを処理します。
  **/
  fun execute(query: String, operationName: String, variables: Map<String, Any>, context: Any): ExecutionResult {
    val graphql = init()
      return graphql.execute(
        ExecutionInput.newExecutionInput()
          .query(query)
          .operationName(operationName)
          .variables(variables)
          .context(context)
          .dataLoaderRegistry(dataLoaderProvider.provideDataLoaderRegistry())
      )
  }
}

可以通过将GraphQL请求参数传递到定义的Handler的execute方法中来执行GraphQL处理。

我将使用 Ktor,在 /graphql 这个端点上进行 GraphQL 的处理实现。

Routes.kt 的中文释义为「路径.kt」。

// pathの指定
@Location("/graphql")
// リクエストで受け取るパラメータの設定
data class GraphQLRequest(val query: String = "", val operationName: String = "", val variables: Map<String, Any> = mapOf())

fun Application.routes() {

  val handler = GraphQLHandler()

  post<GraphQLRequest> {
      val request = call.receive<GraphQLRequest>()
      val query = request.query
      val operationName = request.operationName
      val variables = request.variables
      val context = ConcurrentHashMap<String, Any>()
      call.respond(
        // GraphQLの処理を実行
        handler.execute(query, operationName, variables, context).toSpecification()
      )
  }
}

现在,当启动服务器后,可以通过/graphql端点接收到GraphQL请求。

结束

这次我实现了一个简单的GraphQL服务器,数据结构简单。
如果有需求的话,我可以展示实际的代码。

如果能对您有一点帮助,我将感到非常幸运。

广告
将在 10 秒后关闭
bannerAds