CLOVER🍀

That was when it all began.

Quarkusで始めるGraphQL

これは、なにをしたくて書いたもの?

GraphQLの勉強をしておきたいなと最近思っているのですが、QuarkusにGraphQL向けのExtensionが含まれているのでこちらで始めてみる
ことにしました。

Quarkus - SmallRye GraphQL

GraphQL

GraphQLは、APIのためのクエリー言語です。既存のデータに対して、クエリーを実行するためのランタイムでもあります。

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.

GraphQL | A query language for your API

仕様はこちら。

GraphQL Specification Versions

現時点だと、2021年10月のものが最新版のようです。

GraphQL / October 2021 Edition

とりあえず、ここではGraphQL自体に対してはあまり踏み込まないようにします。

Eclipse MicroProfile GraphQL

QuarkusのGraphQL Extensionでは、Eclipse MicroProfile GraphQLの実装を使用しています。

GitHub - eclipse/microprofile-graphql: microprofile-graphql

Eclipse MicroProfile GraphQLの現時点でのバージョンは、1.1.0です。

MicroProfile GraphQL

1.0が2020年2月に出たばかりの、比較的新しい仕様みたいですね。

MicroProfile GraphQL 1.0 released - MicroProfile

MicroProfile GraphQL 1.0 - MicroProfileファミリの新しいAPI

SmallRye GraphQL

先に名前を出しましたが、QuarkusではEclipse MicroProfile GraphQLの実装として、SmallRye GraphQLを使用しています。

GitHub - smallrye/smallrye-graphql: Implementation for MicroProfile GraphQL

GraphQLのサーバー、クライアントを簡単に作成するためのライブラリです。

ドキュメントは、こちら。

Smallrye GraphQL

仕様自体がドラフトではありますが、GraphQL over HTTPの実装にもなる予定みたいです。

GitHub - graphql/graphql-over-http: Working draft of "GraphQL over HTTP" specification

内部的には、GraphQL Javaを使用しています。

Hello from GraphQL Java | GraphQL Java

GitHub - graphql-java/graphql-java: GraphQL Java implementation

Quarkus GraphQL Extension

QuarkusのGraphQL Extensionですが、サーバー、クライアントともに存在します。

Quarkus - SmallRye GraphQL

Quarkus - SmallRye GraphQL Client

今回は、サーバー側を扱っていきます。

Quarkus - SmallRye GraphQL

今回は、ドキュメントを見ながら簡単に

  • Query
  • Mutation

の2つを扱っていこうと思います。

また、戻り値などに使う型は、リアクティブ(SmallRye Mutiny)なものを使うことにします。
とはいえ、GraphQL ExtensionはIOスレッドで動いているわけではなさそうですけどね。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-104-generic", arch: "amd64", family: "unix"

Quarkus GraphQL Extensionを使ったプロジェクトを作成する

まずはプロジェクトを作成します。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.7.5.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=graphql-getting-started \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions="resteasy-reactive,graphql" \
    -DnoCode

Extension Codestartは含めないようにしておきます。

プロジェクト内に移動。

$ cd graphql-getting-started

Mavenの依存関係はこちら。

  <dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-smallrye-graphql</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

ドキュメントにはRESTEasyのExtensionを加えていて、SmallRye Mutinyも使えるということなのでなんとなくRESTEasy Reactive Extensionを
足してみたのですが。

実際に動かしてみると、そもそもGraphQL Extension単体で良さそうです。

というわけで、依存関係からRESTEasyを外して

  <dependencies>
    <!--
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
    -->
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-smallrye-graphql</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit5</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

mvn dependency:treeでquarkus-smallrye-graphqlの依存関係を見てみると、こんな感じになっていました。

