Aller au contenu
BackJavaquarkusGraphQL

La Taverne GraphQL : aller plus loin avec le client Quarkus

Le client typesafe couvre la plupart des besoins GraphQL avec Quarkus, mais il ne répond pas à tous les cas. Génération automatique depuis le schéma, client dynamique, requêtes construites à la volée : explorons les outils prévus pour aller plus loin.

La Taverne GraphQL : aller plus loin avec le client Quarkus

L'antenne de la guilde fonctionne désormais correctement.
Elle interroge la taverne principale, récupère les aventuriers, envoie les nouveaux arrivants au registre, et le tout repose sur un client typesafe généré automatiquement par SmallRye GraphQL à partir de quelques interfaces Java.

Mais deux questions sont restées sans réponse dans l'article précédent.

La première : si le schéma GraphQL de la taverne existe déjà, pourquoi écrire les modèles Java à la main ? SmallRye GraphQL propose un générateur expérimental capable de produire ces classes automatiquement depuis le schéma distant. La seconde : le client typesafe convient parfaitement quand les opérations sont connues à l'avance, mais que se passe-t-il quand le messager doit improviser, quand la requête ne peut pas être figée dans une interface Java ?

Ce sont ces deux chemins que nous allons explorer.

Générer le client depuis le schéma : la promesse du générateur

Jusqu'ici, l'antenne de la guilde recopiait patiemment les modèles de la taverne principale.
Elle relevait chaque champ qui l'intéressait, créait ses classes Java à la main, et s'assurait que les noms correspondaient bien aux types GraphQL distants.
Un travail de scribe appliqué, mais fastidieux, et surtout fragile : si la taverne fait évoluer son registre, l'antenne doit s'en apercevoir seule et corriger ses propres copies sans se tromper.

Dans l'écosystème Quarkus, SmallRye GraphQL propose une réponse à ce problème sous la forme d'un générateur de code.
L'idée est séduisante : on dépose le schéma de la taverne dans les ressources du projet, on déclare les requêtes que l'antenne souhaite exécuter, et le générateur produit automatiquement les classes Java correspondantes à la compilation. Plus de recopie manuelle, plus de risque de désalignement entre le registre de la taverne et les modèles de l'antenne.

Pour l'équiper, deux ajouts suffisent. D'abord la dépendance :

<dependency>
    <groupId>io.smallrye</groupId>
    <artifactId>smallrye-graphql-client-generator</artifactId>
    <version>3.0.0.Beta1</version>
</dependency>

Le module pour la génération de client

Puis la configuration du compilateur Maven pour activer le processeur d'annotations :

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
        <parameters>true</parameters>
        <annotationProcessorPaths>
            <path>
                <groupId>io.smallrye</groupId>
                <artifactId>smallrye-graphql-client-generator</artifactId>
                <version>3.0.0.Beta1</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

On active l'annotation processor du module

Le schéma de la taverne, récupéré depuis /graphql/schema.graphql, est déposé dans les ressources du projet.
L'antenne déclare ensuite ses requêtes directement sur une interface, en GraphQL pur, sans avoir à écrire les modèles Java à la main :

@GraphQLSchema("resource:schema.graphql")
@GraphQLQuery("""
query Aventuriers {
  aventuriers {
    id
    nom
    classe
    niveau
    quetes {
      id
      titre
      difficulte
      recompenseOr
    }
  }
}
""")
@GraphQLQuery("""
query Aventurier($id: Int) {
  aventurier(id: $id) {
    id
    nom
    classe
    niveau
    quetes {
      id
      titre
      difficulte
      recompenseOr
    }
  }
}
""")
public interface TavernQuery {
}

Notre schéma

Le générateur lit le schéma, analyse les requêtes déclarées, et produit à la compilation les classes Java correspondantes.
Sur le papier, c'est exactement ce dont l'antenne avait besoin : un scribe automatique qui recopie fidèlement le registre de la taverne sans intervention humaine.

public interface TavernQueryApi {
    @Query("aventuriers") @NonNull List<@NonNull AventurierResponse> Aventuriers();
    @Query("aventurier") AventurierResponse Aventurier(Integer id);
}

public class AventurierResponse {
    @NonNull String classe;
    @NonNull Integer id;
    @NonNull Integer niveau;
    @NonNull String nom;
    @NonNull List<@NonNull QueteResponse> quetes;
}

Une partie des classes générées

Mais en pratique, le scribe a ses lacunes. Lors de nos tests, le générateur n'a produit les types imbriqués qu'à moitié : la classe Quete ne contenait que son champ id, les autres champs ayant tout simplement disparu en chemin.
Ce n'est pas un cas limite obscur : c'est le comportement de base sur des objets GraphQL imbriqués, qui sont pourtant au cœur de la plupart des API réelles.

public class QueteResponse {
    @NonNull Integer id;
}

La raison de ces lacunes devient claire en creusant un peu : le module smallrye-graphql-client-generator n'a pas reçu la moindre évolution depuis cinq ans. En ouvrant une issue pour signaler le problème des types imbriqués, la réponse de l'équipe a été directe, ils reconnaissent eux-mêmes que le générateur est resté sans attention, et évoquent vaguement la possibilité de le ressusciter un jour.

Missing field in nested class after using the generator · Issue #2481 · smallrye/smallrye-graphql
Hello, I’m currently testing the smallrye-graphql-client-generator with the version 3.0.0.Beta1, and encountered something strange. I have my graphql schema in src/main/resources “Données publiques…

L'issue ouverte sur ce problème

Une intention sympathique, mais qui ne résout rien aujourd'hui.

Le client dynamique : quand le messager doit improviser

Le client typesafe repose sur une hypothèse forte : les opérations sont connues à l'avance.
L'interface Java déclare les Query et les Mutation disponibles, le framework génère les requêtes correspondantes, et les messagers partent toujours avec des instructions précises gravées dans le parchemin.

Mais un soir, un aventurier inhabituel pousse la porte de l'antenne.
Ce n'est pas un guerrier venu consulter la liste des quêtes, ni un mage cherchant les récompenses en or. C'est un cartographe itinérant qui ne sait pas encore exactement ce dont il a besoin.
Il veut explorer le registre librement, composer sa propre demande selon ce qu'il découvre en chemin, choisir les champs qui l'intéressent au dernier moment, sans que personne n'ait pu anticiper sa requête à l'avance.

Face à lui, le client typesafe est désarmé. Une interface Java figée ne peut pas répondre à une question qui change de forme à chaque visite. Il faut un messager différent, capable d'improviser sa mission plutôt que de suivre un script : c'est le client dynamique.

@Inject
@GraphQLClient("tavern-dynamic")
DynamicGraphQLClient dynamicClient;

Plutôt que de s'appuyer sur une interface annotée, ce messager construit ses requêtes à la volée, champ par champ, selon les besoins du moment :

Document query = document(
    operation(
        field("aventuriers",
            field("nom"),
            field("niveau"),
            field("quetes",
                field("titre"),
                field("recompenseOr")
            )
        )
    )
);

Response response = dynamicClient.executeSync(query);  
return response.getList(Map.class, "aventuriers");

La requête n'est plus gravée dans le marbre avant le départ. Elle se compose au moment où le cartographe formule sa demande, s'adapte à ce qu'il cherche, et peut prendre une forme complètement différente à sa prochaine visite.
L'antenne répond sans avoir eu besoin de prévoir chaque combinaison possible.

Lorsque le messager ne peut pas non plus se permettre d'attendre la réponse de la taverne avant de continuer sa route, il peut partir en éclaireur et récupérer les informations de façon asynchrone :

Uni<Response> response = dynamicClient.execute(query);

Mais cette liberté a un prix. En abandonnant l'interface Java typesafe, on perd le filet de sécurité que le compilateur offrait.
Il ne peut plus vérifier que les champs demandés existent réellement dans le registre de la taverne, ni que les types correspondent. Si la taverne renomme une colonne ou restructure ses aventuriers, le messager ne s'en apercevra qu'en revenant bredouille, à l'exécution, sans que personne ne l'ait prévenu au départ.

C'est pourquoi le client dynamique n'est pas destiné à remplacer le client typesafe dans le quotidien de l'antenne.
Il coexiste avec lui, disponible pour les situations où la rigidité d'une interface Java devient un obstacle plutôt qu'une protection : exploration d'un schéma inconnu, construction d'outils génériques, ou requêtes qui doivent vraiment se composer à la volée selon des paramètres imprévisibles.

Pour tout le reste, le messager avec ses instructions écrites reste le choix le plus sûr.

Conclusion

L'antenne de la guilde dispose maintenant d'une vision complète de ce que SmallRye GraphQL Client peut offrir à une taverne qui cherche à grandir.

Le client typesafe couvre la grande majorité des missions quotidiennes : les opérations sont connues, les modèles Java sont solides, et les messagers partent toujours avec des instructions claires. Le générateur expérimental pousse cette logique encore plus loin en confiant le travail de recopie du registre à un outil automatique, au prix d'une certaine prudence sur sa stabilité actuelle. Et le client dynamique garde une place de choix pour les visiteurs inhabituels, ceux dont les demandes ne pouvaient pas être anticipées et qui ont besoin d'un messager capable de s'adapter sur le chemin.

Dans un royaume bien organisé, chaque type de messager connaît son rôle. C'est ce qui distingue une antenne qui tient la route d'une échoppe qui improvise à chaque nouvelle demande.


Tout le code relatif à cet article est consultable ici :

GitHub - ErwanLT/quarkus-demo: Demo project for quarkus possibility
Demo project for quarkus possibility. Contribute to ErwanLT/quarkus-demo development by creating an account on GitHub.

Dernier