Créer une application GraphQL avec Kotlin

Magazine
Marque
GNU/Linux Magazine
HS n°
Numéro
121
Mois de parution
octobre 2022
Spécialité(s)


Résumé

Quand on entend Kotlin, on pense tout de suite à Android, découvrez comment on peut l’utiliser pour développer une API GraphQL.


Body

Le code développé ici a été écrit sous Linux Mint 20.3 édition Xfce.

Le code source du projet est disponible à l’adresse suivante : https://github.com/imikado/articleLMgraphQLwithKotlin

Quand on évoque le terme API, on pense tout de suite à REST, mais savez-vous qu’il existe une autre qui mériterait votre attention ? Il s’agit de GraphQL, qui permet de littéralement requêter votre API pour obtenir très naturellement et facilement l’ensemble des données qui vous intéresse, sans nécessiter une multitude de requêtes sur des endpoints différents.

Maintenant si vous entendez Kotlin, vous pensez directement à Android, mais savez-vous qu’il permet également de développer toute forme d’application, dont des API web ?

Eh bien, combinez le tout et vous obtenez le mariage efficace d’une API naturelle, et d’un langage moderne et élégant. C’est ce que nous verrons dans cet article.

1. GraphQL : « Ask for what you need, get exactly that »

En 2012, Facebook créé GraphQL et trois ans plus tard, l’entreprise décide de le rendre open source.

GraphQL, si l’on reprend la définition de Red Hat, est un langage de requête et un environnement d'exécution côté serveur pour les API qui s'attache à fournir aux clients uniquement les données qu'ils ont demandées, et rien de plus. Il permet aux développeurs de créer des requêtes qui extraient les données de plusieurs sources à l'aide d'un seul appel.

Avec une syntaxe proche du JSON, il permet de recevoir en miroir les éléments demandés, par exemple une requête de ce type :

query{
    hero{
        firstName
        lastName
        father{
            firstName
            lastName
        }
    }
}

Renverrait une réponse de ce type :