[INFO] +- io.quarkus:quarkus-smallrye-graphql:jar:2.7.5.Final:compile
[INFO] |  +- io.quarkus:quarkus-vertx-http:jar:2.7.5.Final:compile
[INFO] |  |  +- io.quarkus:quarkus-security-runtime-spi:jar:2.7.5.Final:compile
[INFO] |  |  +- io.quarkus:quarkus-mutiny:jar:2.7.5.Final:compile
[INFO] |  |  |  +- io.smallrye.reactive:mutiny:jar:1.4.0:compile
[INFO] |  |  |  |  \- org.reactivestreams:reactive-streams:jar:1.0.3:compile
[INFO] |  |  |  \- io.smallrye.reactive:mutiny-smallrye-context-propagation:jar:1.4.0:compile
[INFO] |  |  +- io.smallrye.common:smallrye-common-vertx-context:jar:1.10.0:compile
[INFO] |  |  |  +- io.vertx:vertx-core:jar:4.2.5:compile
[INFO] |  |  |  |  +- io.netty:netty-common:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-buffer:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-transport:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-handler:jar:4.1.74.Final:compile
[INFO] |  |  |  |  |  \- io.netty:netty-tcnative-classes:jar:2.0.48.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-handler-proxy:jar:4.1.74.Final:compile
[INFO] |  |  |  |  |  \- io.netty:netty-codec-socks:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-codec-http:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-codec-http2:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-resolver:jar:4.1.74.Final:compile
[INFO] |  |  |  |  +- io.netty:netty-resolver-dns:jar:4.1.74.Final:compile
[INFO] |  |  |  |  |  \- io.netty:netty-codec-dns:jar:4.1.74.Final:compile
[INFO] |  |  |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.13.1:compile
[INFO] |  |  |  \- io.smallrye.common:smallrye-common-constraint:jar:1.10.0:compile
[INFO] |  |  +- io.quarkus:quarkus-vertx-http-dev-console-runtime-spi:jar:2.7.5.Final:compile
[INFO] |  |  +- io.quarkus.security:quarkus-security:jar:1.1.4.Final:compile
[INFO] |  |  +- io.quarkus:quarkus-vertx:jar:2.7.5.Final:compile
[INFO] |  |  |  +- io.quarkus:quarkus-netty:jar:2.7.5.Final:compile
[INFO] |  |  |  |  \- io.netty:netty-codec:jar:4.1.74.Final:compile
[INFO] |  |  |  +- io.netty:netty-codec-haproxy:jar:4.1.74.Final:compile
[INFO] |  |  |  +- io.smallrye.common:smallrye-common-annotation:jar:1.10.0:compile
[INFO] |  |  |  +- io.smallrye.reactive:smallrye-mutiny-vertx-core:jar:2.19.0:compile
[INFO] |  |  |  |  +- io.smallrye.reactive:smallrye-mutiny-vertx-runtime:jar:2.19.0:compile
[INFO] |  |  |  |  \- io.smallrye.reactive:vertx-mutiny-generator:jar:2.19.0:compile
[INFO] |  |  |  |     \- io.vertx:vertx-codegen:jar:4.2.5:compile
[INFO] |  |  |  \- io.smallrye:smallrye-fault-tolerance-vertx:jar:5.2.1:compile
[INFO] |  |  +- io.smallrye.reactive:smallrye-mutiny-vertx-web:jar:2.19.0:compile
[INFO] |  |  |  +- io.smallrye.reactive:smallrye-mutiny-vertx-web-common:jar:2.19.0:compile
[INFO] |  |  |  +- io.smallrye.reactive:smallrye-mutiny-vertx-auth-common:jar:2.19.0:compile
[INFO] |  |  |  \- io.smallrye.reactive:smallrye-mutiny-vertx-bridge-common:jar:2.19.0:compile
[INFO] |  |  \- io.vertx:vertx-web:jar:4.2.5:compile
[INFO] |  |     +- io.vertx:vertx-web-common:jar:4.2.5:compile
[INFO] |  |     +- io.vertx:vertx-auth-common:jar:4.2.5:compile
[INFO] |  |     \- io.vertx:vertx-bridge-common:jar:4.2.5:compile
[INFO] |  +- io.quarkus:quarkus-core:jar:2.7.5.Final:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] |  |  +- jakarta.enterprise:jakarta.enterprise.cdi-api:jar:2.0.2:compile
[INFO] |  |  |  +- jakarta.el:jakarta.el-api:jar:3.0.3:compile
[INFO] |  |  |  \- jakarta.interceptor:jakarta.interceptor-api:jar:1.2.5:compile
[INFO] |  |  +- jakarta.inject:jakarta.inject-api:jar:1.0:compile
[INFO] |  |  +- io.quarkus:quarkus-ide-launcher:jar:2.7.5.Final:compile
[INFO] |  |  +- io.quarkus:quarkus-development-mode-spi:jar:2.7.5.Final:compile
[INFO] |  |  +- io.smallrye.config:smallrye-config:jar:2.9.1:compile
[INFO] |  |  |  \- io.smallrye.config:smallrye-config-core:jar:2.9.1:compile
[INFO] |  |  |     +- io.smallrye.common:smallrye-common-expression:jar:1.10.0:compile
[INFO] |  |  |     |  \- io.smallrye.common:smallrye-common-function:jar:1.10.0:compile
[INFO] |  |  |     +- io.smallrye.common:smallrye-common-classloader:jar:1.10.0:compile
[INFO] |  |  |     \- io.smallrye.config:smallrye-config-common:jar:2.9.1:compile
[INFO] |  |  +- org.jboss.logging:jboss-logging:jar:3.4.3.Final:compile
[INFO] |  |  +- org.jboss.logmanager:jboss-logmanager-embedded:jar:1.0.9:compile
[INFO] |  |  +- org.jboss.logging:jboss-logging-annotations:jar:2.2.1.Final:compile
[INFO] |  |  +- org.jboss.threads:jboss-threads:jar:3.4.2.Final:compile
[INFO] |  |  +- org.slf4j:slf4j-api:jar:1.7.33:compile
[INFO] |  |  +- org.jboss.slf4j:slf4j-jboss-logmanager:jar:1.1.0.Final:compile
[INFO] |  |  +- org.graalvm.sdk:graal-sdk:jar:21.3.1:compile
[INFO] |  |  +- org.wildfly.common:wildfly-common:jar:1.5.4.Final-format-001:compile
[INFO] |  |  +- io.quarkus:quarkus-bootstrap-runner:jar:2.7.5.Final:compile
[INFO] |  |  \- io.quarkus:quarkus-fs-util:jar:0.0.9:compile
[INFO] |  +- io.quarkus:quarkus-jsonb:jar:2.7.5.Final:compile
[INFO] |  |  +- org.eclipse:yasson:jar:1.0.11:compile
[INFO] |  |  |  \- jakarta.json.bind:jakarta.json.bind-api:jar:1.0.2:compile
[INFO] |  |  \- io.quarkus:quarkus-jsonp:jar:2.7.5.Final:compile
[INFO] |  |     \- org.glassfish:jakarta.json:jar:1.1.6:compile
[INFO] |  +- io.smallrye:smallrye-graphql-schema-builder:jar:1.4.3:compile
[INFO] |  |  +- io.smallrye:smallrye-graphql-schema-model:jar:1.4.3:compile
[INFO] |  |  \- org.jboss:jandex:jar:2.4.2.Final:compile
[INFO] |  +- io.smallrye:smallrye-graphql-cdi:jar:1.4.3:compile
[INFO] |  |  \- io.smallrye:smallrye-graphql:jar:1.4.3:compile
[INFO] |  |     +- io.smallrye:smallrye-graphql-api:jar:1.4.3:compile
[INFO] |  |     |  \- org.eclipse.microprofile.graphql:microprofile-graphql-api:jar:1.1.0:compile
[INFO] |  |     \- com.graphql-java:graphql-java:jar:17.3:compile
[INFO] |  |        +- com.graphql-java:java-dataloader:jar:3.1.0:compile
[INFO] |  |        \- org.antlr:antlr4-runtime:jar:4.9.2:runtime
[INFO] |  +- io.quarkus:quarkus-smallrye-context-propagation:jar:2.7.5.Final:compile
[INFO] |  |  \- io.smallrye:smallrye-context-propagation:jar:1.2.2:compile
[INFO] |  |     +- io.smallrye:smallrye-context-propagation-api:jar:1.2.2:compile
[INFO] |  |     +- io.smallrye:smallrye-context-propagation-storage:jar:1.2.2:compile
[INFO] |  |     \- org.eclipse.microprofile.config:microprofile-config-api:jar:2.0.1:compile
[INFO] |  \- org.eclipse.microprofile.metrics:microprofile-metrics-api:jar:3.0.1:compile

