SmallRye GraphQL

本指南演示了您的 Quarkus 应用程序如何使用 SmallRye GraphQL,即 MicroProfile GraphQL规范的实现。

This guide demonstrates how your Quarkus application can use SmallRye GraphQL, an implementation of the MicroProfile GraphQL specification.

正如 GraphQL规范网站所述:

As the GraphQL specification website states: GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

*GraphQL*最初由 *Facebook*开发于 2012 年,自 2015 年以来一直是一个开放标准。

GraphQL was originally developed by Facebook in 2012 and has been an open standard since 2015.

GraphQL 并不是 REST API 规范的替代品,而仅仅是一种替代方案。与 REST 不同,GraphQL API 能够通过以下方式使客户端受益:

GraphQL is not a replacement for REST API specification but merely an alternative. Unlike REST, GraphQL API’s have the ability to benefit the client by: Preventing Over-fetching and Under-fetching:: REST APIs are server-driven fixed data responses that cannot be determined by the client. Although the client does not require all the fields the client must retrieve all the data hence Over-fetching. A client may also require multiple REST API calls according to the first call (HATEOAS) to retrieve all the data that is required thereby Under-fetching.

API Evolution

Since GraphQL API’s returns data that are requested by the client adding additional fields and capabilities to existing API will not create breaking changes to existing clients.

Prerequisites

Unresolved directive in smallrye-graphql.adoc - include::{includes}/prerequisites.adoc[]

Architecture

在本指南中,我们构建了一个简单的 GraphQL 应用程序,该应用程序在 `/graphql`上公开一个 GraphQL API。

In this guide, we build a simple GraphQL application that exposes a GraphQL API at /graphql.

此示例受一个流行的 GraphQL API 启发。

This example was inspired by a popular GraphQL API.

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

克隆 Git 存储库: git clone {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

解决方案位于 microprofile-graphql-quickstart directory中。

The solution is located in the microprofile-graphql-quickstart directory.

Creating the Maven Project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

First, we need a new project. Create a new project with the following command:

Unresolved directive in smallrye-graphql.adoc - include::{includes}/devtools/create-app.adoc[]

此命令生成一个项目,导入 `smallrye-graphql`扩展。

This command generates a project, importing the smallrye-graphql extension.

如果您已经配置了 Quarkus 项目,则可以通过在项目基本目录中运行以下命令将 `smallrye-graphql`扩展添加到您的项目:

If you already have your Quarkus project configured, you can add the smallrye-graphql extension to your project by running the following command in your project base directory:

Unresolved directive in smallrye-graphql.adoc - include::{includes}/devtools/extension-add.adoc[]

这会将以下内容添加到构建文件中:

This will add the following to your build file:

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

Preparing an Application: GraphQL API

在本节中,我们将开始创建 GraphQL API。

In this section we will start creating the GraphQL API.

首先,创建以下代表遥远星系中一部电影的实体:

First, create the following entities representing a film from a galaxy far, far away:

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
}

为了便于阅读,我们使用具有公共字段的类,但具有公共获取器和设置器的私有字段的类也可以正常工作。

For readability we use classes with public fields, but classes with private fields with public getters and setters will also work.

我们刚刚创建的类描述了 GraphQL 模式,这是客户端可以访问的一组可能的数据(对象、字段、关系)。

The classes we have just created describe the GraphQL schema which is a set of possible data (objects, fields, relationships) that a client can access.

让我们继续使用一个示例 CDI bean,它可以充当存储库:

Let’s continue with an example CDI bean, that would work as a repository:

@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。

Now, let’s create our first GraphQL API.

org.acme.microprofile.graphql.FilmResource 类编辑为以下内容:

Edit the org.acme.microprofile.graphql.FilmResource class as following:

@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 annotation indicates that the CDI bean will be a GraphQL endpoint
2 @Query annotation defines that this method will be queryable with the name allFilms
3 Documentation of the queryable method

@Query 注释的值是可选的且不存在时,将隐式默认值为方法名。

The value of the @Query annotation is optional and would implicitly be defaulted to the method name if absent.

通过这种方式,我们创建了第一个可查询 API,我们稍后将对其进行扩展。

This way we have created our first queryable API which we will later expand.

Launch