{
"hero":{
        "firstName":"Luke"
        "lastName":"Skywalker"
        "father":{
            "firstName": "Anakin"
            "lastName": "Skywalker"
        }
}

Si vous décidez de ne pas demander un des champs, ou un des objets comme ici le père, l’API ne le retournera pas et ne fera pas les requêtes associées pour le récupérer.

2. Kotlin

En 2010, JetBrains, la célèbre société d’édition de logiciel de développement décide de créer son propre langage pour pallier les défauts de Java, mais avec une contrainte importante : être compatible avec l’écosystème JVM. Ce langage moins verbeux, plus moderne et surtout compatible avec l’écosystème Java existant séduisit rapidement le public et poussa Google à le proposer, puis à le conseiller pour développer ses applications Android.

Puisque des lignes de code valent plus que des mots, voici un exemple de deux classes écrites en Java et son équivalent en Kotlin.

En Java :

public class User{
    
    private String firstName;
    private String lastName;
 
    public User(String firstName, String lastName){
        this.firstName=firstName
        this.lastName=lastName
    }
 
    public getFirstName(){ return firstName; }
    public getLastName(){ return lastName; }
 
    public setFirstName(String firstName){ this.firstName=firstName; }
    public setLasttName(String lastName){ this.lastName=lastName; }
 
}

En Kotlin :

class User( var firstName:String, var lastName:String)

On remarque ici le côté simple et concis de Kotlin, et on appréciera le fait de ne pas se soucier des points-virgules.

Autre exemple, cette fois avec une fonction.

En Java :

public void addUser(String firstName, int age, String city){
    list<User> userList = new ArrayList<>();
    userList.add(new User("Luke", 20, "Polis Massa") );
    userList.add(new User("Anakin", 20, "Polis Massa") );
 
    User newUser = new User(firstName,age,city);
    newUser.setCity("MetaversCity");
    newUser.incrementAge();
 
    userList.add(newUser)
 
    System.out.println(userList.get(userList.size()-1);
    
}

En Kotlin :

fun addUser(firstName:String,age:Int,city:String){
    val userList=mutableListOf(
        User("Luke", 10, "Polis Massa")
        User("Anakin", 32, "Tatooine"
    )
 
    User(firstName,age,city).let{
        it.city= "MetaversCity"
        it.incrementAge()
 
        userList.add(it)
        
        println(userList.last() )
    }
}

On notera ici la simplicité de créer d’une traite une liste en écrivant naturellement les objets qui la composent. Et on remarquera l’utilisation du mot-clé it pour faire référence au contexte ici utilisateur que l’on ajoute à la volée.

3. L’éditeur de code

Pour développer notre application web en Kotlin, je vous recommande d’utiliser l’IDE de JetBrains : IntelliJ.

Rendez-vous à l’adresse https://www.jetbrains.com/fr-fr/idea/ pour télécharger le logiciel.

Une fois l’archive .tar.gz téléchargée, désarchivez-la dans le répertoire de votre choix et exécuter le bash bin/idea.sh pour lancer l’éditeur.

4. Création de l’application

L’application que nous allons créer s’appuiera sur le framework Spring Boot, et plutôt que de démarrer d’un projet totalement vide, nous allons passer par un site qui nous permettra de créer la base de départ.

Rendez-vous à l’adresse : https://start.spring.io/.

Renseignez le formulaire comme sur la figure 1 pour indiquer que l’on souhaite créer un projet en utilisant le langage Kotlin plutôt que Java, que l’on veut la version 2.7.0 de Spring Boot, un packaging en JAR et la version 17 de Java.

createGraphQLApplicationWithKotlin image01-s

Fig. 1 : Formulaire de génération d’une application Spring.

Vous n’avez pas besoin ici de sélectionner des dépendances, nous les ajouterons ensuite dans Gradle directement. Vous pouvez cliquer sur Generate pour télécharger une archive de votre projet.

Désarchivez-la dans le répertoire de travail de votre choix, par exemple pour notre article :

mkdir -p /home/mika/code/LM/kotlin/
cd /home/mika/code/LM/kotlin/
mv /home/mika/Downloads/tutorialKotlinGraphQL.zip .
unzip tutorialKotlinGraphQL.zip

Vous pouvez lancer IntelliJ et ouvrir ce nouveau projet en cliquant sur File > Open puis en sélectionnant ce répertoire.

5. Ajout de la librairie GraphQL dans l’application

Pour information, il existe plusieurs librairies pour faire du GraphQL avec Kotlin, dans cet article, nous utiliserons celle développée par le groupe Expedia : la société de voyage propose sa librairie en open source à l’adresse https://opensource.expediagroup.com/graphql-kotlin/docs/.

Pour commencer et nous familiariser avec la technologie, nous commencerons par créer une première implémentation très simple en GraphQL, puis nous ferons un exemple plus complexe et concret.

Nous avons besoin ici d’ajouter uniquement une dépendance, celle de la librairie GraphQL du groupe.

Éditez le fichier build.gradle.kts :

dependencies {
    ...
    implementation("com.expediagroup", "graphql-kotlin-spring-server", "3.6.8") // graphql-kotlin
    ...
}

Après avoir ajouté la ligne de la librairie, vous verrez une petite icône en haut à droite permettant de prendre en compte la modification du fichier Gradle et donc de télécharger les nouvelles dépendances ajoutées.

Après l’installation de cette dépendance, il nous faut créer un fichier pour indiquer à la librairie où trouver notre implémentation GraphQL : en effet, cette librairie utilise l’introspection pour générer l’API.

Créez un fichier resources/application.yml :

graphql:
  packages:
    - "com.mika.tutorialKotlinGraphQL"

On indique ici le namespace de notre projet : vous pouvez le retrouver en entête du fichier Kotlin situé dans votre répertoire main/kotlin.

6. Il était une fois notre première route GraphQL

En GraphQL, on distingue deux types de routes : les Query qui permettent de lire les informations et les Mutations qui permettent de les modifier.

Pour créer une première route de lecture, nous allons créer une classe qui héritera de la classe Query et lui ajouter des méthodes publiques qui seront nos routes API.

Note : pour éviter d’avoir des chemins de fichiers trop longs dans cet article, j’indiquerai uniquement kotlin/.../ à la place de kotlin/com/mika/tutorialKotlinGraphQL/.

Avant de créer notre première classe, nous allons commencer par organiser un minimum notre projet : créer un package graphql, à l’intérieur deux packages query et types et dans ce dernier un package entity.

Commençons par créer l’entité qui sera retournée lors d’une requête.

Fichier kotlin/.../graphql/types/entity/Fruit.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.entity
 
import com.expediagroup.graphql.annotations.GraphQLDescription
 
@GraphQLDescription("A simple Fruit")
data class Fruit(
    val id: Long,
    val name: String
)

Nous aurons ici de simples fruits avec un identifiant et un nom. Notez l’utilisation d’une annotation qui sera utilisée par la suite pour documenter automatiquement notre API. Passons désormais aux routes qui nous permettront de les requêter.

Fichier kotlin/.../graphql/query/FruitsQuery.kt :

package com.mika.tutorialKotlinGraphQL.graphql.query
 
import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.spring.operations.Query
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Fruit
import org.springframework.stereotype.Component
 
@Component
class FruitsQuery : Query {
 
    var fruitList : List<Fruit> = listOf(
        Fruit(1,"Apple"),
        Fruit(2, "Pear"),
        Fruit(3,"Orange")
    )
 
    @GraphQLDescription("Return all fruits")
    fun allFruits(): List<Fruit>{
        return fruitList
    }
 
    @GraphQLDescription("Return a fruit with an id")
    fun fruit(id: Long): Fruit?{
        return fruitList.find { id == it.id }
    }
}

Nous avons ici deux routes très simples, l’une qui permet de récupérer l’ensemble des fruits, et l’autre uniquement l’un d’entre eux via son identifiant.

Nous utilisons également ici des annotations de documentation pour indiquer le rôle de chaque route. Dans ce premier exemple, on fait très simple juste pour bien comprendre le fonctionnement : on se contente d’un simple tableau d’objet Fruit que l’on retourne dans sa totalité ou que l’on filtre selon l’identifiant reçu.

Lançons notre projet pour observer cette première implémentation.

Ouvrez le fichier kotlin/.../TutorialKotlinGraphQlApplication.kt :

package com.mika.tutorialKotlinGraphQL
 
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
 
@SpringBootApplication
class TutorialKotlinGraphQlApplication
 
fun main(args: Array<String>) {
    runApplication<TutorialKotlinGraphQlApplication>(*args)
}

Vous pouvez remarquer à gauche de la fonction main un bouton en forme de triangle vert pour exécuter/lancer l’application. Cliquez dessus et sélectionnez run.

Vous verrez dans la sortie d’IntelliJ l’affichage du schéma de l’API exposée, basée sur l’introspection des classes du namespace paramétré dans notre fichier de configuration. Votre instance est lancée, elle écoute sur le port 8080.

Si vous ouvrez votre navigateur à l’adresse http://localhost:8080/, vous aurez une erreur 404, en effet actuellement votre instance ne comprend que les requêtes GraphQL adressées en HTTP via la méthode POST et au chemin /graphql, ce qui donne l’URL http://localhost:8080/graphql.Vous avez ici deux solutions pour tester votre instance : soit lancer votre logiciel client API préféré, Postman ou Insomnia, soit apprécier la présence d’un client API intégré, Playground IDE.

Nous utiliserons cette deuxième option : ouvrez l’URL http://localhost:8080/playground pour observer une application web permettant de requêter votre API comme sur la figure 2.

createGraphQLApplicationWithKotlin image02-s

Fig. 2 : L’application Playground permettant de tester l’API et de lire sa documentation.

S’appuyant sur le schéma de votre application, l’application web propose l’autocomplétion contextuelle et vous propose sur sa droite la documentation automatiquement générée via le schéma généré de votre API.

Je vous invite à entrer la requête suivante pour utiliser les deux routes précédemment écrites.

query{
  
  fruit(id:2){
    id
    name
}
  
  allFruits{
    id
    name
  }
  
}

En GraphQL, nous obtenons en réponse le miroir de notre demande, nous aurons donc ici dans un premier temps un objet correspondant à l’identifiant 2, puis un tableau de l’ensemble des fruits sous cette forme :

{
  "data": {
    "fruit": {
      "id": 2,
      "name": "Pear"
    },
    "allFruits": [
      {
        "id": 1,
        "name": "Apple"
      },
      {
        "id": 2,
        "name": "Pear"
      },
      {
        "id": 3,
        "name": "Orange"
      }
    ]
  }
}

7. Connectons-nous à une base de données

Passons la seconde, maintenant que vous avez pu apprécier la simplicité d’écriture d’une API GraphQL simplissime, nous allons pouvoir utiliser une véritable base de données MariaDB.

7.1 Création d’une base MariaDB avec Docker

On ne présente plus ici ni Docker ni Docker Compose, je vais donc passer directement ici à la création d’un fichier Docker Compose pour avoir d’une part une instance SGBD MariaDB ainsi qu’une application web pour l’administrer : ici phpMyAdmin.

Créez un fichier docker-compose.yaml :

version: '2'
 
services:
  db:
    image: mariadb
    volumes:
      - /home/mika/code/docker/mysql:/var/lib/mysql
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=VOTREMOTDEPASSEROOT
 
  myadmin:
    image: phpmyadmin/phpmyadmin
    ports:
      - "8081:80"
    links:
      - db:db

On a ici deux configurations : une pour notre SGBD, l’autre pour l’application web qui permettra de l’administrer.

Notez que pour la première partie, j’ai défini un volume afin de ne pas perdre les données de la base à chaque extinction/redémarrage de Docker. Je vous conseille de faire de même sur vos postes (vous verrez plus tard dans l’article que cela peut s’avérer très pratique pour analyser nos logs SQL).

Une fois ce fichier créé, pour démarrer les deux instances, il vous suffit de lancer la commande :

docker-compose up

Vous pouvez ouvrir votre navigateur à l’adresse http://localhost:8081 pour accéder à phpMyAdmin avec les identifiants root/VOTREMOTDEPASSEROOT.

7.2 Créons une base de données

Pour cette partie, nous allons partir sur d’autres entités : des livres, leur auteur et leur catégorie.

Créez une base tutoKotlinGraphQLdb avec le code SQL suivant :

CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(150) NOT NULL,
  `author_id` int(11) NOT NULL,
  `category_id` int(11) NOT NULL,
 
    PRIMARY KEY (`id`)
) ;
 
CREATE TABLE `authors` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `firstname` varchar(150) NOT NULL,
  `lastname` varchar(150) NOT NULL,
 
    PRIMARY KEY (`id`)
) ;
 
CREATE TABLE `categories` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(150) NOT NULL,
 
    PRIMARY KEY (`id`)
) ;

