The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

SmallRye GraphQL

このガイドでは、 MicroProfile GraphQL 仕様の実装である SmallRye GraphQL をQuarkusアプリケーションで使用する方法を説明します。

GraphQL の仕様のWebサイトにも記載されているように

GraphQLは、APIのクエリ言語であり、既存のデータでこれらのクエリを実行するためのランタイムです。 GraphQLは、API内のデータの完全で理解しやすい説明を提供し、クライアントが必要なものだけを正確に要求できるようにし、時間の経過とともにAPIを進化させやすくし、強力な開発者ツールを実現にします。

GraphQL はもともと2012年に Facebook が開発したもので、2015年からはオープンスタンダードとなっています。

GraphQLはREST APIの仕様を置き換えるものではなく、単なる別の手段です。RESTとは異なり、GraphQL APIは以下のようにクライアントに利益をもたらす機能を持っています。

オーバーフェッチとアンダーフェッチの防止

REST API は、クライアントが決定できないサーバー主導の固定データレスポンスです。クライアントはすべてのフィールドを必要としなくても、クライアントはすべてのデータを取得しなければならないため、 Over-fetching です。クライアントはまた、必要とされるすべてのデータを取得するために、最初の呼び出し(HATEOAS)に応じて複数のREST API呼び出しを必要とする場合があり、 Under-fetching です。

APIの進化

GraphQL APIはクライアントから要求されたデータを返すので、既存のAPIにフィールドや機能を追加しても、既存のクライアントに大きな変更を加えることはありません。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 17+がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.6

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

アーキテクチャ

このガイドでは、 /graphql で GraphQL API を公開するシンプルな GraphQL アプリケーションを構築します。

この例は、人気のある GraphQL API にインスパイアされたものです。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.gitアーカイブ をダウンロードします。

ソリューションは microprofile-graphql-quickstart ディレクトリ にあります。

Mavenプロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:

コマンドラインインタフェース
quarkus create app org.acme:microprofile-graphql-quickstart \
    --extension='quarkus-smallrye-graphql' \
    --no-code
cd microprofile-graphql-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.8.3:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=microprofile-graphql-quickstart \
    -Dextensions='quarkus-smallrye-graphql' \
    -DnoCode
cd microprofile-graphql-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=microprofile-graphql-quickstart"

このコマンドは、 smallrye-graphql エクステンションをインポートした Maven プロジェクトを生成します。

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに smallrye-graphql エクステンションを追加することができます。

コマンドラインインタフェース
quarkus extension add quarkus-smallrye-graphql
Maven
./mvnw quarkus:add-extension -Dextensions='quarkus-smallrye-graphql'
Gradle
./gradlew addExtension --extensions='quarkus-smallrye-graphql'

これにより、ビルドファイルに以下が追加されます:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-graphql</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-smallrye-graphql")

アプリケーションの準備: GraphQL API

このセクションでは、GraphQL APIの作成を開始します。

まず、はるか彼方の銀河系の映画を表す以下のエンティティを作成します:

package org.acme.microprofile.graphql;

public class Film {

    public String title;
    public Integer episodeID;
    public String director;
    public LocalDate releaseDate;

}

public class Hero {

    public String name;
    public String surname;
    public Double height;
    public Integer mass;
    public Boolean darkSide;
    public LightSaber lightSaber;
    public List<Integer> episodeIds = new ArrayList<>();

}

enum LightSaber {
    RED, BLUE, GREEN
}
可読性の為にpublicなフィールドを持つクラスを使用していますが、publicなゲッターとセッターを持つprivateなフィールドを持つクラスも使用できます。

先ほど作成したクラスは、クライアントがアクセス可能なデータ(オブジェクト、フィールド、リレーションシップ)のセットであるGraphQLスキーマを記述しています。

引き続き、リポジトリとして動作するCDI Beanの例を見てみましょう。

@ApplicationScoped
public class GalaxyService {

    private List<Hero> heroes = new ArrayList<>();

    private List<Film> films = new ArrayList<>();