在开发模式下启动 Quarkus 应用程序:

Launch the quarkus application in dev mode:

Unresolved directive in smallrye-graphql.adoc - include::{includes}/devtools/dev.adoc[]

Introspect

可通过调用以下内容来检索 GraphQL API 的完整架构:

The full schema of the GraphQL API can be retrieved by calling the following:

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

服务器将返回 GraphQL API 的完整架构。

The server will return the complete schema of the GraphQL API.

GraphQL UI

实验性 - 未包含在 MicroProfile 规范中

Experimental - not included in the MicroProfile specification

GraphQL UI 是一个出色的工具,允许与 GraphQL API 方便地进行交互。

GraphQL UI is a great tool permitting easy interaction with your GraphQL APIs.

Quarkus smallrye-graphql 扩展附带 GraphiQL,并在 devtest 模式中默认启用它,但也可以通过将 quarkus.smallrye-graphql.ui.always-include 配置属性设置为 true 来对 production 模式显式进行配置。

The Quarkus smallrye-graphql extension ships with GraphiQL and enables it by default in dev and test modes, but it can also be explicitly configured for production mode as well, by setting the quarkus.smallrye-graphql.ui.always-include configuration property to true.

可从 [role="bare"][role="bare"]http://localhost:8080/q/graphql-ui/ 访问 GraphQL UI。

The GraphQL UI can be accessed from [role="bare"]http://localhost:8080/q/graphql-ui/ .

graphql ui screenshot01

请参阅 Authorization of Web Endpoints 指南,了解如何为 GraphQL UI 添加/删除安全性。

Have a look at the Authorization of Web Endpoints Guide on how to add/remove security for the GraphQL UI.

Query the GraphQL API

现在访问在 dev 模式中已部署的 GraphQL UI 页面。

Now visit the GraphQL UI page that has been deployed in dev mode.

将以下查询输入 GraphQL UI 然后按 play 按钮:

Enter the following query to the GraphQL UI and press the play button:

query allFilms {
  allFilms {
    title
    director
    releaseDate
    episodeID
  }
}

由于我们的查询包含 Film 类中的所有字段,因此我们将检索我们响应中的所有字段。由于 GraphQL API 响应是由客户端确定的,因此客户端可以选择它所需的字段。

Since our query contains all the fields in the Film class we will retrieve all the fields in our response. Since GraphQL API responses are client determined, the client can choose which fields it will require.

让我们假设我们的客户端仅需要 titlereleaseDate,这样就使得之前对不需要数据的 API Over-fetching 的调用。

Let’s assume that our client only requires title and releaseDate making the previous call to the API Over-fetching of unnecessary data.

将以下查询输入 GraphQL UI 然后按 play 按钮:

Enter the following query into the GraphQL UI and hit the play button:

query allFilms {
  allFilms {
    title
    releaseDate
  }
}

在响应中需要注意,我们只检索必需字段。因此,我们已防止 Over-fetching.

Notice in the response we have only retrieved the required fields. Therefore, we have prevented Over-fetching.

让我们继续向 FilmResource 类中添加以下内容,以扩展我们的 GraphQL API。

Let’s continue to expand our GraphQL API by adding the following to the FilmResource class.

    @Query
    @Description("Get a Films from a galaxy far far away")
    public Film getFilm(@Name("filmId") int id) {
        return service.getFilm(id);
    }

需要注意的是,我们已排除了 @Query 注释中的值。因此,查询名称隐式设定为方法名称,排除 get.

Notice how we have excluded the value in the @Query annotation. Therefore, the name of the query is implicitly set as the method name excluding the get.

此查询将允许客户端按 ID 检索电影,并且参数上的 @Name 注释将参数名称更改为 filmId,而非省略 @Name 注释时的默认名称 id.

This query will allow the client to retrieve the film by id, and the @Name annotation on the parameter changes the parameter name to filmId rather than the default id that it would be if you omit the @Name annotation.

GraphQL UI 中输入以下内容并发出请求。

Enter the following into the GraphQL UI and make a request.

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

如之前示例所示,可以确定 film 查询方法请求的字段。通过这种方式,我们可检索单独的电影信息。

The film query method requested fields can be determined as such in our previous example. This way we can retrieve individual film information.