quarkus-smallrye-graphqlの依存関係に、quarkus-vertx-httpが入っていたりします。

ドキュメントを見ていても、RESTEasyを使っていないんですよね。

Quarkus - SmallRye GraphQL

今回はこのままRESTEasyを外したまま、quarkus-smallrye-graphql(とquarkus-arc)のみでいくことにします。

プログラムを作成する

では、プログラムを作成します。お題は書籍にして、以下の2つのエンティティを用意することにしましょう。

  • カテゴリー
  • 書籍

書籍は、なにかのカテゴリーに属するものとしましょう。

カテゴリー、書籍でそれぞれエンティティとリポジトリーを作成。

参考にしているのは、こちらです。

Implementing GraphQL Services / Preparing an Application: GraphQL API

カテゴリー。

src/main/java/org/littlewings/quarkus/graphql/Category.java

package org.littlewings.quarkus.graphql;

import java.util.List;

public class Category {
    Integer id;
    String name;

    // getter/setterは省略
}

src/main/java/org/littlewings/quarkus/graphql/CategoryRepository.java

package org.littlewings.quarkus.graphql;

import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.enterprise.context.ApplicationScoped;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class CategoryRepository {
    ConcurrentMap<Integer, Category> categories = new ConcurrentHashMap<>();

    public Uni<Category> save(Category category) {
        return Uni
                .createFrom()
                .item(categories.put(category.getId(), category))
                .onItem()
                .transform(v -> category);
    }

    public Uni<Category> findById(Integer id) {
        return Uni
                .createFrom()
                .item(categories.get(id));
    }

    public Multi<Category> findAll() {
        return Multi
                .createFrom()
                .iterable(categories.values().stream().sorted(Comparator.comparing(Category::getId)).toList());
    }
}