    public GalaxyService() {

        Film aNewHope = new Film();
        aNewHope.title = "A New Hope";
        aNewHope.releaseDate = LocalDate.of(1977, Month.MAY, 25);
        aNewHope.episodeID = 4;
        aNewHope.director = "George Lucas";

        Film theEmpireStrikesBack = new Film();
        theEmpireStrikesBack.title = "The Empire Strikes Back";
        theEmpireStrikesBack.releaseDate = LocalDate.of(1980, Month.MAY, 21);
        theEmpireStrikesBack.episodeID = 5;
        theEmpireStrikesBack.director = "George Lucas";

        Film returnOfTheJedi = new Film();
        returnOfTheJedi.title = "Return Of The Jedi";
        returnOfTheJedi.releaseDate = LocalDate.of(1983, Month.MAY, 25);
        returnOfTheJedi.episodeID = 6;
        returnOfTheJedi.director = "George Lucas";

        films.add(aNewHope);
        films.add(theEmpireStrikesBack);
        films.add(returnOfTheJedi);

        Hero luke = new Hero();
        luke.name = "Luke";
        luke.surname = "Skywalker";
        luke.height = 1.7;
        luke.mass = 73;
        luke.lightSaber = LightSaber.GREEN;
        luke.darkSide = false;
        luke.episodeIds.addAll(Arrays.asList(4, 5, 6));

        Hero leia = new Hero();
        leia.name = "Leia";
        leia.surname = "Organa";
        leia.height = 1.5;
        leia.mass = 51;
        leia.darkSide = false;
        leia.episodeIds.addAll(Arrays.asList(4, 5, 6));


        Hero vader = new Hero();
        vader.name = "Darth";
        vader.surname = "Vader";
        vader.height = 1.9;
        vader.mass = 89;
        vader.darkSide = true;
        vader.lightSaber = LightSaber.RED;
        vader.episodeIds.addAll(Arrays.asList(4, 5, 6));

        heroes.add(luke);
        heroes.add(leia);
        heroes.add(vader);

    }

    public List<Film> getAllFilms() {
        return films;
    }

    public Film getFilm(int id) {
        return films.get(id);
    }

    public List<Hero> getHeroesByFilm(Film film) {
        return heroes.stream()
                .filter(hero -> hero.episodeIds.contains(film.episodeID))
                .collect(Collectors.toList());
    }

    public void addHero(Hero hero) {
        heroes.add(hero);
    }

    public Hero deleteHero(int id) {
        return heroes.remove(id);
    }

    public List<Hero> getHeroesBySurname(String surname) {
        return heroes.stream()
                .filter(hero -> hero.surname.equals(surname))
                .collect(Collectors.toList());
    }
}

それでは、最初のGraphQL APIを作成してみましょう。

org.acme.microprofile.graphql.FilmResource クラスを以下のように編集します。

@GraphQLApi (1)
public class FilmResource {

    @Inject
    GalaxyService service;

    @Query("allFilms") (2)
    @Description("Get all Films from a galaxy far far away") (3)
    public List<Film> getAllFilms() {
        return service.getAllFilms();
    }
}
1 @GraphQLApi アノテーションは、CDI BeanがGraphQLのエンドポイントになることを示しています。
2 @Query アノテーションは、このメソッドが allFilms という名前でクエリー可能であることを定義します。
3 クエリー可能なメソッドのドキュメントです。
アノテーション @Query の値はオプションで、これがない場合は暗黙のうちにメソッド名がデフォルトになります。

このようにして、最初のクエリー可能なAPIが作成されましたが、これは後に拡張されます。

起動

quarkusアプリを開発モードで起動します:

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

イントロスペクト

GraphQL APIの完全なスキーマは、以下を呼び出すことで取得できます。

curl http://localhost:8080/graphql/schema.graphql

サーバーは、GraphQL APIの完全なスキーマを返します。

GraphQL UI

実験的 - MicroProfileの仕様に含まれません

GraphQL UIは、GraphQL APIとの簡単なやり取りを可能にする素晴らしいツールです。

Quarkusの smallrye-graphql エクステンションは、 GraphiQL と一緒に出荷され、 devtest モードでデフォルトで有効になりますが、 quarkus.smallrye-graphql.ui.always-include 設定プロパティを true に設定することで production モードでも明示的に有効にすることができます。

GraphQLは、 http://localhost:8080/q/graphql-ui/ からアクセスできます。

GraphQL UI

GraphQL UIのセキュリティを追加/削除する方法については、「 Authorization of Web Endpoints Guide」を参照してください。

GraphQL APIへのクエリー

それでは、 dev モードでデプロイされたGraphQL UIページにアクセスしてみましょう。

以下のクエリをGraphQL UIに入力し、 play ボタンを押します。

query allFilms {
  allFilms {
    title
    director
    releaseDate
    episodeID
  }
}