然而,假设我们的客户端同时需要具有影片 ID 01 的两部电影。在 REST API 中,客户端需要向 API 发出两个调用。因此,客户端将为 Under-fetching.

However, say our client requires both films with filmId 0 and 1. In a REST API the client would have to make two calls to the API. Therefore, the client would be Under-fetching.

在 GraphQL 中,一次执行多个查询是可能的。

In GraphQL, it is possible to make multiple queries at once.

GraphQL UI 中输入以下内容来检索两部电影:

Enter the following into the GraphQL UI to retrieve two films:

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

这使客户端能够在单个请求中获取所需数据。

This enabled the client to fetch the required data in a single request.

Expanding the API

到目前为止,我们已创建了一个 GraphQL API 来检索电影数据。我们现在希望使客户端能够检索 FilmHero 数据。

Until now, we have created a GraphQL API to retrieve film data. We now want to enable the clients to retrieve the Hero data of the Film.

将以下内容添加到我们的 FilmResource 类:

Add the following to our FilmResource class:

    public List<Hero> heroes(@Source Film film) { (1)
        return service.getHeroesByFilm(film);
    }
1 Enable List<Hero> data to be added to queries that respond with Film

通过添加此方法,我们已有效地改变了 GraphQL API 的架构。尽管架构发生了变化,但以前的查询仍然有效。因为我们仅扩展了 API 以能够检索 FilmHero 数据。

By adding this method we have effectively changed the schema of the GraphQL API. Although the schema has changed the previous queries will still work. Since we only expanded the API to be able to retrieve the Hero data of the Film.

GraphQL UI 中输入以下内容来检索电影和英雄数据。

Enter the following into the GraphQL UI to retrieve the film and hero data.

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

现在,响应中包括了电影的英雄。

The response now includes the heroes of the film.

Batching

当您公开 getAllFilms 一般的 Collection 返回值时,您可以使用上述批处理方式,以更有效地获取英雄:

When you are exposing a Collection return like our getAllFilms, you might want to use the batch form of the above, to more efficiently fetch the heroes:

    public List<List<Hero>> heroes(@Source List<Film> films) { (1)
        // Here fetch all hero lists
    }
1 Here receive the films as a batch, allowing you to fetch the corresponding heroes.

Non blocking

可以通过将 Uni 用作返回类型来让查询变为响应式,或向方法添加 @NonBlocking

Queries can be made reactive by using Uni as a return type, or adding @NonBlocking to the method:

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

或可以使用 @NonBlocking

Or you can use @NonBlocking:

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

使用 `Uni`或 `@NonBlocking`表示请求将在事件循环线程上执行,而不是在工作线程上。

Using Uni or @NonBlocking means that the request will be executed on Event-loop threads rather than Worker threads.

可以一次将阻塞和非阻塞混合在同一个请求中,

You can mix Blocking and Non-blocking in one request,

    @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);
    }

上述内容会在事件循环线程上获取电影,但是会切换到工作人员线程来获取英雄。

Above will fetch the film on the event-loop threads, but switch to the worker thread to fetch the heroes.

Abstract Types

当前模式很简单,只有两个具体类型 Hero`和 `Film。现在我们想要使用附加类型来扩展我们的 API,并且添加一些抽象,以便让客户端可以轻松与之交互。

The current schema is simple with only two concrete types, Hero and Film. Now we want to expand our API with additional types and add some abstractions that make interacting with them easy for clients.

Interfaces

让我们给我们的英雄配一些盟友。

Let’s give our heroes some allies.

首先,创建一个新实体来表示我们的 Ally

First, create a new entity to represent our Ally.