Nous allons juste ajouter quelques auteurs qui nous permettront de vérifier que la communication avec la base de données fonctionne bien en exécutant le code SQL suivant :

insert into authors (firstname,lastname) VALUES ('Isaac' ,'Asimov'), ('Ray', 'Bradbury'), ('JRR','Tolkien');

7.3 Ajoutons les librairies de communication avec MariaDB

Nous utiliserons ici plusieurs choses pour communiquer avec celle-ci.

Ajoutez dans votre fichier build.gradle.kts :

dependencies {
    ...
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
 
    runtimeOnly("dev.miku:r2dbc-mysql") // MySQL database r2dbc driver
    runtimeOnly("mysql:mysql-connector-java") // MySQL database
    ...
}

Passons au paramétrage de notre base dans l’application resources/application.yml :

...
spring:
  r2dbc:
    initialization-mode: never
    initialize=false:
    url: r2dbc:mysql://127.0.0.1:3306/tutoKotlinGraphQLdb?useSSL=false&useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&serverTimezone=UTC
    username: root
    password: VOTREMOTDEPASSEROOT

À cet instant (c’est toujours bien de vérifier au fur et à mesure), vous devriez toujours pouvoir exécuter votre application sans erreur. Si ce n’est pas le cas, je vous invite à bien revérifier dans l’article, ou à regarder sur le dépôt GitHub de l’article.