クエリには Film クラスのすべてのフィールドが含まれているので、レスポンスではすべてのフィールドを取得します。GraphQL APIのレスポンスはクライアントが決めるものなので、クライアントはどのフィールドを必要とするかを選択することができます。

クライアントが必要としているのは titlereleaseDate だけで、先ほどの API 呼び出しでは不要なデータを Over-fetching していたと仮定しましょう。

以下のクエリをGraphQL UIに入力し、 play ボタンを押します。

query allFilms {
  allFilms {
    title
    releaseDate
  }
}

レスポンスでは、必要なフィールドのみを取得していることに注意してください。そのため、 Over-fetching を防ぐことができました。

引き続き、 FilmResource クラスに以下を追加して、GraphQL API を拡張してみましょう。

    @Query
    @Description("Get a Films from a galaxy far far away")
    public Film getFilm(@Name("filmId") int id) {
        return service.getFilm(id);
    }
@Query のアノテーションで値を除外していることに注目してください。したがって、クエリの名前は、 get を除いたメソッド名が暗黙的に設定されます。

このクエリでは、クライアントが映画をIDで検索することができます。また、パラメータに @Name のアノテーションを付けると、パラメータ名が @Name のアノテーションを省略した場合のデフォルトの id ではなく、 filmId に変更されます。

GraphQL UIに以下の内容を入力してリクエストしてください。

query getFilm {
  film(filmId: 1) {
    title
    director
    releaseDate
    episodeID
  }
}

先ほどの例では、 film というクエリメソッドの要求フィールドがそのように決定できます。このようにして、個々の映画の情報を取得することができます。

しかし、クライアントが filmId 01 の両方の映画を必要とするとします。REST APIでは、クライアントはAPIに2回コールする必要があります。したがって、クライアントは Under-fetching となります。

GraphQLでは、一度に複数のクエリを実行することが可能です。

以下をGraphQLに入力して、2つの映画を検索します。

query getFilms {
  film0: film(filmId: 0) {
    title
    director
    releaseDate
    episodeID
  }
  film1: film(filmId: 1) {
    title
    director
    releaseDate
    episodeID
  }
}

これにより、クライアントは1回のリクエストで必要なデータを取得できるようになりました。

APIの拡張

これまでは、映画のデータを取得するための GraphQL API を作成していました。今度は、クライアントが FilmHero データを取得できるようにしたいと考えています。

FilmResource のクラスに以下を追加します。

    public List<Hero> heroes(@Source Film film) { (1)
        return service.getHeroesByFilm(film);
    }
1 Film で応答するクエリに List<Hero> データを追加できるようになりました。

このメソッドを追加することで、GraphQL APIのスキーマが事実上変更されました。 スキーマが変更されたにもかかわらず、以前のクエリはそのまま動作します。 今回は、 FilmHero のデータを取得できるようにAPIを拡張しただけですから。

GraphQLに以下のように入力して、フィルムとヒーローのデータを取得します。

query getFilmHeroes {
  film(filmId: 1) {
    title
    director
    releaseDate
    episodeID
    heroes {
      name
      height
      mass
      darkSide
      lightSaber
    }
  }
}

今や、レスポンスには映画のヒーローたちも含まれています。

バッチ処理

今回の getAllFilms のように Collection の戻り値を公開している場合は、より効率的にヒーローを取得するために、上記のバッチ形式を使用するとよいでしょう。

    public List<List<Hero>> heroes(@Source List<Film> films) { (1)
        // Here fetch all hero lists
    }
1 ここでは、映画を一括して受け取ることで、対応するヒーローを取り出すことができます。

ノンブロッキング

クエリは、例えば Uni を戻り値の型として使用するか、 @NonBlocking をメソッドに付与することでリアクティブにすることができます。

    @Query
    @Description("Get a Films from a galaxy far far away")
    public Uni<Film> getFilm(int filmId) {
        // ...
    }

または、 @NonBlocking を使用することもできます:

    @Query
    @Description("Get a Films from a galaxy far far away")
    @NonBlocking
    public Film getFilm(int filmId) {
        // ...
    }

Uni または @NonBlocking を使用すると、リクエストは Worker スレッドではなく Event-loop スレッドで実行されます。

BlockingとNon-blockingを1つのリクエストに混ぜることができます。

    @Query
    @Description("Get a Films from a galaxy far far away")
    @NonBlocking
    public Film getFilm(int filmId) {
        // ...
    }

    public List<Hero> heroes(@Source Film film) {
        return service.getHeroesByFilm(film);
    }

