GraphQLをWebfluxアプリで使いたい

RESTで実装していく中で、

* PUTやDELETEが通信経路のどこかで遮断されるから、X-HTTP-Method-Overrideにメソッドを詰めてPOSTせざるを得ず、面倒。
* リソース毎に通信が必要だから、複数リソースを跨ったトランザクションってできないよね。どうしよう。

なんて悩みがついてまわってたなかでGraphQLの存在を知った。
で、[GraphQL を Spring Boot で試してみる 1](https://kntmr.hatenablog.com/entry/2018/10/16/100104)などを参考にさせて頂いて、なかなか良さそうな感触を得た。

* でも、必要なフィールドを絞れるのは良いけど、絞りたくないときに*のようなワイルドカードが指定できないのが玉に瑕だと思った。

なのでWebfluxで作成中のアプリケーションでGraphQLのAPIを提供できないかと調べてみたところ、上記記事でも使われていた[GraphQL Java](https://github.com/graphql-java)が昨年12月にgraphql-java-spring-boot-starter-webfluxを公開していることを発見したので調べてみた。

# graphql-java-spring-boot-starter-webflux
[Maven Repository](https://mvnrepository.com/)で検索してみると、最初のリリースの1.0以外は全て日時のバージョンが並んでて、どれを使うか悩みましたが、新しもの好きなので現時点の最新版にしました。

```xml
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-spring-boot-starter-webflux</artifactId>
    <version>2019-06-24T11-47-27-31ab4f9</version>
</dependency>
```

ソースを確認するとgraphql-java-spring-boot-starter-webfluxは、依存するgraphql-java-spring-webflux内のGraphQLControllerクラスをBean登録しているだけでした。

なおGraphQLの通信をお手軽に検証できる、graphiql-spring-boot-starterに相当する実装もありませんでした。

# graphql-java-spring-webflux

GraphQLControllerを中心に気になるところを調べてみた。

* GraphQLControllerクラスは名前から察せられるように```@RestController```が付与されたコントローラ。なので聞いた噂によると、WebfluxをFunctional Endpointモデルで開発していると使えないと思われる。
自分的にはAnnotated Controllerモデルの方が可読性的に気に入っているので、今のところ問題ない。

* GraphQLControllerが受け付けるパスは、graphql.urlプロパティで指定できる。デフォルトはgrapshql。

* POSTリクエストのContent-TypeはJSON(application/json)とGraphQL(application/graphql)に対応。なお、単純な文字列一致で判定しているので、application/json;charset=UTF-8だと処理してくれない。

* 依存関係をみるとgraphql-java-toolsは含まれておらず、こちらでGraphQLオブジェクトをBean登録しておく必要がある。

# サンプル
という調査をしながらサンプルをKotlinで実装しました。

```kotlin
@SpringBootApplication
class GraphqlWebfluxApplication(val userRepo: UserRepository) : CommandLineRunner {
  override fun run(vararg args: String?) {
    userRepo.saveAll(listOf(User(name = "ユウキ"), User(name = "カーシャ"), User(name = "ギジェ")))
  }

  @Bean
  fun graphql(appContext: ApplicationContext, resolvers: List<GraphQLResolver<*>>): GraphQL {
    val builder = SchemaParserBuilder()
    val schemas: List<String> = appContext
      .getResources("classpath*:/graphql/*.graphqls")
      .map { StreamUtils.copyToString(it.inputStream, StandardCharsets.UTF_8) }
      .onEach { builder.schemaString(it) }
      .toList()
    val schemaParser = builder.resolvers(resolvers).build();
    val graphQLSchema = schemaParser.makeExecutableSchema();
    return GraphQL.newGraphQL(graphQLSchema).build()
  }
}

fun main(args: Array<String>) {
  runApplication<GraphqlWebfluxApplication>(*args)
}

@Entity
data class User(
  @field:Id
  @field:GeneratedValue
  var id: Int = -1,
  var name: String = StringUtils.EMPTY)

interface UserRepository : JpaRepository<User, Int>

@Component
class QueryResolver(val userRepo: UserRepository) : GraphQLQueryResolver {
  fun users(): List<User> {
    return userRepo.findAll()
  }
}

@Component
class MutationResolver(val userRepo: UserRepository) : GraphQLMutationResolver {
  fun createUser(name: String): Boolean {
    try {
      userRepo.save(User(name = name))
      return true
    } catch (e: Exception) {
      return false
    }
  }
}
```

GraphQLオブジェクトの生成は、pom.xmlにgraphql-java-toolsを依存関係に追加して、graphql-spring-boot-starterのソースを参考に実装しました。

またテストコードを作成して動作確認しました。

```kotlin
@RunWith(SpringRunner::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GraphqlWebfluxApplicationTests {
  @Autowired
  lateinit var context: ApplicationContext

  @Test
  fun query() {
    val client = WebTestClient.bindToApplicationContext(context).build()
    client
      .post()
      .uri("/api/v1")
      .contentType(MediaType.APPLICATION_JSON)
      .body(BodyInserters.fromObject("""
          {
            "query": "{ users { id name } }"
          }
        """.trimIndent()))
      .exchange()
      .expectStatus().isOk
      .expectBody()
      .jsonPath("$.errors").doesNotExist()
      .returnResult().toString().let {
        println(it)
      }
  }

  @Test
  fun mutation() {
    val client = WebTestClient.bindToApplicationContext(context).build()
    client
      .post()
      .uri("/api/v1")
      .contentType(MediaType.parseMediaType("application/graphql"))
      .body(BodyInserters.fromObject("""
          mutation {
            m1: createUser(name: "ベス")
            m2: createUser(name: "カララ")
          }
        """.trimIndent()))
      .exchange()
      .expectStatus().isOk
      .expectBody()
      .jsonPath("$.errors").doesNotExist()
      .returnResult().toString().let {
        println(it)
      }
    query()
  }
}
```
実行すると

```json
{
  "query": "{ users { id name } }"
}
```
の要求に対して

```json
{"data":{"users":[{"id":"1","name":"ユウキ"},{"id":"2","name":"カーシャ"},{"id":"3","name":"ギジェ"}]}}
```
の応答が確認できる。

サーバ側でGraphQLの構文チェックでエラーが発生しても、HTTPステータスが200なので、ボディ部にerrorsが存在しないことで正常性を確認してる。


# 感想
いい感じです。特に

```
mutation {
  m1: createUser(name: "ベス")
  m2: createUser(name: "カララ")
}
```
で2件の処理を1回の通信でできるのが確認できて、ちょっと感動です。
ただ、この時のトランザクション範囲はどうなっているか、継続確認が必要です。


今回作成したコードは[こちら](https://github.com/megasys1968/sandbox/tree/master/graphql-webflux)。

# 追記(2019/7/8)

graphql-java-toolsは、groupIdが[com.graphql-java](https://mvnrepository.com/artifact/com.graphql-java/graphql-java-tools)と[com.graphql-java-kickstart](https://mvnrepository.com/artifact/com.graphql-java-kickstart/graphql-java-tools)2つがあって混乱してたけど、[こちらの記事](https://www.graphql-java.com/blog/moving-projects)で、com.graphql-javaからcom.graphql-java-kickstartに移動したことが確認できました。

そこでサンプルコードをcom.graphql-java-kickstartの最新版(5.6.1)に更新しました。

ただ、そうするとjava.lang.NoClassDefFoundError: Could not initialize class kotlinx.coroutines.Dispatchersが発生しました。graphql-java-toolsの5.4.0でコルーチンのサポートが加わったため、kotlinのバージョンも1.3.41にしています。

# 追記(2020/2/1)

ちょっと時間が経ってしまいましたが、Spring BootでGraphQLの開発する際のスタンダードだと思う[graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)のバージョン6.0.0でWebflux用のモジュールが追加されてました。
ただ、やはりWebfluxだとGraphiQLやVoyagerとかは使えないようなので、性能要件が高くなく、開発効率優先だったりすると、サーブレット版での開発が無難で、自分はWebfluxでの開発は一旦封印してしまいました。
でも、機会があったら、graphql-spring-bootの方でWebfluxの開発をしたいと思います。