7.4 Créons les classes d’accès à la base

Commençons par créer celles qui représenteront nos entités en base : Book, Author, Category.

Fichier kotlin/.../graphql/types/entity/Author.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.entity
 
data class Author (
    val id: Long,
    val firstname: String,
    val lastname: String,
 
)

Fichier kotlin/.../graphql/types/entity/Category.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.entity
 
data class Author (
    val id: Long,
    val firstname: String,
    val lastname: String,
 
)

Fichier kotlin/.../graphql/types/entity/Book.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.entity
 
data class Book (
    val id: Long,
    val title: String,
    val author_id: Long,
    val category_id: Long
)

Il ne manque plus que les classes qui exécuteront les requêtes SQL pour nous retourner les objets instanciant ces classes.

Créons un package repository.

Fichier kotlin/.../repository/AuthorsRepository.kt :

package com.mika.tutorialKotlinGraphQL.repository
 
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Author
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.reactive.ReactiveCrudRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
 
 
@Repository
interface AuthorsRepository : ReactiveCrudRepository<Author, Long> {
 
    @Query("SELECT id, firstname, lastname FROM Authors")
    override fun findAll(): Flux<Author>
 
    @Query("SELECT id, firstname, lastname FROM Authors WHERE id = :id")
    override fun findById(id: Long): Mono<Author>
}

Je n’affiche ici que la classe pour les auteurs qui va nous permettre de voir l’implémentation entière. Ici, rien de bien compliqué, notez que j’utilise les annotations Query afin de spécifier précisément les requêtes. De base, la librairie ReactiveCrudRepository propose nativement des méthodes pour récupérer ces mêmes valeurs, mais d’une part, ici vous auriez eu une erreur, car les noms des classes (au singulier) ne correspondent pas au nom des tables (au pluriel), de plus, si vous devez effectuer des jointures et/ou faire appel à des vues, il est toujours intéressant de savoir comment écrire vos propres requêtes.

On notera en regardant la signature de nos méthodes que l’on ne retourne pas directement des objets, mais un type Flux pour les listes d’objets et un type Mono pour les objets.

Ces deux types issus du « Reactive programming » (forme de programmation asynchrone) permettent de retourner des flux de données pour le cas des listes et ainsi permettre de commencer à traiter l’information dès les premiers enregistrements reçus, plutôt que d’attendre que la totalité soit arrivée avant de continuer.

7.5 Nos premières routes utilisant notre base MariaDB

Nous allons utiliser les packages précédemment conçus pour les fruits pour créer nos premières routes Query connectées.

Fichier kotlin/.../graphql/query/AuthorsQuery.kt :

package com.mika.tutorialKotlinGraphQL.graphql.query
 
import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.spring.operations.Query
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Author
import com.mika.tutorialKotlinGraphQL.repository.AuthorsRepository
import kotlinx.coroutines.reactive.awaitFirst
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.stereotype.Component
 
@Component
class AuthorsQuery(private val authorsRepository: AuthorsRepository) : Query {
 
