これは、なにをしたくて書いたもの?
GraphQLの勉強をしておきたいなと最近思っているのですが、QuarkusにGraphQL向けのExtensionが含まれているのでこちらで始めてみる
ことにしました。
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です。
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のサーバー、クライアントを簡単に作成するためのライブラリです。
ドキュメントは、こちら。
仕様自体がドラフトではありますが、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 Client
今回は、サーバー側を扱っていきます。
今回は、ドキュメントを見ながら簡単に
- 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を使っていないんですよね。
今回はこのまま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が表示されます。
あとは、こちらにクエリーを書いて実行していきます。
カテゴリーを登録する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
というのは、この操作に対する名前でオプションです。
つまり、以下でも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自体が初めてだったので、いろいろわからないところがありましたが、とりあえず雰囲気はつかめてきたのではないかなと
思います。
ちょっとずつ慣れていきましょう。