Quarkus 中的 SmallRye GraphQL

SmallRye GraphQL

本指南演示 Quarkus 应用如何使用 SmallRye GraphQL,它是 MicroProfile GraphQL 规范的一种实现。

GraphQL 规范网站这样介绍它:

GraphQL 是面向 API 的查询语言,也是基于现有数据执行这些查询的运行时。它为 API 数据提供完整、易懂的描述,使客户端准确获取所需内容,并便于 API 随时间演进,同时提供强大的开发工具。

GraphQL 最初由 Facebook 于 2012 年开发,自 2015 年起成为开放标准。

GraphQL 是 REST API 的另一种选择。它可以从以下方面帮助客户端:

避免过量获取与获取不足

典型 REST API 的响应结构由服务器决定。即使不需要所有字段,客户端也可能不得不接收全部数据,造成过量获取。反过来,客户端也可能需要根据第一次响应继续执行多个请求(例如 HATEOAS 场景),才能获取全部所需数据,形成获取不足。

API 演进

GraphQL 按客户端请求返回数据,因此为已有 API 增加字段和能力,通常不会破坏已有客户端。

前置条件

完成本指南需要:

  • 约 15 分钟。

  • 一个 IDE。

  • JDK 17 或以上版本,并正确配置 JAVA_HOME。

  • Apache Maven 3.9.16

  • 如需使用,可安装 Quarkus CLI。

  • 如需构建原生可执行文件,可安装并配置 Mandrel 或 GraalVM;也可以使用 Docker 执行原生容器构建。

架构

我们将构建一个简单应用,在 /graphql 暴露 GraphQL API。

本例受到原文所链接的一个常见 GraphQL API 示例启发。

完整示例

建议按照后面的步骤逐步创建应用,也可以直接查看完整示例。

克隆仓库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载原文链接中的归档。

完整代码位于 microprofile-graphql-quickstart 目录。

创建 Maven 项目

首先创建项目,执行以下命令:

CLI
quarkus create app org.acme:microprofile-graphql-quickstart \
    --extension='quarkus-smallrye-graphql' \
    --no-code
cd microprofile-graphql-quickstart

创建 Gradle 项目时,添加 --gradle 或 --gradle-kotlin-dsl。

CLI 的安装与使用详见 Quarkus CLI 指南。

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

使用 Maven 命令创建 Gradle 项目时,添加 -DbuildTool=gradle 或 -DbuildTool=gradle-kotlin-dsl。

Windows 用户请注意:

  • 在 cmd 中,不使用反斜杠续行,应把命令放在同一行。

  • 在 PowerShell 中,用双引号包住 -D 参数,例如 "-DprojectArtifactId=microprofile-graphql-quickstart"。

该命令生成项目并引入 smallrye-graphql 扩展。

对于已有 Quarkus 项目,在项目根目录执行以下命令添加扩展:

CLI
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
}
为便于阅读,这里使用公共字段;私有字段配合公共 getter/setter 同样适用。

这些类描述 GraphQL schema,即客户端能够访问的对象、字段和关系。

接着创建一个充当仓库的 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:

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

内省

可以通过以下请求取得完整 GraphQL schema:

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

服务器会返回 API 的完整 schema。

GraphQL UI

实验性功能,不属于 MicroProfile 规范。

GraphQL UI 便于直接与 API 交互。

smallrye-graphql 扩展附带 GraphiQL,默认在开发和测试模式启用。若希望生产模式也包含它,可将 quarkus.smallrye-graphql.ui.always-include 设为 true。

访问 http://localhost:8080/q/graphql-ui/ 打开 GraphQL UI。

GraphQL UI

为 UI 增减安全控制的方法,见“Web 端点授权”指南。

查询 GraphQL API

打开开发模式部署的 GraphQL UI。

输入以下查询并点击执行按钮:

query allFilms {
  allFilms {
    title
    director
    releaseDate
    episodeID
  }
}