データは、インメモリで保持することにしました。また、戻り値型はすべてSmallRye MutinyのUniまたはMultiにしてあります。

書籍。

src/main/java/org/littlewings/quarkus/graphql/Book.java

package org.littlewings.quarkus.graphql;

public class Book {
    String isbn;
    String title;
    Integer price;
    Integer categoryId;

    // getter/setterは省略
}

src/main/java/org/littlewings/quarkus/graphql/BookRepository.java

package org.littlewings.quarkus.graphql;

import java.util.Comparator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.enterprise.context.ApplicationScoped;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class BookRepository {
    ConcurrentMap<String, Book> books = new ConcurrentHashMap<>();

    public Uni<Book> save(Book book) {
        return Uni
                .createFrom()
                .item(books.put(book.getIsbn(), book))
                .onItem()
                .transform(v -> book);
    }

    public Uni<Book> findByIsbn(String isbn) {
        return Uni
                .createFrom()
                .item(books.get(isbn));
    }

    public Multi<Book> findAll() {
        return Multi
                .createFrom()
                .iterable(books.values().stream().sorted(Comparator.comparing(Book::getPrice).reversed()).toList());
    }

    public Multi<Book> findByCategoryId(Integer categoryId) {
        return Multi
                .createFrom()
                .iterable(books.values().stream().filter(b -> b.getCategoryId().equals(categoryId)).sorted(Comparator.comparing(Book::getPrice).reversed()).toList());
    }
}

ここまでは、CDIとSmallRye Mutinyを使ったプログラムです。

次に、SmallRye GraphQLを使ったGraphQL APIリソースクラスを作成していきます。

まずは、カテゴリー。

src/main/java/org/littlewings/quarkus/graphql/CategoryResource.java

package org.littlewings.quarkus.graphql;

import java.util.List;
import javax.inject.Inject;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Mutation;
import org.eclipse.microprofile.graphql.Query;
import org.eclipse.microprofile.graphql.Source;
import org.jboss.logging.Logger;

@GraphQLApi
public class CategoryResource {
    Logger logger = Logger.getLogger(CategoryResource.class);

    @Inject
    CategoryRepository categoryRepository;

    @Inject
    BookRepository bookRepository;

    @Mutation
    public Uni<Category> createCategory(Category category) {
        logger.infof("mutation create category, id = %d", category.getId());
        return categoryRepository.save(category);
    }

    @Query
    public Uni<Category> category(Integer id) {
        logger.infof("query category, id = %d", id);
        return categoryRepository.findById(id);
    }

    @Query("allCategories")
    public Uni<List<Category>> categories() {
        logger.infof("query categories");
        return categoryRepository
                .findAll()
                .collect()
                .asList();
    }

    public List<Book> books(@Source Category category) {
        logger.infof("source category, id = %d", category.getId());
        return bookRepository.findByCategoryId(category.getId()).collect().asList().await().indefinitely();
    }

    public List<List<Book>> batchBooks(@Source List<Category> categories) {
        logger.infof("source categories, ids = %s", categories.stream().map(Category::getId).toList());
        return categories
                .stream()
                .map(Category::getId)
                .map(id -> bookRepository.findByCategoryId(id).collect().asList().await().indefinitely())
                .toList();
    }
}

書籍。

src/main/java/org/littlewings/quarkus/graphql/BookResource.java

package org.littlewings.quarkus.graphql;

import java.util.List;
import javax.inject.Inject;

import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Mutation;
import org.eclipse.microprofile.graphql.Query;
import org.jboss.logging.Logger;

@GraphQLApi
public class BookResource {
    Logger logger = Logger.getLogger(BookResource.class);

    @Inject
    BookRepository bookRepository;

    @Mutation("createBook")
    public Uni<Book> createBook(Book book) {
        logger.infof("mutation create book, isbn = %s", book.getIsbn());
        return bookRepository.save(book);
    }

    @Query
    public Uni<Book> book(String isbn) {
        logger.infof("query book, isbn = %s", isbn);
        return bookRepository.findByIsbn(isbn);
    }

    @Query("allBooks")
    public Uni<List<Book>> books() {
        logger.infof("query books");
        return bookRepository.findAll().collect().asList();
    }
}

とりあえずプログラムを載せましたが、順に説明を書いていきましょう。

まず、GraphQL APIのリソースクラスには、@GraphQLApiアノテーションを付与します。

@GraphQLApi
public class CategoryResource {

このリソースクラスには、@Injectが使用可能です。

    @Inject
    CategoryRepository categoryRepository;