上記では、イベントループのスレッドで映画を取得しますが、ヒーローを取得するためにワーカスレッドに切り替えます。

抽象型

現在のスキーマは, HeroFilm という2つの具象型だけのシンプルなものです。ここで、型を追加して拡張し、クライアントが簡単に型と対話できるような抽象化機能を追加してAPIを拡張したいと思います。

インターフェイス

ヒーローに味方をつけてみましょう。

まず、 Ally を表すエンティティを新規に作成します。

public class Ally {

    public String name;
    public String surname;
    public Hero partner;
}

GalaxyService を更新して、味方がいるようにします。

    private List<Ally> allies = new ArrayList();

    public GalaxyService() {
        // ...

        Ally jarjar = new Ally();
        jarjar.name = "Jar Jar";
        jarjar.surname = "Binks";
        allies.add(jarjar);
    }

    public List<Ally> getAllAllies() {
        return allies;
    }

また、 FilmResource を更新して、クライアントがすべての味方を照会できるようにしましょう。

    @Query
    public List<Ally> allies() {
        return service.getAllAllies();
    }

GraphQL UIに以下の内容を入力してリクエストしてください。

query getAllies {
    allies {
        name
        surname
    }
}

AllyHero と同じフィールドをいくつか持っていることに注目してください。 クライアントがクエリーをより簡単に行えるように、任意のキャラクターに対する抽象化を作成しましょう。

私たちの共通のキャラクターの特徴を定義した新しいJavaインタフェースを作成します。

public interface Character {

    (1)
    String getName();
    String getSurname();
}
1 インターフェイスで定義されたゲッターは、それが含むGraphQLフィールドを定義します。

さて、このインタフェースを実装するために、 HeroAlly のエンティティを更新します。

public class Hero implements Character {
    // ...

    (1)
    public String getName() {
        return name;
    }

    (1)
    public String getSurname() {
        return surname;
    }
}

public class Ally implements Character {
    // ...

    (1)
    public String getName() {
        return name;
    }

    (1)
    public String getSurname() {
        return surname;
    }
}
1 インターフェースはフィールドを定義できないので、ゲッターを実装する必要があります。

インターフェイスを追加し、既存のエンティティを更新してそれを実装することで、事実上スキーマを変更したことになります。更新されたスキーマには、新しい型 Ally とインターフェース Character が含まれるようになりました。

(1)
interface Character {
    name: String
    surname: String
}

(2)
type Ally implements Character {
    name: String
    surname: String
    partner: Hero
}

(3)
type Hero implements Character {
    name: String
    surname: String
    # ...
}
1 ゲッターをフィールドとする Character インターフェイスが定義されました。
2 Ally 型が追加され、 Character を実装しています。
3 Hero 型を更新して Character を実装しました。

全キャラクターを提供するために、 GalaxyService を更新します。

    public List<Character> getAllCharacters() {
        List<Character> characters = new ArrayList<>();
        characters.addAll(heroes);
        characters.addAll(allies);
        return characters;
    }

これで、クライアントがヒーローだけでなく、すべてのキャラクターをクエリできるようになりました。

FilmResource のクラスに以下を追加します。

    @Query
    @Description("Get all characters from a galaxy far far away")
    public List<Character> characters() {
        return service.getAllCharacters();
    }

GraphQL UIに以下の内容を入力してリクエストしてください。

query getCharacters {
    characters {
        name
        surname
    }
}

ユニオン

実験的 - MicroProfileの仕様に含まれません

これまで、私たちのAPIでは、エンティティやエンティティのリストに対して直接クエリを発行することだけが可能でした。今度は、クライアントが当社のすべてのエンティティを検索できるようにしたいと思います。 HeroAlly は共有の抽象型である Character を持っていますが、 Film も含む抽象型はありません。

まず、検索結果の返り値を表す新しい抽象型を作成します。

package org.acme.microprofile.graphql;

import io.smallrye.graphql.api.Union;

@Union (1)
public interface SearchResult {

}
1 @Union は、このJavaインターフェースが、GraphQLインターフェースではなく、GraphQLユニオンを表していることを示すために必要です。
GraphQLユニオンを表すJavaインターフェイスは空である必要はありませんが、定義されたゲッターは明示的にGraphQLスキーマを変更するものではありません。

SearchResult を実装するためにエンティティを更新します:

public class Film implements SearchResult {
    // ...
}

public class Hero implements Character, SearchResult {
    // ...
}