该查询包含 Film 的全部字段,因此响应也返回全部字段。GraphQL 允许客户端自行选择需要的字段。

假设客户端只需要 title 和 releaseDate,前一个请求就获取了不必要的数据。

改为执行以下查询:

query allFilms {
  allFilms {
    title
    releaseDate
  }
}

响应只包含所需字段,避免了过量获取。

继续向 FilmResource 添加:

    @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;省略时默认是 id。

在 UI 中执行:

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

与前面的例子一样,可以自行选择 film 查询返回的字段,获取单部电影。

如果客户端同时需要 ID 为 0 和 1 的电影,按单资源端点设计的 REST API 通常需要两次调用。

GraphQL 可以在一次请求中执行多个查询。

执行以下查询获取两部电影:

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

客户端由此在一个请求中获得所需数据。

扩展 API

目前 API 只能查询电影,现在增加获取电影中英雄角色的能力。

在 FilmResource 中添加:

    public List<Hero> heroes(@Source Film film) { (1)
        return service.getHeroesByFilm(film);
    }
1 允许返回 Film 的查询同时获取 List<Hero> 数据。

这个方法改变了 schema,但旧查询仍可继续工作,因为这里只是新增获取电影角色数据的能力。

执行以下查询获取电影及角色:

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

响应现在包含电影中的英雄角色。

批量处理

当返回类似 getAllFilms 的集合时,可以使用批量形式,更高效地获取相关角色:

    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 时,请求在事件循环线程上执行,而不是工作线程。

一个请求可以混合阻塞和非阻塞操作:

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

上例在事件循环上获取电影,然后切换到工作线程获取角色。

RunOnVirtualThread

给方法添加 @RunOnVirtualThread,即可在虚拟线程中执行查询:

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

请同时遵循虚拟线程的通用使用指南。

事务

当存在 JTA 事务管理器(如 Hibernate ORM 或 quarkus-narayana-jta)时,扩展会自动为阻塞式 GraphQL 操作添加事务行为,等同于标注 @Transactional。这方便了需要读写数据库的解析器。

仅当以下条件全部满足时,才添加隐式事务:

  • 存在 TRANSACTIONS 能力,即类路径中有 JTA 扩展。

  • 不存在 Hibernate Reactive;后者由响应式会话自行管理事务。

  • 方法位于 @GraphQLApi 类中,且是 GraphQL 操作:@Query、@Mutation、@Subscription、@Name、@Resolver 或 @Source 方法。

  • 方法返回阻塞类型。Uni、Multi、CompletionStage、CompletableFuture、Publisher 等响应式返回类型不会被自动事务化。

  • 方法没有 @NonBlocking。

  • 方法尚未显式标注 @Transactional;显式配置总是优先。

若某个操作不需要此行为,例如调用的库自行管理 JDBC 事务,可以标注:

    @Query("aiGenerationQuota")
    @Transactional(Transactional.TxType.NOT_SUPPORTED)
    public AiQuota getRemainingQuota() {
        // ...
    }

也可以使用 REQUIRES_NEW 等其他 TxType 控制事务参与方式,因为显式注解优先于隐式事务。

抽象类型

当前 schema 只有 Hero 和 Film 两个具体类型。下面增加其他类型,并引入便于客户端使用的抽象。

接口

先为英雄增加盟友。

创建表示 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();
    }

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

schema 随之改变,新增 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 被定义为 GraphQL 字段。
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();
    }

在 UI 中执行:

query getCharacters {
    characters {
        name
        surname
    }
}

联合类型

实验性功能,不属于 MicroProfile 规范。

目前 API 只能直接查询某个实体或实体列表。现在希望支持跨实体搜索。Hero 和 Ally 共有 Character 抽象,但它不包含 Film。

创建表示搜索结果可能类型的新抽象:

package org.acme.microprofile.graphql;

import io.smallrye.graphql.api.Union;

