SmallRye GraphQL

本指南演示了您的 Quarkus 应用程序如何使用 SmallRye GraphQL,即 MicroProfile GraphQL规范的实现。 正如 GraphQL规范网站所述: 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 并不是 REST API 规范的替代品,而仅仅是一种替代方案。与 REST 不同,GraphQL API 能够通过以下方式使客户端受益: 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

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Architecture

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

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

Solution

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

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

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

Creating the Maven Project

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

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

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

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

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

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

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。

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

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
}

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

我们刚刚创建的类描述了 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,我们稍后将对其进行扩展。

Launch

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

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

Introspect

可通过调用以下内容来检索 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 模式显式进行配置。

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

graphql ui screenshot01

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

Query the 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 注释将参数名称更改为 filmId,而非省略 @Name 注释时的默认名称 id.

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

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

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

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

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

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

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

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

Expanding the API

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

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

    public List<Hero> heroes(@Source Film film) { (1)
        return service.getHeroesByFilm(film);
    }
1 使 List&lt;Hero&gt; 数据能够被添加到使用 Film 响应的查询中

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

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

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

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

Batching

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

    public List<List<Hero>> heroes(@Source List<Film> films) { (1)
        // Here fetch all hero lists
    }
1 此处批量接收电影,让您可以获取相应的英雄。

Non blocking

可以通过将 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`表示请求将在事件循环线程上执行,而不是在工作线程上。

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

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

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

Abstract Types

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

Interfaces

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

首先,创建一个新实体来表示我们的 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
    }
}

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

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

public interface Character {

    (1)
    String getName();
    String getSurname();
}
1 在接口中定义的 getter 将定义其包含的 GraphQL 字段

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

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 由于接口无法定义字段,所以我们必须实现 getter

通过添加一个接口并更新现有实体来实现它,我们有效地更改了模式。更新的模式现在将包括新的 `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`接口定义,将 getter 作为字段
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
    }
}

Unions

实验性 - 未包含在 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 接口不必为空,但任何定义的 getter 都不会显式更改 GraphQL 模式。

更新我们的实体以实现 SearchResult

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

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

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

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

更新 `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 联合包含实现它的成员。

Mutations

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

让我们现在在我们的 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
  }
}

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

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

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

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

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

Subscriptions

订阅允许您订阅查询。它允许您接收事件并正在使用 Web 套接字。请参阅 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 Multi 处理器将广播任何新的 `Hero`es
2 在添加新的 Hero 时,还要广播它
3 在运行时通过 schema 和 WebSocket 提供流

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

subscription ListenForNewHeroes {
  heroCreated {
    name
    surname
  }
}

Creating Queries by fields

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

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

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

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

使用 GraphQL UI 测试以下查询:

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

Context

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

@Inject
Context context;

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

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

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

  • the original request (Query/Mutation)

  • the arguments

  • the path

  • the selected fields

  • any variables

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

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

GraphQL-Java

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

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

在生成 Schema 期间,您还可以访问底层 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,您可以向 Schema 构建器添加任何内容。

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

Adapting

Adapt to Scalar

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

Adapting an existing Scalar to another type:

public class Movie {

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

    // ....
}

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

Adapting a complex object to a Scalar type:

public class Person {

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

    // ....
}

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

为了能够执行上述操作, Phone 对象需要有一个使用 String 的构造器(或 Int / Date / 等),或为 String(或 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;
    }
}

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

Adapt with

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

请参阅 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

Built-in support for Maps

默认情况下,由于映射很难在架构中建模(因为键和值在运行时可能是动态的),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
    }

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

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

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

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

Error code

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

@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 The error code

Additional Notes

如果您正在使用 smallrye-graphql 扩展,并且存在且启用了 micrometer 指标扩展,则可能会遇到 java.lang.NoClassDefFoundError,因为某些版本的 smallrye-graphql 扩展对 Microprofile Metrics API 有运行时要求。添加以下 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")

Conclusion

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

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

Configuration Reference

Unresolved include directive in modules/ROOT/pages/smallrye-graphql.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-smallrye-graphql.adoc[]