public class Ally implements Character, SearchResult {
    // ...
}
また、継承を利用して、 interface Character extends SearchResult とすることも出来ます。これにより、 Character のすべての実装がスキーマの SearchResult ユニオンのメンバとして追加されることになります。

GalaxyService を更新し、検索を提供します:

    public List<SearchResult> search(String query) {
        List<SearchResult> results = new ArrayList<>();
        List<Film> matchingFilms = films.stream()
            .filter(film -> film.title.contains(query)
                || film.director.contains(query))
            .collect(Collectors.toList());
        results.addAll(matchingFilms);
        List<Character> matchingCharacters = getAllCharacters().stream()
            .filter(character -> character.getName().contains(query)
                || character.getSurname().contains(query))
            .collect(Collectors.toList());
        results.addAll(matchingCharacters);
        return results;
    }

FilmResource のクラスに以下を追加します。

    @Query
    @Description("Search for heroes or films")
    public List<SearchResult> search(String query) {
        return service.search(query);
    }

GraphQL UIに以下の内容を入力してリクエストしてください。

query searchTheGalaxy {
    search(query: "a") {
        ... on Film {
            title
            director
        }
        ... on Character {
            name
            surname
        }
    }
}
Character インターフェースを使用できるのは、 SearchResult ユニオンにそれを実装したメンバが含まれているからです。

ミューテーション

ミューテーションは、データの作成、更新、削除の際に使用されます。

それでは、GraphQL APIにヒーローの追加と削除の機能を追加してみましょう。

FilmResource のクラスに以下を追加します。

    @Mutation
    public Hero createHero(Hero hero) {
        service.addHero(hero);
        return hero;
    }

    @Mutation
    public Hero deleteHero(int id) {
        return service.deleteHero(id);
    }

GraphQL UI に次のように入力すると、 Hero が挿入されます。

mutation addHero {
  createHero(hero: {
      name: "Han",
      surname: "Solo"
      height: 1.85
      mass: 80
      darkSide: false
      episodeIds: [4, 5, 6]
  	}
  )
  {
    name
    surname
  }
}

このミューテーションを利用することで、私たちのサービスに Hero エンティティを作成しました。

レスポンスの中で、作成したヒーローの namesurname を取得していることに注目してください。これは mutation クエリの { } の中で、これらのフィールドをレスポンスで取得することを選択したためです。これは、クライアントが必要とするサーバー側で生成されたフィールドである可能性があります。

それでは、エントリーを削除してみましょう。

mutation DeleteHero {
  deleteHero(id :3){
    name
    surname
  }
}

createHero の mutation 法と同様に、 { } で定義されている削除したヒーローの namesurname も取得します。

サブスクリプション

サブスクリプションは、クエリーを購読することができます。イベントを受信することができ、web socketを使用しています。詳しくは GraphQL over WebSocket Protocol 仕様をご覧ください。

例: 新しいヒーローが作成されるタイミングを知りたい。

    BroadcastProcessor<Hero> processor = BroadcastProcessor.create(); (1)

    @Mutation
    public Hero createHero(Hero hero) {
        service.addHero(hero);
        processor.onNext(hero); (2)
        return hero;
    }

    @Subscription
    public Multi<Hero> heroCreated(){
        return processor; (3)
    }
1 新しいHeroをブロードキャストする Multi プロセッサー
2 新しいヒーローの追加時に、ブロードキャストも行います
3 実行時にスキーマ内で、WebSocketとしてストリームを利用できるようにする

現在、 /graphql WebSocket接続に接続しているすべてのクライアントは、新しいHeroが作成されたときにイベントを受け取ります。

subscription ListenForNewHeroes {
  heroCreated {
    name
    surname
  }
}

フィールド別のクエリの作成

クエリは個々のフィールドに対して行うこともできます。例えば、ヒーローの名字を照会するメソッドを作ってみましょう。

FilmResource のクラスに以下を追加します。

    @Query
    public List<Hero> getHeroesWithSurname(@DefaultValue("Skywalker") String surname) {
        return service.getHeroesBySurname(surname);
    }

@DefaultValue のアノテーションを使用して、パラメータが提供されていない場合、姓の値は Skywalker になることを決定しました。

以下のクエリを GraphQL UI でテストしてみましょう。

query heroWithDefaultSurname {
  heroesWithSurname{
    name
    surname
    lightSaber
  }
}
query heroWithSurnames {
  heroesWithSurname(surname: "Vader") {
    name
    surname
    lightSaber
  }
}

コンテキスト

この実験的なSmallRye固有の機能を使えば、コードのどこにいてもGraphQLリクエストの情報を得ることができます。