@Union (1)
public interface SearchResult {

}
1 @Union 表明 Java 接口代表 GraphQL 联合类型,而不是 GraphQL 接口。
表示联合类型的 Java 接口不必为空,但其中的 getter 不会直接改变 GraphQL schema。

让实体实现 SearchResult:

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

public interface Character extends 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);
    }

在 UI 中执行:

query searchTheGalaxy {
    search(query: "a") {
        ... on Film {
            title
            director
        }
        ... on Character {
            name
            surname
        }
    }
}
SearchResult 联合包含实现 Character 的成员,因此可以在查询中使用 Character 接口。

变更操作(Mutation)

创建、更新或删除数据时使用 Mutation。

下面为 API 增加添加和删除英雄的能力。

向 FilmResource 添加:

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

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

执行以下 Mutation 插入英雄:

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

它会在服务中创建 Hero 实体。

响应包含新角色的 name 和 surname,因为 Mutation 的花括号选择集请求了这两个字段。也可以请求客户端需要的服务端生成字段。

再尝试删除条目:

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

与创建操作类似,花括号中的选择集让响应返回被删除角色的 name 和 surname。

订阅

订阅允许持续接收查询相关事件,底层使用 WebSocket。详情见 GraphQL over WebSocket 协议规范。

例如,在创建新英雄时接收通知:

    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 处理器。
2 添加新英雄时,同时广播它。
3 将该流暴露到 schema,并在运行时通过 WebSocket 提供。

客户端连接 /graphql 的 WebSocket 后,就能接收新英雄创建事件:

subscription ListenForNewHeroes {
  heroCreated {
    name
    surname
  }
}

按字段查询

还可以按单个字段查询,例如按姓氏查找英雄。

向 FilmResource 添加:

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

@DefaultValue 指定未提供 surname 参数时使用 Skywalker。

在 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);

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

通过观察者,可向 schema builder 添加自定义内容。

要使用观察者,需在 application.properties 中设置 quarkus.smallrye-graphql.events.enabled=true。

类型适配

适配到标量

另一项 SmallRye 专有实验功能,可以将已有标量对应的 Java 类型映射成其他标量类型,也可以将原本会生成 GraphQL Type 或 Input 的复杂对象映射到已有标量。

将已有标量映射为另一种类型

public class Movie {

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

    // ....
}

上例把 Java Long 映射为 Int 标量,而非默认 BigInteger。

将复杂对象映射为标量

public class Person {

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

    // ....
}

这样会映射为 String 标量,而不是创建 GraphQL Type 或 Input。

为支持这种转换,Phone 需要接收 String(或 Int、Date 等对应类型)的构造函数、相应 setter,或者 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;
    }
}

@AdaptToScalar 的详情见 JavaDoc。

通过适配器转换

更复杂的情况可以提供 Adapter,在其中自行完成映射。

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 的内置支持

Map 的键和值可能在运行时变化,因此难以描述在 schema 中,GraphQL 默认不支持 Map。Quarkus 利用上述适配机制,将 Map 映射为带有可选 key 参数的 Entry<Key,Value>,使你可以返回整个 Map,也可以按键查询。

例如:

    @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
    }
键和值均可为枚举、标量或复杂对象。

查询整个 Map 的所有字段:

{
  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"
        }
      }
    ]
  }
}
可以用自己的实现覆盖默认 Map 适配器。

错误码

使用 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 错误码。

JavaScript 客户端

将 quarkus.smallrye-graphql.js-client.enabled 设为 true 后,扩展在构建时生成 JavaScript 客户端库和类型化代理模块,作为 Web 资源。配合 Web Dependency Locator,可通过标准 import map 约定导入。

启用客户端

在 application.properties 中添加:

quarkus.smallrye-graphql.js-client.enabled=true

这会生成两个资源:

资源 用途

@quarkus/graphql

可复用的 GraphQLClient 类,提供 query()、mutate() 和 subscribe()。

@quarkus/graphql-api

类型化代理,为各个查询、变更和订阅导出函数。

使用类型化代理