    @Inject
    BookRepository bookRepository;

更新系のMutationは、対象のメソッドに@Mutationアノテーションを付与することで作成できます。

Implementing GraphQL Services / Mutations

    @Mutation
    public Uni<Category> createCategory(Category category) {
        logger.infof("mutation create category, id = %d", category.getId());
        return categoryRepository.save(category);
    }

デフォルトでは@Mutationアノテーションを付与したメソッドの名前がそのままMutationの名前になりますが、@Mutationアノテーションの
value属性を指定することで、異なる名前にすることもできます。

    @Mutation("registerCategory")
    public Uni<Category> createCategory(Category category) {
        logger.infof("mutation create category, id = %d", category.getId());
        return categoryRepository.save(category);
    }

Queryを作成する場合は、@Queryアノテーションを付与したメソッドを作成します。Queryの名前はMutationと同様にメソッド名が使用されますが、
@Mutationアノテーションと同様に@Queryアノテーションのvalue属性に値を指定することで変更することもできます。

    @Query
    public Uni<Category> category(Integer id) {
        logger.infof("query category, id = %d", id);
        return categoryRepository.findById(id);
    }

    @Query("allCategories")
    public Uni<List<Category>> categories() {
        logger.infof("query categories");
        return categoryRepository
                .findAll()
                .collect()
                .asList();
    }

ここまでメソッドの戻り値にはUniを使っていますが、戻り値として使えるのはUniかCompletionStageとなります。

Queries can be made reactive by using Uni, or CompletionStage as a return type

Implementing GraphQL Services / Reactive

最初、Multiを使おうとして失敗しました…。

関連するデータを取得する際には、@Sourceアノテーションを使用します。

Implementing GraphQL Services / Expanding the API

    public List<Book> books(@Source Category category) {
        logger.infof("source category, id = %d", category.getId());
        return bookRepository.findByCategoryId(category.getId()).collect().asList().await().indefinitely();
    }

    public List<List<Book>> batchBooks(@Source List<Category> categories) {
        logger.infof("source categories, ids = %s", categories.stream().map(Category::getId).toList());
        return categories
                .stream()
                .map(Category::getId)
                .map(id -> bookRepository.findByCategoryId(id).collect().asList().await().indefinitely())
                .toList();
    }

2つ目の方は、バッチ処理的な取得方法ですね。

@Sourceアノテーションにname属性があるのですが、こちらを指定してもtypeには反映されず、変更したければ@Queryアノテーションを
付与することになりそうです。この場合は、Queryとしても公開されてしまいますが…。

なお、こちらはリアクティブな型(Uni、Multi、CompletionStage)は使えません。

スキーマ定義を見てみる

ここで、パッケージングして

$ mvn package

起動してみます。

$ java -jar target/quarkus-app/quarkus-run.jar

有効になっているExtension。

2022-03-21 02:10:09,707 INFO  [io.quarkus] (main) Installed features: [cdi, smallrye-context-propagation, smallrye-graphql, vertx]

graphql/schema.graphqlで、現在のスキーマ定義を確認できます。

Implementing GraphQL Services / Introspect

$ curl localhost:8080/graphql/schema.graphql

こちらが今回の定義です。

type Book {
  categoryId: Int
  isbn: String
  price: Int
  title: String
}

type Category {
  batchBooks: [Book]
  books: [Book]
  id: Int
  name: String
}

"Mutation root"
type Mutation {
  createBook(book: BookInput): Book
  createCategory(category: CategoryInput): Category
}

"Query root"
type Query {
  allBooks: [Book]
  allCategories: [Category]
  book(isbn: String): Book
  category(id: Int): Category
}

input BookInput {
  categoryId: Int
  isbn: String
  price: Int
  title: String
}

input CategoryInput {
  id: Int
  name: String
}

順に見ましょう。

型定義。

type Book {
  categoryId: Int
  isbn: String
  price: Int
  title: String
}

type Category {
  batchBooks: [Book]
  books: [Book]
  id: Int
  name: String
}

GraphQL APIリソースクラスの各メソッドの戻り値が反映されていますが、以下の部分は@Sourceによる部分ですね。