@Inject
Context context;

または、 GraphQLApi クラスであれば、メソッドのパラメータとして使用します。例:

    @Query
    @Description("Get a Films from a galaxy far far away")
    public Film getFilm(Context context, int filmId) {
        // ...
    }

コンテキストオブジェクトによって、以下を取得することができます。

  • オリジナルのリクエスト(Query/Mutation)

  • 引数

  • パス

  • 選択されたフィールド

  • 任意の変数

これにより、データストアへのダウンストリームクエリを最適化することができます。

詳細は JavaDoc を参照してください。

GraphQL-Java

また、このコンテキストオブジェクトは、リーキーな抽象化を利用することで、基礎となる graphql-java の機能にフォールダウンすることを可能にします。

DataFetchingEnvironment dfe = context.unwrap(DataFetchingEnvironment.class);

また、スキーマ生成時に基盤となる graphql-java にアクセスして、独自の機能を直接追加することもできます。

public GraphQLSchema.Builder addMyOwnEnum(@Observes GraphQLSchema.Builder builder) {

    // Here add your own features directly, example adding an Enum
    GraphQLEnumType myOwnEnum = GraphQLEnumType.newEnum()
            .name("SomeEnum")
            .description("Adding some enum type")
            .value("value1")
            .value("value2").build();

    return builder.additionalType(myOwnEnum);
}

@Observer を使用することで、スキーマビルダーに何かを追加することができます。

Observerを動作させるには、イベントを有効にする必要があります。 application.properties で、以下を追加します: quarkus.smallrye-graphql.events.enabled=true

変換

スカラーへの変換

また、SmallRye特有の実験的な機能として、既存のスカラー(実装によって特定のJava型にマッピングされている)を別の型にマッピングしたり、GraphQLで通常 TypeInput を作成するような複雑なオブジェクトを既存のスカラーにマッピングしたりすることができます。

既存のスカラーを別のタイプに変換します。

public class Movie {

    @AdaptToScalar(Scalar.Int.class)
    Long idLongThatShouldChangeToInt;

    // ....
}

上記では、 Long java型を、 デフォルトBigInteger ではなく、 Int スカラー型に変換しています。

複雑なオブジェクトをスカラー型にマッピングします:

public class Person {

    @AdaptToScalar(Scalar.String.class)
    Phone phone;

    // ....
}

これは、GraphQLで TypeInput を作成するのではなく、Stringのスカラーにマッピングされます。

上記の操作を行うためには、 Phone オブジェクトは、文字列(または Int / Date など)を受け取るコンストラクタを持つか、文字列(または Int / Date など)のセッターメソッドを持つか、 fromString (または fromInt / fromDate - スカラーの型に依存)のスタティックメソッドを持つ必要があります。

例えば、以下のようになります。

public class Phone {

    private String number;

    // Getters and setters....

    public static Phone fromString(String number) {
        Phone phone = new Phone();
        phone.setNumber(number);
        return phone;
    }
}

@ToScalar の機能については、 JavaDoc を参照してください。

AdaptWithによる変換

より複雑なケースには、アダプターを用意するという方法もあります。そうすれば、アダプターの中で自分でマッピングを行うことができます。

AdaptWith の機能については、 JavaDoc を参照してください。

例えば、以下のようになります。

    public class Profile {
        // Map this to an email address
        @AdaptWith(AddressAdapter.class)
        public Address address;

        // other getters/setters...
    }

    public class AddressAdapter implements Adapter<EmailAddress, Address> {

        @Override
        public Address from(EmailAddress email) {
            Address a = new Address();
            a.addressType = AddressType.email;
            a.addLine(email.getValue());
            return a;
        }

        @Override
        public EmailAddress to(Address address) {
            if (address != null && address.addressType != null && address.addressType.equals(AddressType.email)) {
                return new EmailAddress(address.lines.get(0));
            }
            return null;
        }
    }
@JsonbTypeAdapter もサポートされています。

Map の組込サポート

デフォルトでは、マップはスキーマでモデル化するのが難しいため(キーと値が実行時に動的になる可能性があるため)、 GraphQL はデフォルトでマップをサポートしません。 上記の適応により、Quarkusでは Map のサポートが追加され、オプションのキーパラメータを持つ Entry<Key,Value> にマップされるようになりました。 これにより、マップを返すことができ、オプションでキーでクエリを実行することができます。