生成的代理导出 Queries、Mutations、Subscriptions 对象,以及底层 client 实例:

import { client, Queries, Mutations } from '@quarkus/graphql-api';

// Call a query
const data = await Queries.allBooks();

// Call a query with variables
const result = await Queries.book({ id: 42 });

// Call a mutation
const created = await Mutations.createBook({ title: 'Quarkus in Action' });

每个函数接收与 GraphQL 操作参数对应的 variables 对象。包含选择集的 GraphQL 查询字符串在构建时自动生成。

订阅

订阅操作返回 Subscription 对象:

import { Subscriptions } from '@quarkus/graphql-api';

const sub = Subscriptions.onBookCreated()
    .onData(data => console.log('New book:', data))
    .onError(err => console.error('Error:', err))
    .onComplete(() => console.log('Stream completed'));

// Cancel later
await sub.cancel();

订阅使用 graphql-transport-ws WebSocket 子协议。

直接使用客户端库

需要更多控制时,可直接导入 GraphQLClient:

import { GraphQLClient } from '@quarkus/graphql';

const client = new GraphQLClient({ endpoint: '/graphql' });

// Custom query
const data = await client.query(`
    query {
        allBooks {
            title
            author { name }
        }
    }
`);

结语

SmallRye GraphQL 让客户端准确获取所需数据,减少过量获取与获取不足。

API 可以通过新增能力演进,同时保持已有查询兼容。

配置参考

带标记的配置项在构建时固定;其他配置可以在运行时覆盖。

配置项

类型

默认值

提供查询的根路径,默认 graphql;默认相对于 ${quarkus.http.root-path} 解析。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_ROOT_PATH

更多信息

string

graphql

启用 Apollo Federation。未明确设置时,如果检测到 GraphQL Federation 注解,就自动启用。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_FEDERATION_ENABLED

更多信息

boolean

启用 Federation 批量解析,默认关闭。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_FEDERATION_BATCH_RESOLVING_ENABLED

更多信息

boolean

启用指标,默认 false;启用时需要指标扩展。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_METRICS_ENABLED

更多信息

boolean

启用追踪。添加追踪扩展后默认启用。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_TRACING_ENABLED

更多信息

boolean

启用事件,让应用可以接收启动和执行期间的事件。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_EVENTS_ENABLED

更多信息

boolean

false

启用非阻塞支持,默认 true。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_NONBLOCKING_ENABLED

更多信息

boolean

类型命名策略,可选 default、merge-inner-class、full。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_AUTO_NAME_STRATEGY

更多信息

string

Default

将数据获取器异常写入日志;开发和测试模式默认 true,生产模式默认 false。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_PRINT_DATA_FETCHER_EXCEPTION

更多信息

boolean

通过 HTTP 提供 schema。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_AVAILABLE

更多信息

boolean

true

服务端支持的 GraphQL WebSocket 子协议,可选 graphql-ws、graphql-transport-ws,默认两者都启用。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_WEBSOCKET_SUBPROTOCOLS

更多信息

字符串列表

为所有 GraphQL 操作生成 JavaScript 客户端代理。启用后,会生成静态客户端库、类型化代理及裸模块导入所需的 import map。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_JS_CLIENT_ENABLED

更多信息

boolean

false

额外注册的标量,来自 graphql-java-extended-scalars 库。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_EXTRA_SCALARS

更多信息

可选 uuid(java.util.UUID)、object(java.lang.Object)、json(jakarta.json.JsonObject)。

客户端初始化负载中保存 Authorization 信息的键名,其值按 Authorization 请求头处理。优先使用请求头,但 JavaScript 等环境无法直接为 WebSocket 添加任意头。另一选择是 Bearer Token Authentication,它可在连接打开前注入头,有些情况下更合适。默认未定义,即不从初始化负载读取认证信息。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_AUTHORIZATION_CLIENT_INIT_PAYLOAD_NAME

更多信息

string