  batchBooks: [Book]
  books: [Book]

更新系のMutation。

"Mutation root"
type Mutation {
  createBook(book: BookInput): Book
  createCategory(category: CategoryInput): Category
}

Mutationで指定されているパラメーター。

input BookInput {
  categoryId: Int
  isbn: String
  price: Int
  title: String
}

input CategoryInput {
  id: Int
  name: String
}

Query。

"Query root"
type Query {
  allBooks: [Book]
  allCategories: [Category]
  book(isbn: String): Book
  category(id: Int): Category
}

これで、定義は確認できました。

GraphiQL UIを使ってアクセスする

では、実際にGraphQLを使ったリクエストを実行していきましょう。

たとえば、今回のカテゴリーを登録するためのMutationは以下のようになります。

mutation createJavaCategory {
  createCategory(category: {
    id: 1
    name: "java"
  }) {
    id
    name
  }
}

これをcurlを使って表現した場合は、以下のようになります。

$ curl -XPOST \
  -H 'Content-Type: application/json' \
  localhost:8080/graphql \
  -d '{"query": "mutation createJavaCategory { createCategory(category: { id: 1 name: \"java\" }) { id name }}" }'

結果。

{"data":{"createCategory":{"id":1,"name":"java"}}}

curlでも良いのですが、GraphQLとして渡すクエリーパラメーターがJSON内の文字列として表現されるため、ちょっと厳しいです…。

'{ "query": "mutation createJavaCategory { createCategory(category: { id: 1 name: \"java\" }) { id name }}" }'

GraphQL ExtensionにはGraphiQL UIが付属しているので、こちらを使っていきます。

Implementing GraphQL Services / GraphiQL UI

GraphiQL UIは開発モードではデフォルトで有効ですが、今回は明示的に有効にします。

src/main/resources/application.properties

quarkus.smallrye-graphql.ui.always-include=true

今回はその他のパラメーターは扱いませんが、GraphQLでアクセスするエンドポイントが/graphqlだったのはquarkus.smallrye-graphql.root-path
というプロパティのデフォルト値がgraphqlだったからです。

これを例えば以下のようにすると

quarkus.smallrye-graphql.root-path=quarkus-graphql

アクセスする際のURLは、http://localhost:8080/quarkus-graphqlとなります。quarkus.smallrye-graphql.root-pathプロパティで指定する値の
先頭に、/は不要です。

その他の設定は、こちら。

Implementing GraphQL Services / Configuration Reference

ちょっと脱線したので、話を戻して。

設定を追加したので、再度ビルドして起動。

$ mvn package
$ java -jar target/quarkus-app/quarkus-run.jar

この状態でhttp://localhost:8080/q/graphql-ui/にアクセスすると、GraphiQL UIが表示されます。

f:id:Kazuhira:20220321024115p:plain

あとは、こちらにクエリーを書いて実行していきます。

f:id:Kazuhira:20220321024147p:plain

カテゴリーを登録するMutation。

mutation createJavaCategory {
  createCategory(category: {
    id: 1
    name: "java"
  }) {
    id
    name
  }
}

2つ登録しましょう。

mutation createMysqlCategory {
  createCategory(category: {
    id: 2
    name: "mysql"
  }) {
    id
    name
  }
}

呼び出されるのは、こちらの定義ですね。

    @Mutation
    public Uni<Category> createCategory(Category category) {
        logger.infof("mutation create category, id = %d", category.getId());
        return categoryRepository.save(category);
    }

ちなみに、createJavaCategoryやcreateMysqlCategoryというのは、この操作に対する名前でオプションです。

2.3Operations

つまり、以下でもOKです。

mutation {
  createCategory(category: {
    id: 2
    name: "mysql"
  }) {
    id
    name
  }
}

結果は、それぞれこんな感じに。

{
  "data": {
    "createCategory": {
      "id": 1,
      "name": "java"
    }
  }
}
{
  "data": {
    "createCategory": {
      "id": 2,
      "name": "mysql"
    }
  }
}

書籍は5つ登録しましょう。呼び出されるのは、以下の定義になります。

    @Mutation("createBook")
    public Uni<Book> createBook(Book book) {
        logger.infof("mutation create book, isbn = %s", book.getIsbn());
        return bookRepository.save(book);
    }

javaカテゴリーで3件

mutation createJavaBook1 {
  createBook(book: {
    isbn: "978-4621303252"
    title: "Effective Java 第3版"
    price: 4400
    categoryId: 1
  }) {
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "createBook": {
      "isbn": "978-4621303252",
      "title": "Effective Java 第3版",
      "price": 4400,
      "categoryId": 1
    }
  }
}

データ。

mutation createJavaBook2 {
  createBook(book: {
    isbn: "978-4774189093"
    title: "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで"
    price: 3278
    categoryId: 1
  }) {
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "createBook": {
      "isbn": "978-4774189093",
      "title": "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
      "price": 3278,
      "categoryId": 1
    }
  }
}

データ。

mutation createJavaBook3 {
  createBook(book: {
    isbn: "978-4295008477"
    title: "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]"
    price: 2860
    categoryId: 1
  }) {
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "createBook": {
      "isbn": "978-4295008477",
      "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]",
      "price": 2860,
      "categoryId": 1
    }
  }
}

mysqlカテゴリーで2件。

mutation createMysqlBook1 {
  createBook(book: {
    isbn: "978-4798161488"
    title: "MySQL徹底入門 第4版"
    price: 4180
    categoryId: 2
  }) {
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "createBook": {
      "isbn": "978-4798161488",
      "title": "MySQL徹底入門 第4版",
      "price": 4180,
      "categoryId": 2
    }
  }
}

データ。

mutation createMysqlBook2 {
  createBook(book: {
    isbn: "978-4798147406"
    title: "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド"
    price: 3960
    categoryId: 2
  }) {
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "createBook": {
      "isbn": "978-4798147406",
      "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
      "price": 3960,
      "categoryId": 2
    }
  }
}

データが入ったので、今度はQueryを実行してみましょう。

全カテゴリーを取得してみます。