    @GraphQLDescription("Return all authors")
    suspend fun allAuthors(): List<Author>{
        return authorsRepository.findAll().collectList().awaitFirst()
    }
 
    @GraphQLDescription("Return an author by his id")
    suspend fun author(id: Long): Author?{
        return authorsRepository.findById(id).awaitSingle()
    }
}

Si nous observons cette implémentation, par rapport à la première de cet article où nous manipulions un tableau de fruits, on remarque d’une part qu’on utilise l’autowiring pour obtenir via notre constructeur notre classe repository.

Puis dans nos routes exposées, on se contente d’appeler les méthodes précédemment écrites dans notre repository pour retourner soit la liste totale de nos auteurs, soit un seul via son identifiant.

Vous remarquerez que pour la liste des auteurs, on demande à collecter sous forme de liste, on attend le premier enregistrement avant de commencer à retourner en GraphQL la liste.

Dans le cas de la route par identifiant, on indique aussi le caractère asynchrone de la requête, mais on précise que l’on attend uniquement un seul enregistrement (pour ne pas attendre indéfiniment la suite).

À cette étape, vous pouvez exécuter votre application et apprécier le travail déjà accompli ici.

En exécutant le code GraphQL suivant :

query {
  author(id:2){
    firstname
    lastname
  }
  allAuthors{
    id
    firstname
    lastname
  }
  
}

Vous pourrez voir nos auteurs précédemment enregistrés en base.

Passons maintenant à l’écriture en base de données avec l’utilisation des routes de type mutation.

Créez un package graphql.mutation.

Fichier kotlin/.../graphql/mutation/AuthorsMutation.kt :

package com.mika.tutorialKotlinGraphQL.graphql.mutation
 
import com.expediagroup.graphql.spring.operations.Mutation
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Author
import com.mika.tutorialKotlinGraphQL.repository.AuthorsRepository
import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.stereotype.Component
 
@Component
class AuthorsMutation(private val authorsRepository: AuthorsRepository ): Mutation {
 
    suspend fun insertAuthor(authorInput : Author): Author? {
 
        return authorsRepository.save(authorInput).awaitSingle()
    }
}

Si l’on compare à notre implémentation de Query, on remarque surtout le fait que notre classe hérite de Mutation plutôt que de Query.

On reçoit un auteur en argument pour son ajout en base de données, une fois reçu, on utilise simplement la méthode save() de la classe repository et on retourne l’auteur avec son ID tout juste créé.

On peut vérifier notre création en exécutant le code GraphQL suivant :

mutation{
  insertAuthor(authorInput: {
     firstname:"Fred",
    lastname: "Vargas"
  }){
    
    id
    firstname
    lastname
  }
}

Vous recevrez en réponse le nouvel auteur ajouté (enfin la nouvelle autrice, dans notre exemple) :

{
  "data": {
    "insertAuthor": {
      
      "id": 4,
      "firstname": "Fred",
      "lastname": "Vargas"
    }
  }
}

Passons à la mise à jour d’un auteur : ici, il y a une subtilité, on ne s’attend pas à renvoyer à chaque mise à jour l’ensemble des champs de chaque auteur. On préférait indiquer l’identifiant et juste le/les champs à modifier.

D’abord, créons un type d’input spécifique kotlin/.../graphql/types/input/AuthorUpdate.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.input
 
data class AuthorUpdate (
    var id: Long,
    var firstname: String?,
    var lastname: String?,
 
)

On indique qu’à la mise à jour d’un auteur, seul l’identifiant est le champ obligatoire, l’ensemble des champs à mettre à jour est optionnel.

Ajoutons la méthode de mise à jour.

Fichier kotlin/.../graphql/mutation/AuthorsMutation.kt :

package com.mika.tutorialKotlinGraphQL.graphql.mutation
 
import com.expediagroup.graphql.spring.operations.Mutation
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Author
import com.mika.tutorialKotlinGraphQL.graphql.types.input.AuthorUpdate
import com.mika.tutorialKotlinGraphQL.repository.AuthorsRepository
import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.stereotype.Component
import kotlin.reflect.KMutableProperty
import kotlin.reflect.full.memberProperties
 
@Component
class AuthorsMutation(private val authorsRepository: AuthorsRepository ): Mutation {
 
    ...
 
    suspend fun updateAuthor( authorInput : AuthorUpdate): Author? {
 
        var authorToUpdate=authorsRepository.findById(authorInput.id).awaitSingle()
 
        for(fieldLoop in authorInput::class.memberProperties){
            val valueToUpdate=fieldLoop?.getter?.call(authorInput)
            if( valueToUpdate != null ) {
 
                var fieldToUpdate= authorToUpdate::class.memberProperties.filterIsInstance<KMutableProperty<*>>().find{it.name==fieldLoop.name}
                fieldToUpdate?.setter?.call(authorToUpdate,valueToUpdate)
 
            }
        }
 
        return authorsRepository.save(authorToUpdate).awaitSingle()
 
    }
 
 
}