是否启用 GraphQL UI。默认在 UI 已包含时启用,参见 always-include。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_UI_ENABLED

更多信息

boolean

true

指定 schema 字段可见性。使用逗号分隔的 GraphQLType.GraphQLField 模式确定需要排除的字段;特殊值 no-introspection 禁用内省字段。详情见 graphql-java 文档。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_FIELD_VISIBILITY

更多信息

string

default

从响应 data 中排除 null 字段,但因解析失败产生的错误字段除外。默认关闭。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_EXCLUDE_NULL_FIELDS_IN_RESPONSES

更多信息

boolean

需要解包的异常类名。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_UNWRAP_EXCEPTIONS

更多信息

字符串列表

应公开消息的运行时异常类名列表。默认隐藏运行时异常消息,返回通用 Server Error。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_SHOW_RUNTIME_EXCEPTION_MESSAGE

更多信息

字符串列表

应隐藏消息的受检异常类名列表。默认公开受检异常消息。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_HIDE_CHECKED_EXCEPTION_MESSAGE

更多信息

字符串列表

隐藏异常消息时使用的默认文本,默认为 Server Error。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_DEFAULT_ERROR_MESSAGE

更多信息

string

错误响应包含的扩展字段列表,默认不包含。有效值包括 exception、classification、code、description、validationErrorType、queryPath。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_ERROR_EXTENSION_FIELDS

更多信息

字符串列表

允许通过 HTTP GET 查询。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_HTTP_GET_ENABLED

更多信息

boolean

false

允许 POST 请求通过查询参数提供或覆盖值。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_HTTP_POST_QUERYPARAMETERS_ENABLED

更多信息

boolean

false

在 schema 中包含标量定义。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_SCALARS

更多信息

boolean

false

在 schema 中包含 schema 自身的定义。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_SCHEMA_DEFINITION

更多信息

boolean

false

在 schema 中包含指令。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_DIRECTIVES

更多信息

boolean

false

在 schema 中包含内省类型。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_SCHEMA_INCLUDE_INTROSPECTION_TYPES

更多信息

boolean

false

向标准输出记录请求负载,并可选择包含变量。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_LOG_PAYLOAD

更多信息

off, query-only, query-and-variables

off

是否将被忽略的字符捕获为 AST 节点,默认 false。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_PARSER_CAPTURE_IGNORED_CHARS

更多信息

boolean

是否将 graphql.language.Comment 捕获为 AST 节点。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_PARSER_CAPTURE_LINE_COMMENTS

更多信息

boolean

是否将 graphql.language.SourceLocation 捕获为 AST 节点,默认 true。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_PARSER_CAPTURE_SOURCE_LOCATION

更多信息

boolean

解析器接受的原始 token 数上限,超过后抛出异常,默认 15000。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_PARSER_MAX_TOKENS

更多信息

int

解析器接受的原始空白 token 数上限,超过后抛出异常,默认 200000。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_PARSER_MAX_WHITESPACE_TOKENS

更多信息

int

查询的数据字段总数超过限制时中止,默认不限制。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_INSTRUMENTATION_QUERY_COMPLEXITY

更多信息

int

查询总深度超过限制时中止。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_INSTRUMENTATION_QUERY_DEPTH

更多信息

int

20

自 3.26 起弃用,改用 quarkus.smallrye-graphql.ui.enabled。

是否启用 GraphQL UI,默认在已包含 UI 时启用,参见 always-include。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_UI_ENABLE

更多信息

boolean

SmallRye GraphQL UI 配置

类型

默认值

GraphQL UI 路径。不允许使用 /,否则会阻止应用提供其他内容。默认相对于 ${quarkus.http.non-application-root-path} 解析。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_UI_ROOT_PATH

更多信息

string

graphql-ui

始终包含 UI。默认只在开发和测试中包含;设为 true 后生产环境也会包含。

环境变量: QUARKUS_SMALLRYE_GRAPHQL_UI_ALWAYS_INCLUDE

更多信息

boolean

false

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容