    @Query("allCategories")
    public Uni<List<Category>> categories() {
        logger.infof("query categories");
        return categoryRepository
                .findAll()
                .collect()
                .asList();
    }

Queryはこちら。

query allCategories {
  allCategories{
    id
    name
  }
}

結果。

{
  "data": {
    "allCategories": [
      {
        "id": 1,
        "name": "java"
      },
      {
        "id": 2,
        "name": "mysql"
      }
    ]
  }
}

1件取得も行ってみましょう。

    @Query
    public Uni<Category> category(Integer id) {
        logger.infof("query category, id = %d", id);
        return categoryRepository.findById(id);
    }

Query。

query category {
  category(id: 1) {
    id
    name
  }
}

結果。

{
  "data": {
    "category": {
      "id": 1,
      "name": "java"
    }
  }
}

書籍の全データ取得。

    @Query("allBooks")
    public Uni<List<Book>> books() {
        logger.infof("query books");
        return bookRepository.findAll().collect().asList();
    }

Query。

query allBooks {
  allBooks{
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "allBooks": [
      {
        "isbn": "978-4621303252",
        "title": "Effective Java 第3版",
        "price": 4400,
        "categoryId": 1
      },
      {
        "isbn": "978-4798161488",
        "title": "MySQL徹底入門 第4版",
        "price": 4180,
        "categoryId": 2
      },
      {
        "isbn": "978-4798147406",
        "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
        "price": 3960,
        "categoryId": 2
      },
      {
        "isbn": "978-4774189093",
        "title": "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
        "price": 3278,
        "categoryId": 1
      },
      {
        "isbn": "978-4295008477",
        "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]",
        "price": 2860,
        "categoryId": 1
      }
    ]
  }
}

1件取得。

    @Query
    public Uni<Book> book(String isbn) {
        logger.infof("query book, isbn = %s", isbn);
        return bookRepository.findByIsbn(isbn);
    }

Query。

query book {
  book(isbn: "978-4774189093") {
    isbn
    title
    price
    categoryId
  }
}

結果。

{
  "data": {
    "book": {
      "isbn": "978-4774189093",
      "title": "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
      "price": 3278,
      "categoryId": 1
    }
  }
}

ここまでで、単純なパターンは確認できました。

次は、@Sourceを使って関連したデータを取得してみたいと思います。

2つ実装を用意していました。

    public List<Book> books(@Source Category category) {
        logger.infof("source category, id = %d", category.getId());
        return bookRepository.findByCategoryId(category.getId()).collect().asList().await().indefinitely();
    }

    public List<List<Book>> batchBooks(@Source List<Category> categories) {
        logger.infof("source categories, ids = %s", categories.stream().map(Category::getId).toList());
        return categories
                .stream()
                .map(Category::getId)
                .map(id -> bookRepository.findByCategoryId(id).collect().asList().await().indefinitely())
                .toList();
    }

この2つは、今回は操作の名前(booksかbatchBooksか)で見分けます。

booksを使って、カテゴリーに紐づくデータを取得してみます。

query category {
  category(id: 1) {
    id
    name
    books {
      isbn
      title
      price
      categoryId
    }
  }
}

つまり、こちらですね。

    public List<Book> books(@Source Category category) {
        logger.infof("source category, id = %d", category.getId());
        return bookRepository.findByCategoryId(category.getId()).collect().asList().await().indefinitely();
    }

結果。

{
  "data": {
    "category": {
      "id": 1,
      "name": "java",
      "books": [
        {
          "isbn": "978-4621303252",
          "title": "Effective Java 第3版",
          "price": 4400,
          "categoryId": 1
        },
        {
          "isbn": "978-4774189093",
          "title": "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
          "price": 3278,
          "categoryId": 1
        },
        {
          "isbn": "978-4295008477",
          "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]",
          "price": 2860,
          "categoryId": 1
        }
      ]
    }
  }
}

こちらのQueryでも結果は同じです。

query category {
  category(id: 1) {
    id
    name
    batchBooks {
      isbn
      title
      price
      categoryId
    }
  }
}

呼び出されるのは、こちらになりますが。