À la réception de l’auteur à mettre à jour, on récupère dans un premier temps l’enregistrement en base, puis on boucle sur l’input pour mettre à jour uniquement les données envoyées.

Testons cette route via la commande GraphQL suivante :

mutation {
  updateAuthor(
    authorInput: {
      id:4,
      firstname: "Fred!"
      }){
    
    id
    firstname
    lastname
  }
}

Et vous verrez dans la réponse qu’on a uniquement modifié le prénom, en conservant le nom de famille (non envoyé) :

{
  "data": {
    "updateAuthor": {
      "id": 4,
      "firstname": "Fred!",
      "lastname": "Vargas"
    }
  }
}

8. Le cas N+1

Un cas qui arrive très souvent lorsque l’on fait des appels API, c’est le cas N+1, c’est-à-dire le besoin d’obtenir un élément de plus que celui initialement demandé.

Par exemple, ici, si l’on demande la liste des livres et que l’on aimerait bien avoir l’auteur également, pour chacun d’entre eux.

Première solution : le récupérer à chaque demande. Pour cela, on ajoute d’abord la possibilité de récupérer les auteurs à partir des Query sur les livres.

Fichier kotlin/.../graphql/query/BooksQuery.kt :

package com.mika.tutorialKotlinGraphQL.graphql.query
 
import com.expediagroup.graphql.annotations.GraphQLDescription
import com.expediagroup.graphql.spring.operations.Query
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Author
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Book
import com.mika.tutorialKotlinGraphQL.repository.AuthorsRepository
import com.mika.tutorialKotlinGraphQL.repository.BooksRepository
import kotlinx.coroutines.reactive.awaitFirst
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
 
@Component
class BooksQuery(private val booksRepository: BooksRepository, authorsRepository: AuthorsRepository) : Query {
 
    init {
       authorsRepositoryInstance =authorsRepository
    }
 
    @GraphQLDescription("Return all books")
    suspend fun allBooks(): List<Book>{
        return booksRepository.findAll().collectList().awaitFirst()
    }
 
    @GraphQLDescription("Return a book by his id")
    suspend fun book(id: Long): Book?{
        return booksRepository.findById(id).awaitSingle()
    }
 
    
    companion object AuthorInstance{
 
       lateinit var authorsRepositoryInstance: AuthorsRepository
 
        fun findById(id:Long): Mono<Author> {
            return authorsRepositoryInstance.findById(id)
        }
    }
 
}

Et dans l’entité Book, on ajoute une méthode pour rebondir sur l’objet auteur.

Fichier kotlin/.../graphql/types/entity/Book.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.entity
 
import com.mika.tutorialKotlinGraphQL.graphql.query.BooksQuery.AuthorInstance
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
import reactor.core.publisher.Mono
 
@Table("books")
data class Book (
    @Id
    val id: Long?,
    val title: String,
    val author_id: Long,
    val category_id: Long
){
 
     suspend fun Author(): Author{
 
        return AuthorInstance.findById( author_id).awaitSingle()
 
    }
}

En effet, la librairie GraphQL permet d’ajouter des méthodes sur les classes d’entités pour pouvoir enrichir la réponse.

Pour observer le problème, nous allons mettre quelques éléments en base avec la requête SQL suivante :

INSERT INTO `categories` (`id`, `name`) VALUES
(1, 'SF');
 
INSERT INTO `books` (`id`, `title`, `author_id`, `category_id`) VALUES
(1, 'Fondation', 1, 1),
(2, 'I, Robot', 1, 1),
(3, 'Chroniques martiennes', 2, 1),
(4, 'Fahrenheit 451', 2, 1);

Ainsi, on peut simplement récupérer les auteurs des livres avec la requête GraphQL suivante :

query{
  allBooks{
    id
    title
 
    Author{
      firstname
      lastname
    }
  }
}

On active les logs MariaDB en exécutant la requête SQL.

SET global general_log = 1;
SET global log_output = 'file';

Si vous relancez la requête GraphQL, vous pourrez observer dans votre volume local, dans notre cas /home/mika/code/docker/mysql/, un fichier avec l’extension .log se mettre à jour.

En l’ouvrant, vous pourrez constater le problème :

220605 18:55:55        85 Query    SELECT * FROM books
220605 18:55:56        86 Query    SELECT id, firstname, lastname FROM authors WHERE id = 1
            87 Query    SELECT id, firstname, lastname FROM authors WHERE id = 1
            88 Query    SELECT id, firstname, lastname FROM authors WHERE id = 2
            89 Query    SELECT id, firstname, lastname FROM authors WHERE id = 2

Une requête pour avoir la liste des livres, puis une requête pour chacun des livres pour obtenir l’auteur correspondant.

C’est un souci que l’on peut rencontrer avec une API REST, mais en GraphQL, on a une solution.