例:

    @Query
    public Map<ISO6391, Language> language() {
        return languageService.getLanguages();
    }

    public enum ISO6391 {
        af,
        en,
        de,
        fr
    }

    public class Language {
        private ISO6391 iso6391;
        private String nativeName;
        private String enName;
        private String please;
        private String thankyou;

        // Getters & Setters
    }
キーと値のオブジェクトは、 Enum 、 Scalar 、 Complex のいずれかを指定します。

これで、すべてのフィールドを含むマップ全体のクエリーが可能になりました:

{
  language{
    key
    value {
      enName
      iso6391
      nativeName
      please
      thankyou
    }
  }
}

これは、例えばこのような結果を返します:

{
  "data": {
    "language": [
      {
        "key": "fr",
        "value": {
          "enName": "french",
          "iso6391": "fr",
          "nativeName": "français",
          "please": "s'il te plaît",
          "thankyou": "merci"
        }
      },
      {
        "key": "af",
        "value": {
          "enName": "afrikaans",
          "iso6391": "af",
          "nativeName": "afrikaans",
          "please": "asseblief",
          "thankyou": "dankie"
        }
      },
      {
        "key": "de",
        "value": {
          "enName": "german",
          "iso6391": "de",
          "nativeName": "deutsch",
          "please": "bitte",
          "thankyou": "danke dir"
        }
      },
      {
        "key": "en",
        "value": {
          "enName": "english",
          "iso6391": "en",
          "nativeName": "english",
          "please": "please",
          "thankyou": "thank you"
        }
      }
    ]
  }
}

キーによるクエリも可能です

{
  language (key:af){
    value {
      please
      thankyou
    }
  }
}

そうすると、マップの中のその値だけが返されます:

{
  "data": {
    "language": [
      {
        "value": {
          "please": "asseblief",
          "thankyou": "dankie"
        }
      }
    ]
  }
}
デフォルトのマップアダプタは、独自の実装でオーバーライドすることができます。

エラーコード

(SmallRye特有の) @ErrorCode を使用することで、GraphQLレスポンスのエラー出力にエラーコードを追加することができます。

@ErrorCode("some-business-error-code")
public class SomeBusinessException extends RuntimeException {
    // ...
}

SomeBusinessException が発生した場合、エラー出力にはエラーコードが表示されます。

{
    "errors": [
        {
            "message": "Unexpected failure in the system. Jarvis is working to fix it.",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "annotatedCustomBusinessException"
            ],
            "extensions": {
                "exception": "io.smallrye.graphql.test.apps.error.api.ErrorApi$AnnotatedCustomBusinessException",
                "classification": "DataFetchingException",
                "code": "some-business-error-code" (1)
            }
        }
    ],
    "data": {
        ...
    }
}
1 エラーコード

その他の注意事項

smallrye-graphql エクステンションを使用していて、 micrometer メトリクスエクステンションが存在し、メトリクスが有効になっている場合、 smallrye-graphql エクステンションの一部のバージョンには Microprofile Metrics API のランタイム要件があるため、 java.lang.NoClassDefFoundError が発生する可能性があります。この問題を解決するには、次の Microprofile Metrics API の依存関係を追加します:

pom.xml
<dependency>
    <groupId>org.eclipse.microprofile.metrics</groupId>
    <artifactId>microprofile-metrics-api</artifactId>
</dependency>
build.gradle
implementation("org.eclipse.microprofile.metrics:microprofile-metrics-api")

まとめ

SmallRye GraphQL は、クライアントが Over-fetchingUnder-fetching を防ぐために必要なデータを正確に取得することを可能にします。

GraphQL APIは、以前のクエリを壊すことなく拡張することができ、API evolution を容易にすることができます。

設定リファレンス

ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能

Configuration property

デフォルト

The rootPath under which queries will be served. Default to graphql By default, this value will be resolved as a path relative to ${quarkus.http.root-path}.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_ROOT_PATH

Show more

string

graphql

Enable Apollo Federation. If this value is unspecified, then federation will be enabled automatically if any GraphQL Federation annotations are detected in the application.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_FEDERATION_ENABLED

Show more

boolean

Enable batch resolving for federation. Disabled by default.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_FEDERATION_BATCH_RESOLVING_ENABLED

Show more

boolean

Enable metrics. By default, this is false. If set to true, a metrics extension is required.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_METRICS_ENABLED

Show more

boolean

Enable tracing. By default, this will be enabled if the tracing extension is added.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_TRACING_ENABLED

Show more

boolean

Enable eventing. Allow you to receive events on bootstrap and execution.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_EVENTS_ENABLED

Show more

boolean

false

Enable non-blocking support. Default is true.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_NONBLOCKING_ENABLED

Show more

boolean

Enable GET Requests. Allow queries via HTTP GET.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_HTTP_GET_ENABLED

Show more

boolean

Enable Query parameter on POST Requests. Allow POST request to override or supply values in a query parameter.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_HTTP_POST_QUERYPARAMETERS_ENABLED

Show more

boolean

Change the type naming strategy.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_AUTO_NAME_STRATEGY

Show more

default, merge-inner-class, full

default

List of extension fields that should be included in the error response. By default, none will be included. Examples of valid values include [exception,classification,code,description,validationErrorType,queryPath]

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_ERROR_EXTENSION_FIELDS

Show more

list of string

List of Runtime Exceptions class names that should show the error message. By default, Runtime Exception messages will be hidden and a generic Server Error message will be returned.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_SHOW_RUNTIME_EXCEPTION_MESSAGE

Show more

list of string

List of Checked Exceptions class names that should hide the error message. By default, Checked Exception messages will show the exception message.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_HIDE_CHECKED_EXCEPTION_MESSAGE

Show more

list of string

The default error message that will be used for hidden exception messages. Defaults to "Server Error"

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_DEFAULT_ERROR_MESSAGE

Show more

string

Print the data fetcher exception to the log file. Default true in dev and test mode, default false in prod.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_PRINT_DATA_FETCHER_EXCEPTION

Show more

boolean

Make the schema available over HTTP.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_AVAILABLE

Show more

boolean

true

Include the Scalar definitions in the schema.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_SCALARS

Show more

boolean

false

Include the schema internal definition in the schema.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_SCHEMA_DEFINITION

Show more

boolean

false

Include Directives in the schema.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_DIRECTIVES

Show more

boolean

false

Include Introspection Types in the schema.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_INTROSPECTION_TYPES

Show more

boolean

false

Log the payload (and optionally variables) to System out.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_LOG_PAYLOAD

Show more

off, query-only, query-and-variables

off

Set the Field visibility.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_FIELD_VISIBILITY

Show more

string

default

Exceptions that should be unwrapped (class names).

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_UNWRAP_EXCEPTIONS

Show more

list of string

Subprotocols that should be supported by the server for graphql-over-websocket use cases. Allowed subprotocols are "graphql-ws" and "graphql-transport-ws". By default, both are enabled.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_WEBSOCKET_SUBPROTOCOLS

Show more

list of string

Set to true if ignored chars should be captured as AST nodes. Default to false

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_PARSER_CAPTURE_IGNORED_CHARS

Show more

boolean

Set to true if `graphql.language.Comment`s should be captured as AST nodes

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_PARSER_CAPTURE_LINE_COMMENTS

Show more

boolean

Set to true true if `graphql.language.SourceLocation`s should be captured as AST nodes. Default to true

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_PARSER_CAPTURE_SOURCE_LOCATION

Show more

boolean

The maximum number of raw tokens the parser will accept, after which an exception will be thrown. Default to 15000

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_PARSER_MAX_TOKENS

Show more

int

The maximum number of raw whitespace tokens the parser will accept, after which an exception will be thrown. Default to 200000

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_PARSER_MAX_WHITESPACE_TOKENS

Show more

int

Abort a query if the total number of data fields queried exceeds the defined limit. Default to no limit

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_INSTRUMENTATION_QUERY_COMPLEXITY

Show more

int

Abort a query if the total depth of the query exceeds the defined limit. Default to no limit

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_INSTRUMENTATION_QUERY_DEPTH

Show more

int

Additional scalars to register in the schema. These are taken from the graphql-java-extended-scalars library.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_EXTRA_SCALARS

Show more

list of ExtraScalar

If GraphQL UI should be enabled. By default, GraphQL UI is enabled if it is included (see always-include).

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_UI_ENABLE

Show more

boolean

true

SmallRye GraphQL UI configuration

デフォルト

The path where GraphQL UI is available. The value / is not allowed as it blocks the application from serving anything else. By default, this URL will be resolved as a path relative to ${quarkus.http.non-application-root-path}.

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_UI_ROOT_PATH

Show more

string

graphql-ui

Always include the UI. By default, this will only be included in dev and test. Setting this to true will also include the UI in Prod

Environment variable: QUARKUS_SMALLRYE_GRAPHQL_UI_ALWAYS_INCLUDE

Show more

boolean

false

関連コンテンツ