    public List<List<Book>> batchBooks(@Source List<Category> categories) {
        logger.infof("source categories, ids = %s", categories.stream().map(Category::getId).toList());
        return categories
                .stream()
                .map(Category::getId)
                .map(id -> bookRepository.findByCategoryId(id).collect().asList().await().indefinitely())
                .toList();
    }

このパターンでは両者に差はほぼ出ませんが、以下のようなパターンだと動きが変わってきます。

Query。

booksを使う場合。

query allCategories {
  allCategories{
    id
    name
    books {
      isbn
      title
      price
      categoryId
    }
  }
}

batchBooksを使う場合。

query allCategories {
  allCategories{
    id
    name
    batchBooks {
      isbn
      title
      price
      categoryId
    }
  }
}

結果。

{
  "data": {
    "allCategories": [
      {
        "id": 1,
        "name": "java",
        "books": [
          {
            "isbn": "978-4621303252",
            "title": "Effective Java 第3版",
            "price": 4400,
            "categoryId": 1
          },
          {
            "isbn": "978-4774189093",
            "title": "Java本格入門 ~モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで",
            "price": 3278,
            "categoryId": 1
          },
          {
            "isbn": "978-4295008477",
            "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]",
            "price": 2860,
            "categoryId": 1
          }
        ]
      },
      {
        "id": 2,
        "name": "mysql",
        "books": [
          {
            "isbn": "978-4798161488",
            "title": "MySQL徹底入門 第4版",
            "price": 4180,
            "categoryId": 2
          },
          {
            "isbn": "978-4798147406",
            "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
            "price": 3960,
            "categoryId": 2
          }
        ]
      }
    ]
  }
}

前者のメソッドを使用する場合は呼び出しが2回になりますが、後者の場合は1回になります。
※この後にログを使ったスレッド確認を行っていますが、そちらで確認できます

    public List<Book> books(@Source Category category) {
        logger.infof("source category, id = %d", category.getId());
        return bookRepository.findByCategoryId(category.getId()).collect().asList().await().indefinitely();
    }

    public List<List<Book>> batchBooks(@Source List<Category> categories) {
        logger.infof("source categories, ids = %s", categories.stream().map(Category::getId).toList());
        return categories
                .stream()
                .map(Category::getId)
                .map(id -> bookRepository.findByCategoryId(id).collect().asList().await().indefinitely())
                .toList();
    }

ちょっと雰囲気がわかってきましたね。

SmallRye GraphQLが動作しているスレッドについて

ログ出力を入れていたので、各操作をした時にスレッド名が出力されています。

2022-03-21 03:06:08,057 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) mutation create category, id = 1
2022-03-21 03:06:13,541 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) mutation create category, id = 2
2022-03-21 03:06:23,063 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) mutation create book, isbn = 978-4621303252
2022-03-21 03:06:27,504 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) mutation create book, isbn = 978-4774189093
2022-03-21 03:06:31,608 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) mutation create book, isbn = 978-4295008477
2022-03-21 03:06:36,009 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) mutation create book, isbn = 978-4798161488
2022-03-21 03:06:43,809 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) mutation create book, isbn = 978-4798147406
2022-03-21 03:06:53,371 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) query categories
2022-03-21 03:07:01,256 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) query category, id = 1
2022-03-21 03:07:13,878 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) query books
2022-03-21 03:07:27,443 INFO  [org.lit.qua.gra.BookResource] (executor-thread-0) query book, isbn = 978-4774189093
2022-03-21 03:07:39,773 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) query category, id = 1
2022-03-21 03:07:39,776 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-1) source category, id = 1
2022-03-21 03:07:55,220 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) query category, id = 1
2022-03-21 03:07:55,226 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-1) source categories, ids = [1]
2022-03-21 03:08:01,533 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) query categories
2022-03-21 03:08:01,535 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-1) source category, id = 1
2022-03-21 03:08:01,539 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-1) source category, id = 2
2022-03-21 03:08:26,347 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) query categories
2022-03-21 03:08:26,352 INFO  [org.lit.qua.gra.CategoryResource] (executor-thread-0) source categories, ids = [1, 2]

スレッド名がexecutor-thread-で始まっているので、ワーカースレッド(ブロッキング処理が可能なスレッド)で動作していることが
確認できました。

まとめ

QuarkusでGraphQLを使ってみました。

GraphQL自体が初めてだったので、いろいろわからないところがありましたが、とりあえず雰囲気はつかめてきたのではないかなと
思います。

ちょっとずつ慣れていきましょう。