Cette solution s’appelle les dataloader. L’idée étant la suivante : plutôt que d’effectuer pour chaque ligne une requête pour obtenir l’élément, on amasse la liste des ID des auteurs demandés, et on effectue une requête avec l’ensemble de ces ID en base, puis on retourne pour chaque livre l’auteur correspondant.

Commençons par créer un fichier de config kotlin/.../graphql/config/GraphQLConfig.kt :

package com.mika.tutorialKotlinGraphQL.graphql.config
 
import com.expediagroup.graphql.spring.execution.DataLoaderRegistryFactory
import com.mika.tutorialKotlinGraphQL.abstracts.AbstractListLoader
import com.mika.tutorialKotlinGraphQL.abstracts.AbstractReferenceLoader
import graphql.schema.DataFetchingEnvironment
import org.dataloader.DataLoader
import org.dataloader.DataLoaderRegistry
import org.springframework.beans.factory.annotation.Lookup
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Scope
import org.springframework.context.annotation.ScopedProxyMode
import org.springframework.stereotype.Component
 
@Configuration
internal abstract class GraphQLConfig {
 
    @Bean
    open fun dataLoaderRegistryFactory(): DataLoaderRegistryFactory =
        object: DataLoaderRegistryFactory {
            override fun generate(): DataLoaderRegistry = dataLoaderRegistry()
        }
 
    @Bean
    @Scope(
        ConfigurableBeanFactory.SCOPE_PROTOTYPE,
        proxyMode = ScopedProxyMode.NO
    )
    protected open fun dataLoaderRegistry(
        loaders: List<DataLoader<*, *>>
    ): DataLoaderRegistry =
        DataLoaderRegistry().apply {
            loaders.forEach { loader ->
                register(
                    loader::class.qualifiedName,
                    loader
                )
            }
        }
 
    @Lookup
    protected abstract fun dataLoaderRegistry(): DataLoaderRegistry
}
 
inline fun <reified L: AbstractReferenceLoader<K, R>, K, R> DataFetchingEnvironment.getReferenceLoader(
): DataLoader<K, R?> =
    this.getDataLoader<K, R?>(L::class.qualifiedName)
 
 
inline fun <reified L: AbstractListLoader<K, E>, K, E> DataFetchingEnvironment.getListLoader(
): DataLoader<K, List<E>> =
    this.getDataLoader<K, List<E>>(L::class.qualifiedName)
 
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Component
@Scope(
    ConfigurableBeanFactory.SCOPE_PROTOTYPE,
    proxyMode = ScopedProxyMode.NO
)
annotation class DataLoaderComponent

Une classe abstraite kotlin/.../abstracts/AbstractReferenceLoader.kt :

package com.mika.tutorialKotlinGraphQL.abstracts
 
import org.dataloader.DataLoader
import org.dataloader.DataLoaderOptions
import java.util.concurrent.CompletableFuture
 
abstract class AbstractReferenceLoader<K, R: Any> (
    batchLoader: (Collection<K>) -> Collection<R>,
    keyGetter: (R) ->K,
    optionsInInitializer: (DataLoaderOptions.() -> Unit) ? = null
): DataLoader<K, R?>(
    { keys ->
        CompletableFuture.supplyAsync {
            batchLoader(keys)
                .associateBy(keyGetter)
                .let { map ->
                    keys.map { map[it] }
                }
        }
    },
    optionsInInitializer?.let {
        DataLoaderOptions().apply {
            this.it()
        }
    }
) {
}

Ici, on crée des directives permettant de définir des dataloaders en stockant des tableaux de données indexées par identifiant, puis de retourner les objets demandés facilement.

Et on crée une annotation pour spécifier qu’une classe est un dataloader.

Fichier kotlin/.../dataloader/AuthorsDataLoader.kt :

package com.mika.tutorialKotlinGraphQL.dataloader
 
import com.mika.tutorialKotlinGraphQL.abstracts.AbstractReferenceLoader
import com.mika.tutorialKotlinGraphQL.graphql.config.DataLoaderComponent
import com.mika.tutorialKotlinGraphQL.graphql.types.entity.Author
import com.mika.tutorialKotlinGraphQL.repository.AuthorsRepository
 
 
@DataLoaderComponent
open class AuthorsDataLoader(
    private val authorsRepository: AuthorsRepository
): AbstractReferenceLoader<Long, Author>(
    { authorsRepository.findListByIdList(it.toList()).collectList().block()!!.toMutableList() },
    { it.id!! },
    { setMaxBatchSize(256) }
)

L’implémentation est simple : on récupère le repository correspondant, on récupère l’ensemble des éléments avec la liste des identifiants demandés, et on indique l’identifiant qui sera utilisé ainsi que le maximum de lignes qui seront stockées par ce dataloader.

Et pour l’utiliser (et comparer), créons une seconde méthode AuthorWithDataLoader.

Fichier kotlin/.../graphql/types/entity/Book.kt :