public class Ally {

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

更新 `GalaxyService`让其有一些盟友。

Update the GalaxyService to have some allies.

    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`以允许客户端查询所有盟友:

Let’s also update FilmResource to allow clients to query for all allies:

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

GraphQL UI 中输入以下内容并发出请求。

Enter the following into the GraphQL UI and make a request.

query getAllies {
    allies {
        name
        surname
    }
}

请注意,`Ally`有一些与 `Hero`相同的字段。为了更好地使客户端查询变得容易,我们来为任何角色创建一个抽象。

Notice that Ally has a some of the same fields as a Hero. To better make queries easier for clients, let’s create an abstraction for any character.

创建一个新的 Java 接口来定义我们的共同人物特征。

Create a new Java interface that defines our common character traits.

public interface Character {

    (1)
    String getName();
    String getSurname();
}
1 Getters defined in an interface will define the GraphQL fields that it contains

现在,更新我们的 `Hero`和 `Ally`实体来实现此接口。

Now, update our Hero and Ally entities to implement this interface.

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 Because interfaces can’t define fields, we have to implement the getters

通过添加一个接口并更新现有实体来实现它,我们有效地更改了模式。更新的模式现在将包括新的 `Ally`类型和 `Character`接口。

By adding an interface and updating existing entities to implement it, we have effectively changed the schema. The updated schema will now include the new Ally type and Character interface.

(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 The Character interface was defined with the getters as fields
2 The Ally type was added and it implements Character
3 The Hero type was updated to implement Character

更新我们的 `GalaxyService`来提供所有角色。

Update our GalaxyService to provide all characters.

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

现在,我们可以允许客户端查询所有角色,而不仅仅是英雄。

Now we can allow clients to query for all characters, not just heroes.

将以下内容添加到我们的 FilmResource 类:

Add the following to our FilmResource class:

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

GraphQL UI 中输入以下内容并发出请求。

Enter the following into the GraphQL UI and make a request.

query getCharacters {
    characters {
        name
        surname
    }
}

Unions

实验性 - 未包含在 MicroProfile 规范中

Experimental - not included in the MicroProfile specification

到目前为止,我们的 API 仅允许我们直接查询实体或实体列表。现在,我们希望允许客户端搜索我们所有的实体。虽然 HeroAlly 具有 Character 的一个共享抽象类型,但没有包含 Film 的抽象。

So far, our API has only allowed us to query directly for an entity or list of entities. Now we want to allow clients to search all of our entities. While Hero and Ally have a shared abstract type of Character, there is no abstraction that also includes Film.

首先,创建此新抽象类型,表示搜索结果的可能返回类型。

First, create this new abstract type representing the possible return types for a search result.

package org.acme.microprofile.graphql;

import io.smallrye.graphql.api.Union;

@Union (1)
public interface SearchResult {

}
1 @Union is required to indicate this Java interface represents a GraphQL union, not a GraphQL interface

表示 GraphQL 联合的 Java 接口不必为空,但任何定义的 getter 都不会显式更改 GraphQL 模式。

The Java interface representing the GraphQL union does not have to be empty, but any getters defined will not explicitly change the GraphQL schema.

更新我们的实体以实现 SearchResult

Update our entities to implement SearchResult:

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

public interface Character implements SearchResult {
    // ...
}

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

public class Ally implements Character {
    // ...
}

更新 `GalaxyService`以提供搜索功能:

Update GalaxyService to provide search:

    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 类:

Add the following to our FilmResource class:

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

GraphQL UI 中输入以下内容并发出请求。

Enter the following into the GraphQL UI and make a request.

query searchTheGalaxy {
    search(query: "a") {
        ... on Film {
            title
            director
        }
        ... on Character {
            name
            surname
        }
    }
}

我们能够使用 Character 接口,因为 SearchResult 联合包含实现它的成员。

We are able to use the Character interface because the SearchResult union contains members that implement it.

Mutations

当创建、更新或删除数据时,我们将使用 Mutations。

Mutations are used when data is created, updated or deleted.

让我们现在在我们的 GraphQL API 中添加添加和删除英雄的能力。

Let’s now add the ability to add and delete heroes to our GraphQL API.

将以下内容添加到我们的 FilmResource 类:

Add the following to our FilmResource class:

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

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

GraphQL UI 中输入以下内容以插入 Hero

Enter the following into the GraphQL UI to insert a Hero:

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

通过使用此 Mutation,我们在服务中创建了一个 Hero 实体。

By using this mutation we have created a Hero entity in our service.

请注意,我们在响应中如何检索创建的英雄的 namesurname。这是因为我们选择在 Mutation 查询中的 { } 中的响应中检索这些字段。这可能是一个服务器端生成字段,客户端可能会要求。

Notice how in the response we have retrieved the name and surname of the created Hero. This is because we selected to retrieve these fields in the response within the { } in the mutation query. This can easily be a server side generated field that the client may require.

我们现在尝试删除一个条目:

Let’s now try deleting an entry:

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

类似于 createHero Mutation 方法,我们还检索我们已删除的英雄的 namesurname,这些英雄在 { } 中进行定义。

Similar to the createHero mutation method we also retrieve the name and surname of the hero we have deleted which is defined in { }.

Subscriptions

订阅允许您订阅查询。它允许您接收事件并正在使用 Web 套接字。请参阅 GraphQL over WebSocket Protocol 规范以了解更多详细信息。

Subscriptions allow you to subscribe to a query. It allows you to receive events and is using web sockets. See the GraphQL over WebSocket Protocol spec for more details.

示例:我们想要知道何时创建新英雄:

Example: We want to know when new Heroes are being created:

    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 The Multi processor that will broadcast any new `Hero`es
2 When adding a new Hero, also broadcast it
3 Make the stream available in the schema and as a WebSocket during runtime

现在,任何连接到 /graphql WebSocket 连接的客户端都将接收关于创建新英雄的事件:

Any client that now connect to the /graphql WebSocket connection will receive events on new Heroes being created:

subscription ListenForNewHeroes {
  heroCreated {
    name
    surname
  }
}

Creating Queries by fields

还可以对各个字段进行查询。例如,我们创建一种通过姓氏查询英雄的方法。

Queries can also be done on individual fields. For example, let’s create a method to query heroes by their last name.

将以下内容添加到我们的 FilmResource 类:

Add the following to our FilmResource class:

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

通过使用注释 @DefaultValue,我们确定当未提供参数时,surname 的值将为 Skywalker

By using the @DefaultValue annotation we have determined that the surname value will be Skywalker when the parameter is not provided.

使用 GraphQL UI 测试以下查询:

Test the following queries with the GraphQL UI:

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

Context

您可以使用该实验性 SmallRye 特定功能,在代码中的任何位置获取有关 GraphQL 请求的信息:

You can get information about the GraphQL request anywhere in your code, using this experimental, SmallRye specific feature:

@Inject
Context context;

或者,如果您位于 GraphQLApi 类中,则可以用作方法中的参数,例如:

or as a parameter in your method if you are in the GraphQLApi class, for instance:

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

上下文对象允许您获取以下信息:

The context object allows you to get:

  • the original request (Query/Mutation)

  • the arguments

  • the path

  • the selected fields

  • any variables

这使您可以优化到数据存储区的下游查询。

This allows you to optimize the downstream queries to the datastore.

请参阅 JavaDoc 了解更多详细信息。

See the JavaDoc for more details.

GraphQL-Java

该上下文对象还允许您通过使用泄漏抽象,访问底层 graphql-java 特征:

This context object also allows you to fall down to the underlying graphql-java features by using the leaky abstraction:

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

在生成 Schema 期间,您还可以访问底层 graphql-java 以直接添加自己的特征:

You can also get access to the underlying graphql-java during schema generation, to add your own features directly:

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,您可以向 Schema 构建器添加任何内容。

By using the @Observer you can add anything to the Schema builder.

为了让观察者正常工作,您需要启用事件。在 application.properties 中,添加以下内容: quarkus.smallrye-graphql.events.enabled=true

For the Observer to work, you need to enable events. In application.properties, add the following: quarkus.smallrye-graphql.events.enabled=true.

Adapting

Adapt to Scalar

另一项 SmallRye 特定的实验性功能,允许您将现有标量(由实现映射到特定 Java 类型),映射到另一种类型,或映射复杂对象,这通常会在 GraphQL 中创建 TypeInput,到现有的标量。

Another SmallRye specific experimental feature, allows you to map an existing scalar (that is mapped by the implementation to a certain Java type) to another type, or to map complex object, that would typically create a Type or Input in GraphQL, to an existing scalar.

Adapting an existing Scalar to another type:

public class Movie {

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

    // ....
}

以上将调整 Long java 类型为 Int 标量类型,而不是 default BigInteger

Above will adapt the Long java type to an Int Scalar type, rather than the default BigInteger.

Adapting a complex object to a Scalar type:

public class Person {

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

    // ....
}

这将映射到字符串标量,而不是在 GraphQL 中创建 TypeInput

This will, rather than creating a Type or Input in GraphQL, map to a String scalar.

为了能够执行上述操作, Phone 对象需要有一个使用 String 的构造器(或 Int / Date / 等),或为 String(或 Int / Date / 等)设置一个设置器方法,或有一个 fromString (或 fromInt / fromDate - 取决于标量类型)静态方法。

To be able to do the above, the Phone object needs to have a constructor that takes a String (or Int / Date / etc.), or have a setter method for the String (or Int / Date / etc.), or have a fromString (or fromInt / fromDate - depending on the Scalar type) static method.

例如:

For example:

public class Phone {

    private String number;

    // Getters and setters....

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

请参阅 JavaDoc 中的 @AdaptToScalar 特征的更多信息。

See more about the @AdaptToScalar feature in the JavaDoc.

Adapt with

对于更复杂的情况,另一个选项是提供适配器。之后,您便可以在适配器中自行进行映射。

Another option for more complex cases is to provide an Adapter. You can then do the mapping yourself in the adapter.

请参阅 AdaptWith 中有关 JavaDoc 功能的更多信息。

See more about the AdaptWith feature in the JavaDoc.

例如:

For example:

    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

@JsonbTypeAdapter is also supported.

Built-in support for Maps

默认情况下,由于映射很难在架构中建模(因为键和值在运行时可能是动态的),GraphQL 不支持默认映射。使用上述调整,已为 Quarkus 添加 Map 支持,并将其映射到具有可选键参数的 Entry<Key,Value>。这使您可以返回映射,也可以按键查询映射。

By default, due to the fact that maps are hard to model in a schema (as the keys and values can be dynamic at runtime) GraphQL does not support maps by default. Using the above adaption, Map support is added for Quarkus and are mapped to an Entry<Key,Value> with an optional key parameter. This allows you to return a map, and optionally query it by key.

示例:

Example:

    @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
    }

键和值对象可以是枚举、标量或复杂对象。

The key and value object can be any of Enum, Scalar or Complex object

现在,您可以使用所有字段来查询整个映射:

You can now query the whole map with all the fields:

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

例如,这将返回如下结果:

This will return a result like this for example:

{
  "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"
        }
      }
    ]
  }
}

您还可以按键查询

You can also query by key

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

这将只返回映射中的该值:

That will return only that value in the map:

{
  "data": {
    "language": [
      {
        "value": {
          "please": "asseblief",
          "thankyou": "dankie"
        }
      }
    ]
  }
}

可以用我们自己的实现来覆盖默认映射适配器。

The default map adapter can to overridden with our own implementation.

Error code

您可以在 GraphQL 响应中使用(SmallRye 特有的) @ErrorCode 在错误输出上添加错误代码:

You can add an error code on the error output in the GraphQL response by using the (SmallRye specific) @ErrorCode:

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

当出现 SomeBusinessException 时,错误输出将包含错误代码:

When SomeBusinessException occurs, the error output will contain the Error code:

{
    "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 The error code

Additional Notes

如果您正在使用 smallrye-graphql 扩展,并且存在且启用了 micrometer 指标扩展,则可能会遇到 java.lang.NoClassDefFoundError,因为某些版本的 smallrye-graphql 扩展对 Microprofile Metrics API 有运行时要求。添加以下 MicroProfile Metrics API 依赖项以解决此问题:

If you are using the smallrye-graphql extension and the micrometer metrics extension is present and metrics are enabled, you may encounter a java.lang.NoClassDefFoundError as some versions of the smallrye-graphql extension have runtime requirements on the Microprofile Metrics API. Add the following MicroProfile Metrics API dependency to resolve the issue:

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")

Conclusion

SmallRye GraphQL 使客户端能够检索所需的确切数据,从而避免 Over-fetchingUnder-fetching

SmallRye GraphQL enables clients to retrieve the exact data that is required preventing Over-fetching and Under-fetching.

可以对 GraphQL API 进行扩展,而不会中断以前的查询,从而可以轻松实现 API evolution

The GraphQL API can be expanded without breaking previous queries enabling easy API evolution.

Configuration Reference

Unresolved directive in smallrye-graphql.adoc - include::{generated-dir}/config/quarkus-smallrye-graphql.adoc[]