package com.mika.tutorialKotlinGraphQL.graphql.types.entity
 
import com.mika.tutorialKotlinGraphQL.dataloader.AuthorsDataLoader
import com.mika.tutorialKotlinGraphQL.graphql.config.getReferenceLoader
import com.mika.tutorialKotlinGraphQL.graphql.query.BooksQuery.AuthorInstance
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.future.await
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
 
@Table("books")
data class Book (
    @Id
    val id: Long?,
    val title: String,
    val author_id: Long,
    val category_id: Long
){
 
     suspend fun Author(): Author{
 
        return AuthorInstance.findById( author_id).awaitSingle()
 
    }
 
    suspend fun AuthorWithDataLoader(env: DataFetchingEnvironment): Author?{
        
        return env
            .getReferenceLoader<AuthorsDataLoader, Long, Author>()
            .load(author_id)
            .await()
    }
}

On précise quel dataloader utiliser et on demande à récupérer l’auteur via la clé externe de l’auteur.

Cette fois-ci, en effectuant la requête GraphQL avec cette jointure plutôt que la précédente :

query{
  allBooks{
    id
    title
 
    AuthorWithDataLoader{
      firstname
      lastname
    }
    }
   
}

Vous pourrez avoir l’agréable surprise de ne voir que 2 requêtes SQL dans le fichier log de MariaDB :

           157 Query    SELECT * FROM books
           157 Query    SELECT id, firstname, lastname FROM authors WHERE id IN ( 1, 2 )

9. One more thing

Vous pouvez désactiver l’application Playground qui vous permet de tester votre API, par exemple au moment du passage en production, en ajoutant le bloc suivant dans votre fichier de configuration.

Fichier resources/application.yml :

graphql:
  packages:
    - "com.mika.tutorialKotlinGraphQL"
  playground:
    enabled: false

Vous pouvez retrouver l’ensemble des paramètres disponibles sur ce ficher de configuration à cette adresse : https://opensource.expediagroup.com/graphql-kotlin/docs/server/spring-server/spring-properties/.

Conclusion

Nous avons dans cet article pu écrire en quelques lignes une API GraphQL, sans nous soucier du schéma à générer. Nous avons mis en place un système pour optimiser les requêtes SQL nécessaires pour obtenir des enregistrements et leurs relations (avec la partie N+1).

Pour ceux qui préfèrent différencier le code et les schémas, il existe d’autres librairies GraphQL en Kotlin qui le permettent, mais j’ai préféré vous montrer celle-ci pour le côté très simple/rapide pour produire vos API.

En effet, en quelques minutes, vous avez une API utilisable, performante et auto documentée.

À l’heure où l’on préfère écrire des applications web avec une séparation front end avec un framework JavaScript /back end proposant des API, vous l’aurez compris avec ce mode de requête monoappel, il est bien plus simple pour le front de proposer une API GraphQL. Ensuite, vous avez vu à quel point ce langage est simple et efficace à utiliser. On obtient donc un couple parfait front end avec un framework JavaScript et un back end en Kotlin exposant une API GraphQL.

J’espère vous avoir donné envie de vous intéresser à ces deux technologies qui ont toutes les qualités pour être utilisées de paire.



Article rédigé par

Par le(s) même(s) auteur(s)

Créer son photobooth custom

Magazine
Marque
Hackable
Numéro
49
Mois de parution
juillet 2023
Spécialité(s)
Résumé

Vous avez peut-être déjà vu pendant un mariage ou autre événement festif un espace réservé où des personnes se prenaient automatiquement en photo via un drôle d’appareil. Cette installation de prise de photo automatique s’appelle un photobooth, et nous allons voir dans cet article comment vous pouvez facilement créer le vôtre.

Les derniers articles Premiums

Les derniers articles Premium

Brève introduction pratique à ZFS

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Il est grand temps de passer à un système de fichiers plus robuste et performant : ZFS. Avec ses fonctionnalités avancées, il assure une intégrité des données inégalée et simplifie la gestion des volumes de stockage. Il permet aussi de faire des snapshots, des clones, et de la déduplication, il est donc la solution idéale pour les environnements de stockage critiques. Découvrons ensemble pourquoi ZFS est LE choix incontournable pour l'avenir du stockage de données.

Générez votre serveur JEE sur-mesure avec Wildfly Glow

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Et, si, en une ligne de commandes, on pouvait reconstruire son serveur JEE pour qu’il soit configuré, sur mesure, pour les besoins des applications qu’il embarque ? Et si on pouvait aller encore plus loin, en distribuant l’ensemble, assemblé sous la forme d’un jar exécutable ? Et si on pouvait même déployer le tout, automatiquement, sur OpenShift ? Grâce à Wildfly Glow [1], c’est possible ! Tout du moins, pour le serveur JEE open source Wildfly [2]. Démonstration dans cet article.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous