0% ont trouvé ce document utile (0 vote)
6 vues91 pages

GraphQL Avec Spring Boot

Ce document est un guide complet sur le développement d'APIs modernes avec GraphQL et Spring Boot. Il couvre des sujets allant de l'introduction à GraphQL, à la mise en place avec Spring Boot, en passant par les requêtes, mutations, gestion des erreurs, et les meilleures pratiques. Le document inclut également des sections sur l'authentification, les tests, et le déploiement.

Transféré par

Hythame Rami
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
6 vues91 pages

GraphQL Avec Spring Boot

Ce document est un guide complet sur le développement d'APIs modernes avec GraphQL et Spring Boot. Il couvre des sujets allant de l'introduction à GraphQL, à la mise en place avec Spring Boot, en passant par les requêtes, mutations, gestion des erreurs, et les meilleures pratiques. Le document inclut également des sections sur l'authentification, les tests, et le déploiement.

Transféré par

Hythame Rami
Copyright
© All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

GraphQL avec Spring Boot

Guide complet du développement d’APIs modernes

Dr. BADR EL KHALYLY


2
Table des matières

1 Introduction à GraphQL 1
1.1 Historique et motivations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Architecture générale de GraphQL . . . . . . . . . . . . . . . . . . . . . . 2
1.3 GraphQL vs REST : comparaison détaillée . . . . . . . . . . . . . . . . . . 3
1.4 Les trois opérations fondamentales . . . . . . . . . . . . . . . . . . . . . . 4
1.4.1 Query (lecture) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4.2 Mutation (écriture) . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4.3 Subscription (temps réel) . . . . . . . . . . . . . . . . . . . . . . . . 5

2 Le système de types GraphQL 6


2.1 Vue d’ensemble du système de types . . . . . . . . . . . . . . . . . . . . . 6
2.2 Types scalaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2.1 Scalaires personnalisés . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Types objets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.4 Types énumération . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.5 Types interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.6 Types union . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.7 Types input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.8 Modificateurs de types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3 Spring Boot et GraphQL : mise en place 12


3.1 Présentation de Spring for GraphQL . . . . . . . . . . . . . . . . . . . . . 12
3.2 Création du projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.2.1 Initialisation avec Spring Initializr . . . . . . . . . . . . . . . . . . . 13
3.2.2 Dépendances Maven . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.3 Configuration de l’application . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.4 Structure du projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.5 Définition du schéma GraphQL . . . . . . . . . . . . . . . . . . . . . . . . 17
3.6 Création des entités JPA . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

4 Queries et résolveurs 24
4.1 Le flux d’exécution d’une query . . . . . . . . . . . . . . . . . . . . . . . . 24
4.2 Chaîne de résolution des champs . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 Repositories Spring Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.4 Couche service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
4.5 Contrôleurs GraphQL (résolveurs) . . . . . . . . . . . . . . . . . . . . . . . 28

i
TABLE DES MATIÈRES

4.6 Exemples de requêtes et réponses . . . . . . . . . . . . . . . . . . . . . . . 30

5 Mutations 32
5.1 Principe des mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.2 Implémentation des mutations . . . . . . . . . . . . . . . . . . . . . . . . . 32
5.3 Objets Input (DTOs) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.4 Validation des entrées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
5.5 Service PostService complet . . . . . . . . . . . . . . . . . . . . . . . . . . 35
5.6 Exemples de mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

6 Problème N+1 et DataLoader 39


6.1 Comprendre le problème N+1 . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.2 Solution : @BatchMapping . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
6.3 BatchMapping pour les collections . . . . . . . . . . . . . . . . . . . . . . . 42
6.4 Comparaison des performances . . . . . . . . . . . . . . . . . . . . . . . . 43

7 Gestion des erreurs 44


7.1 Modèle d’erreur GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
7.2 Exceptions personnalisées . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
7.3 DataFetcherExceptionResolver . . . . . . . . . . . . . . . . . . . . . . . . . 46
7.4 Erreurs partielles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

8 Pagination et filtrage 49
8.1 Stratégies de pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.2 Pagination Offset (classique) . . . . . . . . . . . . . . . . . . . . . . . . . . 49
8.3 Pagination Cursor (Relay-style) . . . . . . . . . . . . . . . . . . . . . . . . 50
8.4 Filtrage et tri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53

9 Subscriptions (temps réel) 56


9.1 Principe des subscriptions . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
9.2 Configuration WebSocket . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
9.3 Implémentation avec Reactor (Flux) . . . . . . . . . . . . . . . . . . . . . 57
9.4 Déclencher les événements . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
9.5 Côté client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

10 Authentification et sécurisation 61
10.1 Enjeux de sécurité en GraphQL . . . . . . . . . . . . . . . . . . . . . . . . 61
10.2 Authentification JWT avec Spring Security . . . . . . . . . . . . . . . . . . 63
10.2.1 Dépendances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
10.2.2 Service JWT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
10.2.3 Configuration Spring Security . . . . . . . . . . . . . . . . . . . . . 65

ii
TABLE DES MATIÈRES

10.2.4 Filtre JWT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66


10.3 Mutation de login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
10.4 Autorisation au niveau des résolveurs . . . . . . . . . . . . . . . . . . . . . 68
10.5 Protection contre les abus . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
10.5.1 Limitation de la profondeur des requêtes . . . . . . . . . . . . . . . 69
10.5.2 Limitation du débit (Rate Limiting) . . . . . . . . . . . . . . . . . 70

11 Tests 71
11.1 Stratégie de tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
11.2 Tests unitaires des services . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
11.3 Tests d’intégration avec GraphQlTester . . . . . . . . . . . . . . . . . . . . 73
11.4 Tests des mutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
11.5 Tests avec HttpGraphQlTester (E2E) . . . . . . . . . . . . . . . . . . . . . 76

12 Déploiement et architecture avancée 78


12.1 Architecture de déploiement . . . . . . . . . . . . . . . . . . . . . . . . . . 78
12.2 Containerisation avec Docker . . . . . . . . . . . . . . . . . . . . . . . . . 78
12.3 GraphQL dans une architecture microservices . . . . . . . . . . . . . . . . 80
12.4 Monitoring et observabilité . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
12.5 Upload de fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
12.6 Conception du schéma : bonnes pratiques . . . . . . . . . . . . . . . . . . . 83
12.6.1 Règles de conception . . . . . . . . . . . . . . . . . . . . . . . . . . 84
12.7 Résumé des bonnes pratiques . . . . . . . . . . . . . . . . . . . . . . . . . 84

iii
Table des figures

1.1 Chronologie de l’évolution de GraphQL . . . . . . . . . . . . . . . . . . . . 1


1.2 Architecture générale d’un serveur GraphQL . . . . . . . . . . . . . . . . . 2
1.3 Comparaison REST vs GraphQL : nombre de requêtes et précision des
données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

2.1 Hiérarchie du système de types GraphQL . . . . . . . . . . . . . . . . . . . 6

3.1 Stack technique Spring Boot + GraphQL . . . . . . . . . . . . . . . . . . . 13


3.2 Structure recommandée du projet . . . . . . . . . . . . . . . . . . . . . . . 17

4.1 Flux d’exécution d’une requête GraphQL . . . . . . . . . . . . . . . . . . . 24


4.2 Chaîne de résolution des champs . . . . . . . . . . . . . . . . . . . . . . . . 25

5.1 Flux d’exécution d’une mutation . . . . . . . . . . . . . . . . . . . . . . . 32

6.1 Problème N+1 et sa solution avec DataLoader . . . . . . . . . . . . . . . . 39


6.2 Fonctionnement du @BatchMapping . . . . . . . . . . . . . . . . . . . . . 40

7.1 Typologie des erreurs GraphQL . . . . . . . . . . . . . . . . . . . . . . . . 44

8.1 Pagination Offset vs Pagination Cursor . . . . . . . . . . . . . . . . . . . . 49

9.1 Flux de communication d’une subscription GraphQL via WebSocket . . . . 56

10.1 Les couches de sécurité d’une API GraphQL . . . . . . . . . . . . . . . . . 62


10.2 Flux d’authentification JWT complet . . . . . . . . . . . . . . . . . . . . . 63

11.1 Pyramide de tests pour une application GraphQL . . . . . . . . . . . . . . 71

12.1 Architecture de déploiement typique . . . . . . . . . . . . . . . . . . . . . 78


12.2 GraphQL comme gateway dans une architecture microservices . . . . . . . 80
12.3 Flux d’upload de fichiers via GraphQL . . . . . . . . . . . . . . . . . . . . 82
12.4 Exemple de conception de schéma pour une application blog . . . . . . . . 83

iv
Chapitre 1

Introduction à GraphQL

1.1 Historique et motivations


GraphQL est un langage de requête pour les APIs, développé en interne par Facebook
en 2012, puis rendu open source en 2015. Il est né d’un besoin concret : les applications
mobiles de Facebook nécessitaient des échanges de données flexibles et performants que
les APIs REST traditionnelles ne pouvaient pas offrir efficacement.

Figure 1.1 – Chronologie de l’évolution de GraphQL

Les motivations principales derrière la création de GraphQL étaient :

— Over-fetching : les APIs REST renvoient souvent plus de données que nécessaire,
gaspillant de la bande passante.

— Under-fetching : une seule vue peut nécessiter plusieurs appels REST pour récupérer
toutes les données requises.

— Évolution rapide des clients : les équipes front-end et mobile avaient besoin de
pouvoir demander exactement les données nécessaires sans attendre des modifications
côté serveur.

— Typage fort : un système de types permettant la validation automatique et l’auto-


documentation.

[title=Définition : GraphQL] GraphQL est un langage de requête et un environne-


ment d’exécution pour les APIs. Il fournit une description complète et compréhensible
des données de votre API, donne aux clients le pouvoir de demander exactement ce
dont ils ont besoin, facilite l’évolution des APIs dans le temps et permet des outils
de développement puissants.

1
CHAPITRE 1. INTRODUCTION À GRAPHQL

1.2 Architecture générale de GraphQL


L’architecture GraphQL repose sur un point d’entrée unique qui reçoit toutes les
requêtes des clients. Le serveur GraphQL est composé de plusieurs couches : le schéma
(qui définit les types et opérations possibles), le moteur d’exécution (qui valide et exécute
les requêtes), et les résolveurs (qui récupèrent les données depuis les sources).

Figure 1.2 – Architecture générale d’un serveur GraphQL

Les caractéristiques clés de cette architecture sont :

2
1.3. GRAPHQL VS REST : COMPARAISON DÉTAILLÉE

1. Point d’entrée unique : toutes les requêtes passent par /graphql, contrairement à
REST qui multiplie les endpoints.

2. Schéma comme contrat : le schéma GraphQL définit précisément ce que le serveur


peut fournir.

3. Résolveurs découplés : chaque champ peut avoir son propre résolveur, permettant
d’agréger des données de multiples sources.

4. Indépendance des clients : chaque client peut demander exactement les données
dont il a besoin.

1.3 GraphQL vs REST : comparaison détaillée

La comparaison entre GraphQL et REST est essentielle pour comprendre quand uti-
liser chaque approche.

Figure 1.3 – Comparaison REST vs GraphQL : nombre de requêtes et précision des données

3
CHAPITRE 1. INTRODUCTION À GRAPHQL

Table 1.1 – Comparaison détaillée REST vs GraphQL

Critère REST GraphQL

Endpoints Multiples (/users, /posts) Unique (/graphql)


Données Structure fixe, over/under-fetching Données à la demande
Versioning /v1/, /v2/ Évolution par @deprecated
Documentation Swagger/OpenAPI (externe) Introspection (intégrée)
Cache HTTP Natif (GET, ETags) complexe (POST)
Upload fichiers Natif (multipart) Nécessite extension
Temps réel SSE, WebSocket (séparé) Subscriptions (intégré)
Courbe d’apprentissage Faible Modérée
Écosystème Très mature En forte croissance

Attention
GraphQL ne remplace pas REST dans tous les cas. REST reste préférable pour les
APIs simples, le cache HTTP natif, ou les services de téléchargement de fichiers.
GraphQL excelle lorsque les clients ont des besoins de données variés et complexes.

1.4 Les trois opérations fondamentales


GraphQL définit trois types d’opérations :

1.4.1 Query (lecture)


Les queries permettent de lire des données. Elles sont l’équivalent des requêtes GET en
REST.

1 query {
2 user ( id : " 1 " ) {
3 name
4 email
5 posts {
6 title
7 createdAt
8 }
9 }
10 }

Listing 1.1 – Exemple de query GraphQL

4
1.4. LES TROIS OPÉRATIONS FONDAMENTALES

1.4.2 Mutation (écriture)


Les mutations permettent de créer, modifier ou supprimer des données. Elles sont
l’équivalent des requêtes POST, PUT, DELETE en REST.

1 mutation {
2 createUser ( input : {
3 name : " Alice Dupont "
4 email : " alice@example . com "
5 }) {
6 id
7 name
8 }
9 }

Listing 1.2 – Exemple de mutation GraphQL

1.4.3 Subscription (temps réel)


Les subscriptions permettent de recevoir des données en temps réel via WebSocket.

1 subscription {
2 messageAdded ( roomId : " general " ) {
3 content
4 author {
5 name
6 }
7 createdAt
8 }
9 }

Listing 1.3 – Exemple de subscription GraphQL

5
Chapitre 2

Le système de types GraphQL

2.1 Vue d’ensemble du système de types


Le système de types est le cœur de GraphQL. Chaque serveur GraphQL définit un
schéma typé qui décrit l’ensemble des données disponibles. Ce schéma sert de contrat
entre le client et le serveur.

Figure 2.1 – Hiérarchie du système de types GraphQL

2.2 Types scalaires


Les types scalaires représentent les valeurs atomiques. GraphQL fournit cinq types
scalaires par défaut :

Table 2.1 – Types scalaires GraphQL

Type Java Description

Int Integer Entier signé 32 bits


Float Double Nombre à virgule flottante double précision
String String Séquence de caractères UTF-8
Boolean Boolean true ou false
ID String Identifiant unique (sérialisé comme String)

2.2.1 Scalaires personnalisés


On peut définir des scalaires personnalisés pour des types comme les dates :

1 scalar DateTime
2 scalar Date
3 scalar URL

6
2.3. TYPES OBJETS

4 scalar Long
5 scalar BigDecimal

Listing 2.1 – Déclaration de scalaires personnalisés

Implémentation Java avec graphql-java-extended-scalars :

1 @Configuration
2 public class ScalarConfig {
3

4 @Bean
5 public RuntimeWiringConfigurer runtimeWiringConfigurer () {
6 return wiringBuilder -> wiringBuilder
7 . scalar ( ExtendedScalars . DateTime )
8 . scalar ( ExtendedScalars . Date )
9 . scalar ( ExtendedScalars . Url ) ;
10 }
11 }

Listing 2.2 – Configuration des scalaires étendus

2.3 Types objets


Les types objets sont les types les plus courants en GraphQL. Ils représentent un
ensemble de champs typés.

1 type User {
2 id : ID !
3 name : String !
4 email : String !
5 age : Int
6 role : Role !
7 posts : [ Post !]!
8 createdAt : DateTime !
9 }
10

11 type Post {
12 id : ID !
13 title : String !
14 content : String !
15 published : Boolean !
16 author : User !

7
CHAPITRE 2. LE SYSTÈME DE TYPES GRAPHQL

17 comments : [ Comment !]!


18 tags : [ String !]
19 createdAt : DateTime !
20 updatedAt : DateTime
21 }
22

23 type Comment {
24 id : ID !
25 text : String !
26 author : User !
27 post : Post !
28 createdAt : DateTime !
29 }

Listing 2.3 – Définition de types objets

Note
Le point d’exclamation ! indique qu’un champ est non-nullable (obligatoire).
String! signifie que la valeur ne peut jamais être null. [Post!]! signifie que la
liste est non-nulle ET que chaque élément est non-nul.

2.4 Types énumération


Les enums définissent un ensemble fini de valeurs possibles :

1 enum Role {
2 ADMIN
3 MODERATOR
4 USER
5 GUEST
6 }
7

8 enum PostStatus {
9 DRAFT
10 PUBLISHED
11 ARCHIVED
12 }
13

14 enum SortOrder {
15 ASC
16 DESC

8
2.5. TYPES INTERFACES

17 }

Listing 2.4 – Types énumération

Correspondance Java :

1 public enum Role {


2 ADMIN , MODERATOR , USER , GUEST
3 }
4

5 public enum PostStatus {


6 DRAFT , PUBLISHED , ARCHIVED
7 }

Listing 2.5 – Enum Java correspondante

2.5 Types interfaces


Les interfaces définissent un ensemble de champs qu’un type doit implémenter :

1 interface Node {
2 id : ID !
3 }
4

5 interface Timestamped {
6 createdAt : DateTime !
7 updatedAt : DateTime
8 }
9

10 type User implements Node & Timestamped {


11 id : ID !
12 name : String !
13 email : String !
14 createdAt : DateTime !
15 updatedAt : DateTime
16 }
17

18 type Post implements Node & Timestamped {


19 id : ID !
20 title : String !
21 content : String !
22 createdAt : DateTime !

9
CHAPITRE 2. LE SYSTÈME DE TYPES GRAPHQL

23 updatedAt : DateTime
24 }

Listing 2.6 – Interfaces GraphQL

2.6 Types union


Les unions permettent à un champ de retourner l’un parmi plusieurs types :

1 union SearchResult = User | Post | Comment


2

3 type Query {
4 search ( term : String !) : [ SearchResult !]!
5 }

Listing 2.7 – Types union

Côté client, on utilise des fragments inline pour sélectionner les champs selon le type
retourné :

1 query {
2 search ( term : " GraphQL " ) {
3 ... on User {
4 name
5 email
6 }
7 ... on Post {
8 title
9 content
10 }
11 ... on Comment {
12 text
13 }
14 }
15 }

Listing 2.8 – Requête avec union et fragments

2.7 Types input


Les types input sont utilisés exclusivement pour les arguments des mutations et que-
ries :

10
2.8. MODIFICATEURS DE TYPES

1 input CreateUserInput {
2 name : String !
3 email : String !
4 password : String !
5 role : Role = USER
6 }
7

8 input UpdateUserInput {
9 name : String
10 email : String
11 age : Int
12 }
13

14 input PostFilter {
15 status : PostStatus
16 authorId : ID
17 tag : String
18 search : String
19 }

Listing 2.9 – Types input

Attention
Les types input ne peuvent pas contenir de champs qui référencent des types objets.
Ils ne peuvent contenir que des scalaires, des enums et d’autres types input. C’est
une contrainte de conception de GraphQL.

2.8 Modificateurs de types

Table 2.2 – Modificateurs de types GraphQL

Syntaxe Signification

String Valeur nullable (peut être null)


String! Valeur non-nullable (jamais null)
[String] Liste nullable de valeurs nullables
[String!] Liste nullable de valeurs non-nullables
[String!]! Liste non-nullable de valeurs non-nullables

11
Chapitre 3

Spring Boot et GraphQL : mise en


place

3.1 Présentation de Spring for GraphQL

Spring for GraphQL est le projet officiel de l’écosystème Spring pour l’intégration de
GraphQL. Il s’appuie sur graphql-java, le moteur GraphQL de référence en Java, et
fournit une intégration native avec Spring Boot.

12
3.2. CRÉATION DU PROJET

Figure 3.1 – Stack technique Spring Boot + GraphQL

3.2 Création du projet

3.2.1 Initialisation avec Spring Initializr


Rendez-vous sur [Link] et sélectionnez les dépendances suivantes :
— Spring Web — serveur HTTP embarqué
— Spring for GraphQL — support GraphQL
— Spring Data JPA — accès aux données

13
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE

— PostgreSQL Driver — connecteur base de données


— Spring Boot DevTools — redémarrage automatique
— Lombok — réduction du code répétitif

3.2.2 Dépendances Maven

1 <? xml version = " 1.0 " encoding = " UTF -8 " ? >
2 < project xmlns = " http: // maven . apache . org / POM /4.0.0 "
3 xmlns:xsi = " http: // www . w3 . org /2001/ XMLSchema - instance "
4 xsi:schemaLocation = " http: // maven . apache . org / POM /4.0.0
5 https: // maven . apache . org / xsd / maven -4.0.0. xsd " >
6 < modelVersion > 4.0.0 </ modelVersion >
7

8 < parent >


9 < groupId > org . springframework . boot </ groupId >
10 < artifactId > spring - boot - starter - parent </ artifactId >
11 < version > 3.2.5 </ version >
12 </ parent >
13

14 < groupId > com . example </ groupId >


15 < artifactId > graphql - demo </ artifactId >
16 < version > 1.0.0 </ version >
17 < name > GraphQL Spring Boot Demo </ name >
18

19 < properties >


20 < java . version > 21 </ java . version >
21 </ properties >
22

23 < dependencies >


24 <! -- Spring Web -- >
25 < dependency >
26 < groupId > org . springframework . boot </ groupId >
27 < artifactId > spring - boot - starter - web </ artifactId >
28 </ dependency >
29

30 <! -- Spring GraphQL -- >


31 < dependency >
32 < groupId > org . springframework . boot </ groupId >
33 < artifactId > spring - boot - starter - graphql </ artifactId >
34 </ dependency >
35

14
3.2. CRÉATION DU PROJET

36 <! -- Spring Data JPA -- >


37 < dependency >
38 < groupId > org . springframework . boot </ groupId >
39 < artifactId > spring - boot - starter - data - jpa </ artifactId >
40 </ dependency >
41

42 <! -- PostgreSQL -- >


43 < dependency >
44 < groupId > org . postgresql </ groupId >
45 < artifactId > postgresql </ artifactId >
46 < scope > runtime </ scope >
47 </ dependency >
48

49 <! -- Lombok -- >


50 < dependency >
51 < groupId > org . projectlombok </ groupId >
52 < artifactId > lombok </ artifactId >
53 < optional > true </ optional >
54 </ dependency >
55

56 <! -- GraphQL Extended Scalars -- >


57 < dependency >
58 < groupId > com . graphql - java </ groupId >
59 < artifactId > graphql - java - extended - scalars </ artifactId >
60 < version > 22.0 </ version >
61 </ dependency >
62

63 <! -- Test -- >


64 < dependency >
65 < groupId > org . springframework . boot </ groupId >
66 < artifactId > spring - boot - starter - test </ artifactId >
67 < scope > test </ scope >
68 </ dependency >
69 < dependency >
70 < groupId > org . springframework . graphql </ groupId >
71 < artifactId > spring - graphql - test </ artifactId >
72 < scope > test </ scope >
73 </ dependency >
74 </ dependencies >
75 </ project >

Listing 3.1 – [Link] — dépendances principales

15
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE

3.3 Configuration de l’application

spring :
application :
name : graphql - demo

datasource :
url : jdbc : postgresql :// localhost :5432/ graphql_db
username : postgres
password : postgres
driver - class - name : org . postgresql . Driver

jpa :
hibernate :
ddl - auto : update
show - sql : true
properties :
hibernate :
format_sql : true
dialect : org . hibernate . dialect . PostgreSQLDialect

graphql :
graphiql :
enabled : true
path : / graphiql
schema :
printer :
enabled : true
path : / graphql

server :
port : 8080

Listing 3.2 – [Link]

Conseil
GraphiQL est une interface web interactive pour tester vos requêtes Gra-
phQL. En activant [Link], vous pouvez y accéder à
[Link] C’est un outil indispensable pendant le dévelop-
pement.

16
3.4. STRUCTURE DU PROJET

3.4 Structure du projet

Figure 3.2 – Structure recommandée du projet

3.5 Définition du schéma GraphQL


Le fichier de schéma doit être placé dans src/main/resources/graphql/[Link] :

1 # Scalaires personnalises
2 scalar DateTime
3

4 # === Types principaux ===


5

6 type User {
7 id : ID !
8 name : String !
9 email : String !
10 role : Role !
11 posts : [ Post !]!
12 createdAt : DateTime !
13 }
14

15 type Post {
16 id : ID !
17 title : String !
18 content : String !
19 status : PostStatus !
20 author : User !
21 comments : [ Comment !]!
22 createdAt : DateTime !
23 updatedAt : DateTime
24 }
25

26 type Comment {
27 id : ID !
28 text : String !
29 author : User !

17
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE

30 post : Post !
31 createdAt : DateTime !
32 }
33

34 # === Enumerations ===


35

36 enum Role {
37 ADMIN
38 MODERATOR
39 USER
40 }
41

42 enum PostStatus {
43 DRAFT
44 PUBLISHED
45 ARCHIVED
46 }
47

48 # === Types Input ===


49

50 input CreateUserInput {
51 name : String !
52 email : String !
53 password : String !
54 role : Role = USER
55 }
56

57 input UpdateUserInput {
58 name : String
59 email : String
60 }
61

62 input CreatePostInput {
63 title : String !
64 content : String !
65 status : PostStatus = DRAFT
66 }
67

68 input AddCommentInput {
69 postId : ID !
70 text : String !

18
3.5. DÉFINITION DU SCHÉMA GRAPHQL

71 }
72

73 # === Pagination ===


74

75 type UserPage {
76 content : [ User !]!
77 totalElements : Int !
78 totalPages : Int !
79 currentPage : Int !
80 hasNext : Boolean !
81 }
82

83 # === Operations ===


84

85 type Query {
86 # Utilisateurs
87 users ( page : Int = 0 , size : Int = 10) : UserPage !
88 user ( id : ID !) : User
89 me : User
90

91 # Posts
92 posts ( status : PostStatus ) : [ Post !]!
93 post ( id : ID !) : Post
94

95 # Recherche
96 search ( term : String !) : [ SearchResult !]!
97 }
98

99 type Mutation {
100 # Utilisateurs
101 createUser ( input : CreateUserInput !) : User !
102 updateUser ( id : ID ! , input : UpdateUserInput !) : User !
103 deleteUser ( id : ID !) : Boolean !
104

105 # Posts
106 createPost ( input : CreatePostInput !) : Post !
107 publishPost ( id : ID !) : Post !
108

109 # Commentaires
110 addComment ( input : AddCommentInput !) : Comment !
111 }

19
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE

112

113 type Subscription {


114 postPublished : Post !
115 commentAdded ( postId : ID !) : Comment !
116 }
117

118 union SearchResult = User | Post | Comment

Listing 3.3 – [Link] — Schéma complet

3.6 Création des entités JPA

1 @Entity
2 @Table ( name = " users " )
3 @Data
4 @NoArgsConstructor
5 @AllArgsConstructor
6 @Builder
7 public class User {
8

9 @Id
10 @GeneratedValue ( strategy = GenerationType . IDENTITY )
11 private Long id ;
12

13 @Column ( nullable = false )


14 private String name ;
15

16 @Column ( nullable = false , unique = true )


17 private String email ;
18

19 @Column ( nullable = false )


20 private String password ;
21

22 @Enumerated ( EnumType . STRING )


23 @Column ( nullable = false )
24 private Role role = Role . USER ;
25

26 @OneToMany ( mappedBy = " author " , cascade = CascadeType . ALL )


27 private List < Post > posts = new ArrayList < >() ;
28

20
3.6. CRÉATION DES ENTITÉS JPA

29 @Column ( nullable = false , updatable = false )


30 private LocalDateTime createdAt ;
31

32 @PrePersist
33 protected void onCreate () {
34 this . createdAt = LocalDateTime . now () ;
35 }
36 }

Listing 3.4 – [Link] — Entité utilisateur

1 @Entity
2 @Table ( name = " posts " )
3 @Data
4 @NoArgsConstructor
5 @AllArgsConstructor
6 @Builder
7 public class Post {
8

9 @Id
10 @GeneratedValue ( strategy = GenerationType . IDENTITY )
11 private Long id ;
12

13 @Column ( nullable = false )


14 private String title ;
15

16 @Column ( nullable = false , columnDefinition = " TEXT " )


17 private String content ;
18

19 @Enumerated ( EnumType . STRING )


20 @Column ( nullable = false )
21 private PostStatus status = PostStatus . DRAFT ;
22

23 @ManyToOne ( fetch = FetchType . LAZY )


24 @JoinColumn ( name = " author_id " , nullable = false )
25 private User author ;
26

27 @OneToMany ( mappedBy = " post " , cascade = CascadeType . ALL )


28 private List < Comment > comments = new ArrayList < >() ;
29

30 @Column ( nullable = false , updatable = false )


31 private LocalDateTime createdAt ;

21
CHAPITRE 3. SPRING BOOT ET GRAPHQL : MISE EN PLACE

32

33 private LocalDateTime updatedAt ;


34

35 @PrePersist
36 protected void onCreate () {
37 this . createdAt = LocalDateTime . now () ;
38 }
39

40 @PreUpdate
41 protected void onUpdate () {
42 this . updatedAt = LocalDateTime . now () ;
43 }
44 }

Listing 3.5 – [Link] — Entité article

1 @Entity
2 @Table ( name = " comments " )
3 @Data
4 @NoArgsConstructor
5 @AllArgsConstructor
6 @Builder
7 public class Comment {
8

9 @Id
10 @GeneratedValue ( strategy = GenerationType . IDENTITY )
11 private Long id ;
12

13 @Column ( nullable = false , columnDefinition = " TEXT " )


14 private String text ;
15

16 @ManyToOne ( fetch = FetchType . LAZY )


17 @JoinColumn ( name = " author_id " , nullable = false )
18 private User author ;
19

20 @ManyToOne ( fetch = FetchType . LAZY )


21 @JoinColumn ( name = " post_id " , nullable = false )
22 private Post post ;
23

24 @Column ( nullable = false , updatable = false )


25 private LocalDateTime createdAt ;
26

22
3.6. CRÉATION DES ENTITÉS JPA

27 @PrePersist
28 protected void onCreate () {
29 this . createdAt = LocalDateTime . now () ;
30 }
31 }

Listing 3.6 – [Link] — Entité commentaire

23
Chapitre 4

Queries et résolveurs

4.1 Le flux d’exécution d’une query

Lorsqu’une requête GraphQL arrive sur le serveur, elle passe par plusieurs étapes : par-
sing de la requête, validation contre le schéma, exécution via les résolveurs, et assemblage
de la réponse.

Figure 4.1 – Flux d’exécution d’une requête GraphQL

4.2 Chaîne de résolution des champs

Chaque champ dans une requête GraphQL est résolu par un résolveur. Les résolveurs
forment une chaîne hiérarchique :

24
4.2. CHAÎNE DE RÉSOLUTION DES CHAMPS

Figure 4.2 – Chaîne de résolution des champs

25
CHAPITRE 4. QUERIES ET RÉSOLVEURS

4.3 Repositories Spring Data

1 @Repository
2 public interface UserRepository extends JpaRepository < User , Long > {
3

4 Optional < User > findByEmail ( String email ) ;


5

6 boolean existsByEmail ( String email ) ;


7

8 List < User > findByRole ( Role role ) ;


9

10 @Query ( " SELECT u FROM User u WHERE " +


11 " LOWER ( u . name ) LIKE LOWER ( CONCAT ( '% ' , : term , '% ') ) OR "
+
12 " LOWER ( u . email ) LIKE LOWER ( CONCAT ( '% ' , : term , '% ') ) " )
13 List < User > search ( @Param ( " term " ) String term ) ;
14 }

Listing 4.1 – [Link]

1 @Repository
2 public interface PostRepository extends JpaRepository < Post , Long > {
3

4 List < Post > findByAuthorId ( Long authorId ) ;


5

6 List < Post > findByStatus ( PostStatus status ) ;


7

8 List < Post > findByAuthorIdIn ( List < Long > authorIds ) ;
9

10 @Query ( " SELECT p FROM Post p WHERE " +


11 " LOWER ( p . title ) LIKE LOWER ( CONCAT ( '% ' , : term , '% ') ) OR "
+
12 " LOWER ( p . content ) LIKE LOWER ( CONCAT ( '% ' , : term , '% ') ) " )
13 List < Post > search ( @Param ( " term " ) String term ) ;
14 }

Listing 4.2 – [Link]

4.4 Couche service

26
4.4. COUCHE SERVICE

1 @Service
2 @RequiredArgsConstructor
3 public class UserService {
4

5 private final UserRepository userRepository ;


6 private final PasswordEncoder passwordEncoder ;
7

8 public Page < User > getUsers ( int page , int size ) {
9 return userRepository . findAll (
10 PageRequest . of ( page , size , Sort . by ( " createdAt " ) .
descending () )
11 );
12 }
13

14 public Optional < User > getUserById ( Long id ) {


15 return userRepository . findById ( id ) ;
16 }
17

18 public User createUser ( CreateUserInput input ) {


19 if ( userRepository . existsByEmail ( input . getEmail () ) ) {
20 throw new DuplicateEmailException (
21 " Email deja utilise : " + input . getEmail ()
22 );
23 }
24

25 User user = User . builder ()


26 . name ( input . getName () )
27 . email ( input . getEmail () )
28 . password ( passwordEncoder . encode ( input . getPassword () ) )
29 . role ( input . getRole () != null ? input . getRole () : Role .
USER )
30 . build () ;
31

32 return userRepository . save ( user ) ;


33 }
34

35 public User updateUser ( Long id , UpdateUserInput input ) {


36 User user = userRepository . findById ( id )
37 . orElseThrow (() -> new UserNotFoundException (
38 " Utilisateur non trouve : " + id
39 ));

27
CHAPITRE 4. QUERIES ET RÉSOLVEURS

40

41 if ( input . getName () != null ) {


42 user . setName ( input . getName () ) ;
43 }
44 if ( input . getEmail () != null ) {
45 user . setEmail ( input . getEmail () ) ;
46 }
47

48 return userRepository . save ( user ) ;


49 }
50

51 public boolean deleteUser ( Long id ) {


52 if (! userRepository . existsById ( id ) ) {
53 throw new UserNotFoundException ( " Utilisateur non trouve
: " + id ) ;
54 }
55 userRepository . deleteById ( id ) ;
56 return true ;
57 }
58 }

Listing 4.3 – [Link]

4.5 Contrôleurs GraphQL (résolveurs)


Spring for GraphQL utilise des annotations pour mapper les résolveurs :

Table 4.1 – Annotations principales de Spring for GraphQL

Annotation Rôle

@QueryMapping Résout un champ du type Query


@MutationMapping Résout un champ du type Mutation
@SubscriptionMapping Résout un champ du type Subscription
@SchemaMapping Résout un champ d’un type spécifique
@BatchMapping Résout un champ par lot (DataLoader)
@Argument Injecte un argument de la requête

1 @Controller
2 @RequiredArgsConstructor
3 public class UserController {

28
4.5. CONTRÔLEURS GRAPHQL (RÉSOLVEURS)

5 private final UserService userService ;


6 private final PostRepository postRepository ;
7

8 @QueryMapping
9 public UserPage users ( @Argument int page , @Argument int size ) {
10 Page < User > userPage = userService . getUsers ( page , size ) ;
11 return new UserPage (
12 userPage . getContent () ,
13 ( int ) userPage . getTotalElements () ,
14 userPage . getTotalPages () ,
15 userPage . getNumber () ,
16 userPage . hasNext ()
17 );
18 }
19

20 @QueryMapping
21 public Optional < User > user ( @Argument Long id ) {
22 return userService . getUserById ( id ) ;
23 }
24

25 // Resolution du champ " posts " du type User


26 @SchemaMapping ( typeName = " User " , field = " posts " )
27 public List < Post > getPosts ( User user ) {
28 return postRepository . findByAuthorId ( user . getId () ) ;
29 }
30 }

Listing 4.4 – [Link] — Résolveurs de queries

1 @Controller
2 @RequiredArgsConstructor
3 public class PostController {
4

5 private final PostService postService ;


6 private final CommentRepository commentRepository ;
7 private final UserRepository userRepository ;
8

9 @QueryMapping
10 public List < Post > posts ( @Argument PostStatus status ) {
11 if ( status != null ) {
12 return postService . getPostsByStatus ( status ) ;

29
CHAPITRE 4. QUERIES ET RÉSOLVEURS

13 }
14 return postService . getAllPosts () ;
15 }
16

17 @QueryMapping
18 public Optional < Post > post ( @Argument Long id ) {
19 return postService . getPostById ( id ) ;
20 }
21

22 // Resolution du champ " author " du type Post


23 @SchemaMapping ( typeName = " Post " , field = " author " )
24 public User getAuthor ( Post post ) {
25 return post . getAuthor () ;
26 }
27

28 // Resolution du champ " comments " du type Post


29 @SchemaMapping ( typeName = " Post " , field = " comments " )
30 public List < Comment > getComments ( Post post ) {
31 return commentRepository . findByPostId ( post . getId () ) ;
32 }
33 }

Listing 4.5 – [Link] — Résolveurs de queries pour Post

Conseil
Avec @SchemaMapping, Spring for GraphQL appelle le résolveur uniquement si le
client demande le champ en question. Si la requête ne demande pas les posts d’un
User, le résolveur getPosts() ne sera jamais appelé. C’est l’un des avantages clés de
GraphQL.

4.6 Exemples de requêtes et réponses

1 query {
2 user ( id : " 1 " ) {
3 name
4 email
5 role
6 posts {
7 title
8 status

30
4.6. EXEMPLES DE REQUÊTES ET RÉPONSES

9 createdAt
10 }
11 }
12 }

Listing 4.6 – Requête : récupérer un utilisateur avec ses posts

Réponse JSON :

{
" data " : {
" user " : {
" name " : " Alice Dupont " ,
" email " : " alice@example . com " ,
" role " : " ADMIN " ,
" posts " : [
{
" title " : " Introduction a GraphQL " ,
" status " : " PUBLISHED " ,
" createdAt " : " 2024 -01 -15 T10 :30:00 "
},
{
" title " : " Spring Boot avance " ,
" status " : " DRAFT " ,
" createdAt " : " 2024 -02 -20 T14 :00:00 "
}
]
}
}
}

Listing 4.7 – Réponse JSON correspondante

31
Chapitre 5

Mutations

5.1 Principe des mutations


Les mutations sont le mécanisme de GraphQL pour modifier les données (création,
mise à jour, suppression). Contrairement aux queries, les mutations sont exécutées de
manière séquentielle pour garantir la cohérence.

Figure 5.1 – Flux d’exécution d’une mutation

5.2 Implémentation des mutations

1 @Controller
2 @RequiredArgsConstructor
3 public class UserMutationController {
4

5 private final UserService userService ;


6

7 @MutationMapping
8 public User createUser ( @Argument CreateUserInput input ) {

32
5.2. IMPLÉMENTATION DES MUTATIONS

9 return userService . createUser ( input ) ;


10 }
11

12 @MutationMapping
13 public User updateUser ( @Argument Long id ,
14 @Argument UpdateUserInput input ) {
15 return userService . updateUser ( id , input ) ;
16 }
17

18 @MutationMapping
19 public boolean deleteUser ( @Argument Long id ) {
20 return userService . deleteUser ( id ) ;
21 }
22 }

Listing 5.1 – [Link]

1 @Controller
2 @RequiredArgsConstructor
3 public class PostMutationController {
4

5 private final PostService postService ;


6

7 @MutationMapping
8 public Post createPost ( @Argument CreatePostInput input ,
9 @AuthenticationPrincipal UserDetails
user ) {
10 return postService . createPost ( input , user . getUsername () ) ;
11 }
12

13 @MutationMapping
14 public Post publishPost ( @Argument Long id ) {
15 return postService . publishPost ( id ) ;
16 }
17

18 @MutationMapping
19 public Comment addComment ( @Argument AddCommentInput input ,
20 @AuthenticationPrincipal UserDetails
user ) {
21 return postService . addComment ( input , user . getUsername () ) ;
22 }
23 }

33
CHAPITRE 5. MUTATIONS

Listing 5.2 – [Link]

5.3 Objets Input (DTOs)


Les types input du schéma GraphQL sont mappés vers des classes Java ou des records :

1 // Utilisation de records Java ( Java 16+)


2 public record CreateUserInput (
3 String name ,
4 String email ,
5 String password ,
6 Role role
7 ) {}
8

9 public record UpdateUserInput (


10 String name ,
11 String email
12 ) {}
13

14 public record CreatePostInput (


15 String title ,
16 String content ,
17 PostStatus status
18 ) {}
19

20 public record AddCommentInput (


21 Long postId ,
22 String text
23 ) {}

Listing 5.3 – DTOs pour les mutations

5.4 Validation des entrées

1 public record CreateUserInput (


2 @NotBlank ( message = " Le nom est obligatoire " )
3 @Size ( min = 2 , max = 100 , message = " Le nom doit contenir entre
2 et 100 caracteres " )

34
5.5. SERVICE POSTSERVICE COMPLET

4 String name ,
5

6 @NotBlank ( message = " L ' email est obligatoire " )


7 @Email ( message = " Format d ' email invalide " )
8 String email ,
9

10 @NotBlank ( message = " Le mot de passe est obligatoire " )


11 @Size ( min = 8 , message = " Le mot de passe doit contenir au
moins 8 caracteres " )
12 String password ,
13

14 Role role
15 ) {}

Listing 5.4 – Validation avec Bean Validation

Pour activer la validation automatique, on configure un intercepteur :

1 @Configuration
2 public class GraphQLConfig {
3

4 @Bean
5 public RuntimeWiringConfigurer runtimeWiringConfigurer () {
6 return wiringBuilder -> wiringBuilder
7 . scalar ( ExtendedScalars . DateTime ) ;
8 }
9 }

Listing 5.5 – Configuration de la validation

5.5 Service PostService complet

1 @Service
2 @RequiredArgsConstructor
3 public class PostService {
4

5 private final PostRepository postRepository ;


6 private final UserRepository userRepository ;
7 private final CommentRepository commentRepository ;
8

9 @Transactional ( readOnly = true )


10 public List < Post > getAllPosts () {

35
CHAPITRE 5. MUTATIONS

11 return postRepository . findAll () ;


12 }
13

14 @Transactional ( readOnly = true )


15 public Optional < Post > getPostById ( Long id ) {
16 return postRepository . findById ( id ) ;
17 }
18

19 @Transactional ( readOnly = true )


20 public List < Post > getPostsByStatus ( PostStatus status ) {
21 return postRepository . findByStatus ( status ) ;
22 }
23

24 @Transactional
25 public Post createPost ( CreatePostInput input , String
authorEmail ) {
26 User author = userRepository . findByEmail ( authorEmail )
27 . orElseThrow (() -> new UserNotFoundException (
28 " Auteur non trouve "
29 ));
30

31 Post post = Post . builder ()


32 . title ( input . title () )
33 . content ( input . content () )
34 . status ( input . status () != null ? input . status ()
35 : PostStatus . DRAFT )
36 . author ( author )
37 . build () ;
38

39 return postRepository . save ( post ) ;


40 }
41

42 @Transactional
43 public Post publishPost ( Long id ) {
44 Post post = postRepository . findById ( id )
45 . orElseThrow (() -> new PostNotFoundException (
46 " Post non trouve : " + id
47 ));
48 post . setStatus ( PostStatus . PUBLISHED ) ;
49 return postRepository . save ( post ) ;
50 }

36
5.6. EXEMPLES DE MUTATIONS

51

52 @Transactional
53 public Comment addComment ( AddCommentInput input , String email )
{
54 Post post = postRepository . findById ( input . postId () )
55 . orElseThrow (() -> new PostNotFoundException (
56 " Post non trouve : " + input . postId ()
57 ));
58 User author = userRepository . findByEmail ( email )
59 . orElseThrow (() -> new UserNotFoundException (
60 " Utilisateur non trouve "
61 ));
62

63 Comment comment = Comment . builder ()


64 . text ( input . text () )
65 . author ( author )
66 . post ( post )
67 . build () ;
68

69 return commentRepository . save ( comment ) ;


70 }
71 }

Listing 5.6 – [Link]

5.6 Exemples de mutations

1 mutation {
2 createUser ( input : {
3 name : " Bob Martin "
4 email : " bob@example . com "
5 password : " secureP@ss123 "
6 role : MODERATOR
7 }) {
8 id
9 name
10 email
11 role
12 createdAt
13 }

37
CHAPITRE 5. MUTATIONS

14 }

Listing 5.7 – Création d’un utilisateur

1 mutation {
2 createPost ( input : {
3 title : " Mon premier article "
4 content : " Contenu de l ' article ... "
5 status : DRAFT
6 }) {
7 id
8 title
9 status
10 }
11 }
12

13 # Puis publication
14 mutation {
15 publishPost ( id : " 1 " ) {
16 id
17 title
18 status
19 updatedAt
20 }
21 }

Listing 5.8 – Création et publication d’un post

38
Chapitre 6

Problème N+1 et DataLoader

6.1 Comprendre le problème N+1


Le problème N+1 est le piège de performance le plus courant en GraphQL. Il survient
lorsqu’un résolveur exécute une requête à la base de données pour chaque élément d’une
liste.

Figure 6.1 – Problème N+1 et sa solution avec DataLoader

Exemple concret : Considérons la requête suivante :

1 query {
2 posts {

39
CHAPITRE 6. PROBLÈME N+1 ET DATALOADER

3 title
4 author {
5 name
6 }
7 }
8 }

Listing 6.1 – Requête déclenchant le problème N+1

Sans optimisation, si nous avons 100 posts, cette requête génèrera :


1. 1 requête SQL pour récupérer tous les posts
2. 100 requêtes SQL pour récupérer l’auteur de chaque post
Soit 101 requêtes SQL au lieu de 2 !

6.2 Solution : @BatchMapping


Spring for GraphQL fournit l’annotation @BatchMapping qui regroupe automatique-
ment les appels en lots :

Figure 6.2 – Fonctionnement du @BatchMapping

40
6.2. SOLUTION : @BATCHMAPPING

1 @Controller
2 public class PostController {
3

4 @SchemaMapping ( typeName = " Post " , field = " author " )


5 public User getAuthor ( Post post ) {
6 // Appele N fois = N requetes SQL !
7 return userRepository . findById ( post . getAuthor () . getId () )
8 . orElse ( null ) ;
9 }
10 }

Listing 6.2 – Résolveur SANS BatchMapping (problème N+1)

1 @Controller
2 @RequiredArgsConstructor
3 public class PostController {
4

5 private final UserRepository userRepository ;


6

7 @BatchMapping ( typeName = " Post " , field = " author " )


8 public Map < Post , User > getAuthors ( List < Post > posts ) {
9 // Collecte des IDs uniques
10 Set < Long > authorIds = posts . stream ()
11 . map ( p -> p . getAuthor () . getId () )
12 . collect ( Collectors . toSet () ) ;
13

14 // UNE SEULE requete SQL


15 Map < Long , User > usersById = userRepository
16 . findAllById ( authorIds ) . stream ()
17 . collect ( Collectors . toMap ( User :: getId , u -> u ) ) ;
18

19 // Association post -> auteur


20 return posts . stream ()
21 . collect ( Collectors . toMap (
22 post -> post ,
23 post -> usersById . get ( post . getAuthor () . getId () )
24 ));
25 }
26 }

Listing 6.3 – Résolveur AVEC @BatchMapping (optimisé)

41
CHAPITRE 6. PROBLÈME N+1 ET DATALOADER

6.3 BatchMapping pour les collections

Pour les relations One-to-Many (ex. : les posts d’un utilisateur) :

1 @Controller
2 @RequiredArgsConstructor
3 public class UserController {
4

5 private final PostRepository postRepository ;


6

7 @BatchMapping ( typeName = " User " , field = " posts " )


8 public Map < User , List < Post > > getPosts ( List < User > users ) {
9 List < Long > userIds = users . stream ()
10 . map ( User :: getId )
11 . toList () ;
12

13 // UNE SEULE requete pour tous les posts


14 List < Post > allPosts = postRepository
15 . findByAuthorIdIn ( userIds ) ;
16

17 // Groupement par auteur


18 Map < Long , List < Post > > postsByAuthorId = allPosts . stream ()
19 . collect ( Collectors . groupingBy (
20 p -> p . getAuthor () . getId ()
21 ));
22

23 // Association user -> posts


24 return users . stream ()
25 . collect ( Collectors . toMap (
26 user -> user ,
27 user -> postsByAuthorId . getOrDefault (
28 user . getId () , List . of ()
29 )
30 ));
31 }
32 }

Listing 6.4 – BatchMapping pour les listes

42
6.4. COMPARAISON DES PERFORMANCES

6.4 Comparaison des performances

Table 6.1 – Impact du DataLoader sur les performances

Scénario (100 posts) Sans DataLoader Avec DataLoader

Requêtes SQL 101 2


Temps de réponse moyen ∼500ms ∼25ms
Charge base de données Très élevée Faible
Scalabilité Mauvaise Bonne

Attention
Le problème N+1 est souvent invisible pendant le développement avec de petits jeux
de données. Activez toujours [Link]-sql=true en développement pour
détecter les requêtes excessives. En production, utilisez les métriques pour surveiller
le nombre de requêtes SQL par requête GraphQL.

43
Chapitre 7

Gestion des erreurs

7.1 Modèle d’erreur GraphQL


Contrairement aux APIs REST qui utilisent les codes HTTP pour signaler les erreurs,
GraphQL retourne toujours un code HTTP 200 et inclut les erreurs dans le corps de la
réponse.

Figure 7.1 – Typologie des erreurs GraphQL

La structure d’une réponse GraphQL avec erreurs :

{
" data " : null ,
" errors " : [

44
7.2. EXCEPTIONS PERSONNALISÉES

{
" message " : " Utilisateur non trouve " ,
" locations " : [ { " line " : 2 , " column " : 3 } ] ,
" path " : [ " user " ] ,
" extensions " : {
" classification " : " NOT_FOUND " ,
" code " : " USER_NOT_FOUND "
}
}
]
}

Listing 7.1 – Structure d’une réponse avec erreur

7.2 Exceptions personnalisées

1 public class GraphQLException extends RuntimeException {


2 private final String code ;
3

4 public GraphQLException ( String message , String code ) {


5 super ( message ) ;
6 this . code = code ;
7 }
8

9 public String getCode () {


10 return code ;
11 }
12 }
13

14 public class UserNotFoundException extends GraphQLException {


15 public UserNotFoundException ( String message ) {
16 super ( message , " USER_NOT_FOUND " ) ;
17 }
18 }
19

20 public class DuplicateEmailException extends GraphQLException {


21 public DuplicateEmailException ( String message ) {
22 super ( message , " DUPLICATE_EMAIL " ) ;
23 }
24 }

45
CHAPITRE 7. GESTION DES ERREURS

25

26 public class PostNotFoundException extends GraphQLException {


27 public PostNotFoundException ( String message ) {
28 super ( message , " POST_NOT_FOUND " ) ;
29 }
30 }
31

32 public class UnauthorizedException extends GraphQLException {


33 public UnauthorizedException ( String message ) {
34 super ( message , " UNAUTHORIZED " ) ;
35 }
36 }

Listing 7.2 – Exceptions personnalisées

7.3 DataFetcherExceptionResolver
Spring for GraphQL permet de personnaliser la gestion des erreurs via un DataFetcherExceptionReso

1 @Component
2 public class GraphQLExceptionHandler
3 implements DataFetcherExceptionResolverAdapter {
4

5 @Override
6 protected GraphQLError resolveToSingleError (
7 Throwable ex , DataFetchingEnvironment env ) {
8

9 if ( ex instanceof GraphQLException graphQLEx ) {


10 return GraphqlErrorBuilder . newError ( env )
11 . message ( graphQLEx . getMessage () )
12 . errorType ( mapErrorType ( graphQLEx ) )
13 . extensions ( Map . of ( " code " , graphQLEx . getCode () ) )
14 . build () ;
15 }
16

17 if ( ex instanceof ConstraintViolationException cve ) {


18 String message = cve . getConstraintViolations ()
19 . stream ()
20 . map ( v -> v . getPropertyPath () + " : "
21 + v . getMessage () )
22 . collect ( Collectors . joining ( " , " ) ) ;

46
7.4. ERREURS PARTIELLES

23

24 return GraphqlErrorBuilder . newError ( env )


25 . message ( " Erreur de validation : " + message )
26 . errorType ( ErrorType . BAD_REQUEST )
27 . extensions ( Map . of ( " code " , " VALIDATION_ERROR " ) )
28 . build () ;
29 }
30

31 // Erreur generique ( ne pas exposer les details )


32 return GraphqlErrorBuilder . newError ( env )
33 . message ( " Erreur interne du serveur " )
34 . errorType ( ErrorType . INTERNAL_ERROR )
35 . build () ;
36 }
37

38 private ErrorType mapErrorType ( GraphQLException ex ) {


39 return switch ( ex . getCode () ) {
40 case " USER_NOT_FOUND " , " POST_NOT_FOUND "
41 -> ErrorType . NOT_FOUND ;
42 case " DUPLICATE_EMAIL " , " VALIDATION_ERROR "
43 -> ErrorType . BAD_REQUEST ;
44 case " UNAUTHORIZED "
45 -> ErrorType . UNAUTHORIZED ;
46 case " FORBIDDEN "
47 -> ErrorType . FORBIDDEN ;
48 default
49 -> ErrorType . INTERNAL_ERROR ;
50 };
51 }
52 }

Listing 7.3 – Gestionnaire global d’erreurs GraphQL

7.4 Erreurs partielles


L’un des avantages de GraphQL est la possibilité de retourner des données partielles
avec des erreurs. Si un champ échoue, les autres champs peuvent toujours être résolus :

{
" data " : {
" user " : {

47
CHAPITRE 7. GESTION DES ERREURS

" name " : " Alice " ,


" email " : " alice@example . com " ,
" posts " : null
}
},
" errors " : [
{
" message " : " Erreur lors du chargement des posts " ,
" path " : [ " user " , " posts " ] ,
" extensions " : {
" classification " : " INTERNAL_ERROR "
}
}
]
}

Listing 7.4 – Réponse avec données partielles et erreur

Conseil
Les erreurs partielles sont une fonctionnalité puissante de GraphQL : même si un
résolveur échoue, le client reçoit les données des autres champs. C’est particulièrement
utile pour les dashboards où certaines sections peuvent être indépendantes.

48
Chapitre 8

Pagination et filtrage

8.1 Stratégies de pagination


La pagination est essentielle pour éviter de charger des volumes massifs de données.
GraphQL propose deux stratégies principales.

Figure 8.1 – Pagination Offset vs Pagination Cursor

8.2 Pagination Offset (classique)


La pagination offset est la plus simple, basée sur un numéro de page et une taille :

1 type Query {
2 users ( page : Int = 0 , size : Int = 10) : UserPage !
3 posts ( page : Int = 0 , size : Int = 10 ,
4 filter : PostFilter ) : PostPage !
5 }
6

7 type UserPage {
8 content : [ User !]!
9 totalElements : Int !
10 totalPages : Int !
11 currentPage : Int !
12 hasNext : Boolean !
13 hasPrevious : Boolean !
14 }
15

16 type PostPage {
17 content : [ Post !]!

49
CHAPITRE 8. PAGINATION ET FILTRAGE

18 totalElements : Int !
19 totalPages : Int !
20 currentPage : Int !
21 hasNext : Boolean !
22 }

Listing 8.1 – Schéma pour pagination offset

1 @Controller
2 @RequiredArgsConstructor
3 public class UserController {
4

5 private final UserService userService ;


6

7 @QueryMapping
8 public UserPage users ( @Argument int page ,
9 @Argument int size ) {
10 Page < User > result = userService . getUsers ( page , size ) ;
11

12 return new UserPage (


13 result . getContent () ,
14 ( int ) result . getTotalElements () ,
15 result . getTotalPages () ,
16 result . getNumber () ,
17 result . hasNext () ,
18 result . hasPrevious ()
19 );
20 }
21 }

Listing 8.2 – Implémentation de la pagination offset

8.3 Pagination Cursor (Relay-style)


La pagination cursor est recommandée pour les jeux de données volumineux ou dyna-
miques. Elle se base sur la spécification Relay :

1 type Query {
2 usersConnection (
3 first : Int
4 after : String

50
8.3. PAGINATION CURSOR (RELAY-STYLE)

5 last : Int
6 before : String
7 ): UserConnection !
8 }
9

10 type UserConnection {
11 edges : [ UserEdge !]!
12 pageInfo : PageInfo !
13 totalCount : Int !
14 }
15

16 type UserEdge {
17 node : User !
18 cursor : String !
19 }
20

21 type PageInfo {
22 hasNextPage : Boolean !
23 hasPreviousPage : Boolean !
24 startCursor : String
25 endCursor : String
26 }

Listing 8.3 – Schéma pagination cursor (Relay)

1 @Controller
2 @RequiredArgsConstructor
3 public class UserConnectionController {
4

5 private final UserRepository userRepository ;


6

7 @QueryMapping
8 public UserConnection usersConnection (
9 @Argument Integer first ,
10 @Argument String after ) {
11

12 int limit = ( first != null ) ? first : 10;


13 Long afterId = decodeCursor ( after ) ;
14

15 List < User > users ;


16 if ( afterId != null ) {
17 users = userRepository

51
CHAPITRE 8. PAGINATION ET FILTRAGE

18 . findByIdGreaterThanOrderByIdAsc (
19 afterId , PageRequest . of (0 , limit + 1)
20 );
21 } else {
22 users = userRepository
23 . findAllByOrderByIdAsc (
24 PageRequest . of (0 , limit + 1)
25 );
26 }
27

28 boolean hasNext = users . size () > limit ;


29 if ( hasNext ) {
30 users = users . subList (0 , limit ) ;
31 }
32

33 List < UserEdge > edges = users . stream ()


34 . map ( u -> new UserEdge (u , encodeCursor ( u . getId () ) ) )
35 . toList () ;
36

37 PageInfo pageInfo = new PageInfo (


38 hasNext ,
39 afterId != null ,
40 edges . isEmpty () ? null
41 : edges . get (0) . cursor () ,
42 edges . isEmpty () ? null
43 : edges . get ( edges . size () - 1) . cursor ()
44 );
45

46 long total = userRepository . count () ;


47 return new UserConnection ( edges , pageInfo , total ) ;
48 }
49

50 private String encodeCursor ( Long id ) {


51 return Base64 . getEncoder ()
52 . encodeToString (( " cursor : " + id ) . getBytes () ) ;
53 }
54

55 private Long decodeCursor ( String cursor ) {


56 if ( cursor == null ) return null ;
57 String decoded = new String (
58 Base64 . getDecoder () . decode ( cursor )

52
8.4. FILTRAGE ET TRI

59 );
60 return Long . parseLong (
61 decoded . replace ( " cursor : " , " " )
62 );
63 }
64 }

Listing 8.4 – Implémentation pagination cursor

8.4 Filtrage et tri

1 input PostFilter {
2 status : PostStatus
3 authorId : ID
4 search : String
5 createdAfter : DateTime
6 createdBefore : DateTime
7 }
8

9 input PostSort {
10 field : PostSortField !
11 order : SortOrder = ASC
12 }
13

14 enum PostSortField {
15 TITLE
16 CREATED_AT
17 UPDATED_AT
18 }
19

20 type Query {
21 posts ( filter : PostFilter , sort : PostSort ,
22 page : Int = 0 , size : Int = 10) : PostPage !
23 }

Listing 8.5 – Schéma avec filtrage

1 @Service
2 @RequiredArgsConstructor
3 public class PostService {
4

53
CHAPITRE 8. PAGINATION ET FILTRAGE

5 private final PostRepository postRepository ;


6

7 public Page < Post > getPosts ( PostFilter filter ,


8 PostSort sort ,
9 int page , int size ) {
10 Specification < Post > spec = Specification . where ( null ) ;
11

12 if ( filter != null ) {
13 if ( filter . status () != null ) {
14 spec = spec . and (( root , query , cb ) ->
15 cb . equal ( root . get ( " status " ) , filter . status () )
16 );
17 }
18 if ( filter . authorId () != null ) {
19 spec = spec . and (( root , query , cb ) ->
20 cb . equal ( root . get ( " author " ) . get ( " id " ) ,
21 filter . authorId () )
22 );
23 }
24 if ( filter . search () != null ) {
25 String pattern = " % " + filter . search ()
26 . toLowerCase () + " % " ;
27 spec = spec . and (( root , query , cb ) ->
28 cb . or (
29 cb . like ( cb . lower ( root . get ( " title " ) ) ,
30 pattern ) ,
31 cb . like ( cb . lower ( root . get ( " content " ) ) ,
32 pattern )
33 )
34 );
35 }
36 }
37

38 Sort jpaSort = Sort . by ( " createdAt " ) . descending () ;


39 if ( sort != null ) {
40 Sort . Direction dir = sort . order () == SortOrder . ASC
41 ? Sort . Direction . ASC
42 : Sort . Direction . DESC ;
43 jpaSort = Sort . by ( dir , sort . field () . toFieldName () ) ;
44 }
45

54
8.4. FILTRAGE ET TRI

46 return postRepository . findAll (


47 spec , PageRequest . of ( page , size , jpaSort )
48 );
49 }
50 }

Listing 8.6 – Service avec filtrage dynamique (Specification)

Table 8.1 – Comparaison des stratégies de pagination

Critère Offset Cursor

Simplicité Simple à implémenter complexe


Accès direct Oui (page N) Non (séquentiel)
Insertions/suppressions Peut sauter des items Stable
Performances Dégrade sur gros volumes Constantes
Cas d’usage Panels d’admin Flux infinis, mobile

55
Chapitre 9

Subscriptions (temps réel)

9.1 Principe des subscriptions


Les subscriptions permettent au serveur d’envoyer des données au client en temps réel
via WebSocket. C’est une alternative élégante au polling.

Figure 9.1 – Flux de communication d’une subscription GraphQL via WebSocket

9.2 Configuration WebSocket

1 < dependency >


2 < groupId > org . springframework . boot </ groupId >
3 < artifactId > spring - boot - starter - websocket </ artifactId >
4 </ dependency >

Listing 9.1 – Dépendance WebSocket dans [Link]

56
9.3. IMPLÉMENTATION AVEC REACTOR (FLUX)

spring :
graphql :
websocket :
path : / graphql
connection - init - timeout : 30 s

Listing 9.2 – Configuration WebSocket dans [Link]

9.3 Implémentation avec Reactor (Flux)


Spring for GraphQL utilise le type Flux de Project Reactor pour les subscriptions :

1 @Service
2 public class PostEventPublisher {
3

4 private final Sinks . Many < Post > postSink =


5 Sinks . many () . multicast () . onBackpressureBuffer () ;
6

7 private final Sinks . Many < Comment > commentSink =


8 Sinks . many () . multicast () . onBackpressureBuffer () ;
9

10 public void publishPostCreated ( Post post ) {


11 postSink . tryEmitNext ( post ) ;
12 }
13

14 public void publishCommentAdded ( Comment comment ) {


15 commentSink . tryEmitNext ( comment ) ;
16 }
17

18 public Flux < Post > getPostPublishedStream () {


19 return postSink . asFlux () ;
20 }
21

22 public Flux < Comment > getCommentAddedStream ( Long postId ) {


23 return commentSink . asFlux ()
24 . filter ( c -> c. getPost () . getId () . equals ( postId ) ) ;
25 }
26 }

Listing 9.3 – Service de publication d’événements

57
CHAPITRE 9. SUBSCRIPTIONS (TEMPS RÉEL)

1 @Controller
2 @RequiredArgsConstructor
3 public class SubscriptionController {
4

5 private final PostEventPublisher eventPublisher ;


6

7 @SubscriptionMapping
8 public Flux < Post > postPublished () {
9 return eventPublisher . getPostPublishedStream () ;
10 }
11

12 @SubscriptionMapping
13 public Flux < Comment > commentAdded ( @Argument Long postId ) {
14 return eventPublisher
15 . getCommentAddedStream ( postId ) ;
16 }
17 }

Listing 9.4 – Contrôleur de subscriptions

9.4 Déclencher les événements


Il faut modifier les services pour publier les événements :

1 @Service
2 @RequiredArgsConstructor
3 public class PostService {
4

5 private final PostRepository postRepository ;


6 private final PostEventPublisher eventPublisher ;
7

8 @Transactional
9 public Post publishPost ( Long id ) {
10 Post post = postRepository . findById ( id )
11 . orElseThrow (() -> new PostNotFoundException (
12 " Post non trouve "
13 ));
14 post . setStatus ( PostStatus . PUBLISHED ) ;
15 Post saved = postRepository . save ( post ) ;
16

17 // Publier l ' evenement temps reel

58
9.5. CÔTÉ CLIENT

18 eventPublisher . publishPostCreated ( saved ) ;


19

20 return saved ;
21 }
22

23 @Transactional
24 public Comment addComment ( AddCommentInput input ,
25 String email ) {
26 // ... creation du commentaire ...
27 Comment saved = commentRepository . save ( comment ) ;
28

29 // Notifier les abonnes


30 eventPublisher . publishCommentAdded ( saved ) ;
31

32 return saved ;
33 }
34 }

Listing 9.5 – Publication lors de la création

9.5 Côté client

1 subscription {
2 postPublished {
3 id
4 title
5 author {
6 name
7 }
8 createdAt
9 }
10 }
11

12 subscription {
13 commentAdded ( postId : " 42 " ) {
14 id
15 text
16 author {
17 name
18 }

59
CHAPITRE 9. SUBSCRIPTIONS (TEMPS RÉEL)

19 }
20 }

Listing 9.6 – Exemple de subscription côté client

Note
Les subscriptions utilisent le protocole graphql-transport-ws. Les clients JavaScript
populaires comme Apollo Client et urql supportent nativement ce protocole.

60
Chapitre 10

Authentification et sécurisation

10.1 Enjeux de sécurité en GraphQL

La sécurité d’une API GraphQL nécessite une approche multi-couches. Contrairement


à REST où chaque endpoint peut avoir ses propres règles, GraphQL possède un point
d’entrée unique qu’il faut protéger en profondeur.

61
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION

62
10.2. AUTHENTIFICATION JWT AVEC SPRING SECURITY

10.2 Authentification JWT avec Spring Security

Figure 10.2 – Flux d’authentification JWT complet

10.2.1 Dépendances

1 < dependency >


2 < groupId > org . springframework . boot </ groupId >
3 < artifactId > spring - boot - starter - security </ artifactId >
4 </ dependency >
5 < dependency >
6 < groupId > io . jsonwebtoken </ groupId >
7 < artifactId > jjwt - api </ artifactId >
8 < version > 0.12.5 </ version >
9 </ dependency >
10 < dependency >
11 < groupId > io . jsonwebtoken </ groupId >
12 < artifactId > jjwt - impl </ artifactId >
13 < version > 0.12.5 </ version >

63
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION

14 < scope > runtime </ scope >


15 </ dependency >
16 < dependency >
17 < groupId > io . jsonwebtoken </ groupId >
18 < artifactId > jjwt - jackson </ artifactId >
19 < version > 0.12.5 </ version >
20 < scope > runtime </ scope >
21 </ dependency >

Listing 10.1 – Dépendances sécurité dans [Link]

10.2.2 Service JWT

1 @Service
2 public class JwtService {
3

4 @Value ( " $ { jwt . secret } ")


5 private String secretKey ;
6

7 @Value ( " $ { jwt . expiration :86400000} " )


8 private long expiration ; // 24 h par defaut
9

10 public String generateToken ( User user ) {


11 return Jwts . builder ()
12 . subject ( user . getEmail () )
13 . claim ( " role " , user . getRole () . name () )
14 . claim ( " userId " , user . getId () )
15 . issuedAt ( new Date () )
16 . expiration ( new Date (
17 System . currentTimeMillis () + expiration
18 ))
19 . signWith ( getSigningKey () )
20 . compact () ;
21 }
22

23 public String extractEmail ( String token ) {


24 return extractClaim ( token , Claims :: getSubject ) ;
25 }
26

27 public boolean isTokenValid ( String token ,


28 UserDetails userDetails ) {

64
10.2. AUTHENTIFICATION JWT AVEC SPRING SECURITY

29 final String email = extractEmail ( token ) ;


30 return email . equals ( userDetails . getUsername () )
31 && ! isTokenExpired ( token ) ;
32 }
33

34 private boolean isTokenExpired ( String token ) {


35 return extractClaim ( token , Claims :: getExpiration )
36 . before ( new Date () ) ;
37 }
38

39 private <T > T extractClaim ( String token ,


40 Function < Claims , T > resolver ) {
41 Claims claims = Jwts . parser ()
42 . verifyWith ( getSigningKey () )
43 . build ()
44 . parseSignedClaims ( token )
45 . getPayload () ;
46 return resolver . apply ( claims ) ;
47 }
48

49 private SecretKey getSigningKey () {


50 return Keys . hmacShaKeyFor (
51 Decoders . BASE64 . decode ( secretKey )
52 );
53 }
54 }

Listing 10.2 – [Link]

10.2.3 Configuration Spring Security

1 @Configuration
2 @EnableWebSecurity
3 @EnableMethodSecurity ( prePostEnabled = true )
4 @RequiredArgsConstructor
5 public class SecurityConfig {
6

7 private final JwtAuthenticationFilter jwtFilter ;


8 private final UserDetailsService userDetailsService ;
9

10 @Bean

65
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION

11 public SecurityFilterChain securityFilterChain (


12 HttpSecurity http ) throws Exception {
13 return http
14 . csrf ( csrf -> csrf . disable () )
15 . sessionManagement ( session ->
16 session . sessionCreationPolicy ( STATELESS )
17 )
18 . authorizeHttpRequests ( auth -> auth
19 . requestMatchers ( " / graphiql /** " ) . permitAll ()
20 . requestMatchers ( " / graphql " ) . permitAll ()
21 . anyRequest () . authenticated ()
22 )
23 . addFilterBefore ( jwtFilter ,
24 UsernamePasswordAuthenticationFilter . class )
25 . build () ;
26 }
27

28 @Bean
29 public PasswordEncoder passwordEncoder () {
30 return new BCryptPasswordEncoder () ;
31 }
32

33 @Bean
34 public AuthenticationManager authenticationManager (
35 AuthenticationConfiguration config )
36 throws Exception {
37 return config . getAuthenticationManager () ;
38 }
39 }

Listing 10.3 – [Link]

10.2.4 Filtre JWT

1 @Component
2 @RequiredArgsConstructor
3 public class JwtAuthenticationFilter
4 extends OncePerRequestFilter {
5

6 private final JwtService jwtService ;


7 private final UserDetailsService userDetailsService ;

66
10.2. AUTHENTIFICATION JWT AVEC SPRING SECURITY

9 @Override
10 protected void doFilterInternal (
11 HttpServletRequest request ,
12 HttpServletResponse response ,
13 FilterChain chain ) throws ServletException ,
14 IOException {
15 String authHeader = request
16 . getHeader ( " Authorization " ) ;
17

18 if ( authHeader == null
19 || ! authHeader . startsWith ( " Bearer " ) ) {
20 chain . doFilter ( request , response ) ;
21 return ;
22 }
23

24 String token = authHeader . substring (7) ;


25 String email = jwtService . extractEmail ( token ) ;
26

27 if ( email != null && SecurityContextHolder


28 . getContext () . getAuthentication () == null ) {
29

30 UserDetails userDetails = userDetailsService


31 . loadUserByUsername ( email ) ;
32

33 if ( jwtService . isTokenValid ( token , userDetails ) ) {


34 var authToken =
35 new UsernamePasswordAuthenticationToken (
36 userDetails , null ,
37 userDetails . getAuthorities ()
38 );
39 authToken . setDetails (
40 new WebAuthenticationDetailsSource ()
41 . buildDetails ( request )
42 );
43 SecurityContextHolder . getContext ()
44 . setAuthentication ( authToken ) ;
45 }
46 }
47

48 chain . doFilter ( request , response ) ;

67
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION

49 }
50 }

Listing 10.4 – [Link]

10.3 Mutation de login

1 @Controller
2 @RequiredArgsConstructor
3 public class AuthController {
4

5 private final AuthenticationManager authManager ;


6 private final UserRepository userRepository ;
7 private final JwtService jwtService ;
8

9 @MutationMapping
10 public AuthPayload login ( @Argument String email ,
11 @Argument String password ) {
12 authManager . authenticate (
13 new UsernamePasswordAuthenticationToken (
14 email , password
15 )
16 );
17

18 User user = userRepository . findByEmail ( email )


19 . orElseThrow () ;
20 String token = jwtService . generateToken ( user ) ;
21

22 return new AuthPayload ( token , user ) ;


23 }
24 }

Listing 10.5 – [Link]

10.4 Autorisation au niveau des résolveurs

1 @Controller
2 @RequiredArgsConstructor
3 public class AdminController {

68
10.5. PROTECTION CONTRE LES ABUS

5 private final UserService userService ;


6

7 @MutationMapping
8 @PreAuthorize ( " hasRole ( ' ADMIN ') " )
9 public boolean deleteUser ( @Argument Long id ) {
10 return userService . deleteUser ( id ) ;
11 }
12

13 @QueryMapping
14 @PreAuthorize ( " isAuthenticated () " )
15 public User me ( @AuthenticationPrincipal
16 UserDetails userDetails ) {
17 return userService
18 . getUserByEmail ( userDetails . getUsername () )
19 . orElseThrow () ;
20 }
21 }

Listing 10.6 – Autorisation avec @PreAuthorize

10.5 Protection contre les abus

10.5.1 Limitation de la profondeur des requêtes

1 @Configuration
2 public class GraphQLSecurityConfig {
3

4 @Bean
5 public Instrumentation maxQueryDepthInstrumentation () {
6 return new MaxQueryDepthInstrumentation (10) ;
7 }
8

9 @Bean
10 public Instrumentation maxQueryComplexity () {
11 return new MaxQueryComplexityInstrumentation (200) ;
12 }
13 }

Listing 10.7 – Configuration de la profondeur maximale

69
CHAPITRE 10. AUTHENTIFICATION ET SÉCURISATION

10.5.2 Limitation du débit (Rate Limiting)

1 @Component
2 public class RateLimitInterceptor
3 implements WebGraphQlInterceptor {
4

5 private final Map < String , Bucket > buckets =


6 new ConcurrentHashMap < >() ;
7

8 @Override
9 public Mono < WebGraphQlResponse > intercept (
10 WebGraphQlRequest request ,
11 Chain chain ) {
12 String clientIp = request . getHeaders ()
13 . getFirst ( "X - Forwarded - For " ) ;
14

15 Bucket bucket = buckets . computeIfAbsent (


16 clientIp , k -> createBucket ()
17 );
18

19 if ( bucket . tryConsume (1) ) {


20 return chain . next ( request ) ;
21 }
22

23 return Mono . error ( new RuntimeException (


24 " Trop de requetes . Reessayez plus tard . "
25 ));
26 }
27

28 private Bucket createBucket () {


29 return Bucket . builder ()
30 . addLimit ( Bandwidth . classic (
31 100 ,
32 Refill . intervally (100 , Duration . ofMinutes (1) )
33 ))
34 . build () ;
35 }
36 }

Listing 10.8 – Rate Limiting avec Bucket4j

70
Chapitre 11

Tests

11.1 Stratégie de tests

Figure 11.1 – Pyramide de tests pour une application GraphQL

11.2 Tests unitaires des services

1 @ExtendWith ( MockitoExtension . class )


2 class UserServiceTest {
3

4 @Mock

71
CHAPITRE 11. TESTS

5 private UserRepository userRepository ;


6

7 @Mock
8 private PasswordEncoder passwordEncoder ;
9

10 @InjectMocks
11 private UserService userService ;
12

13 @Test
14 void createUser_shouldCreateSuccessfully () {
15 // Given
16 var input = new CreateUserInput (
17 " Alice " , " alice@test . com " , " password " , Role . USER
18 );
19 when ( userRepository . existsByEmail ( " alice@test . com " ) )
20 . thenReturn ( false ) ;
21 when ( passwordEncoder . encode ( " password " ) )
22 . thenReturn ( " encoded " ) ;
23 when ( userRepository . save ( any ( User . class ) ) )
24 . thenAnswer ( inv -> {
25 User u = inv . getArgument (0) ;
26 u . setId (1 L) ;
27 return u ;
28 }) ;
29

30 // When
31 User result = userService . createUser ( input ) ;
32

33 // Then
34 assertThat ( result . getName () ) . isEqualTo ( " Alice " ) ;
35 assertThat ( result . getEmail () )
36 . isEqualTo ( " alice@test . com " ) ;
37 verify ( userRepository ) . save ( any ( User . class ) ) ;
38 }
39

40 @Test
41 void createUser_duplicateEmail_shouldThrow () {
42 var input = new CreateUserInput (
43 " Bob " , " exists@test . com " , " pass " , null
44 );
45 when ( userRepository . existsByEmail ( " exists@test . com " ) )

72
11.3. TESTS D’INTÉGRATION AVEC GRAPHQLTESTER

46 . thenReturn ( true ) ;
47

48 assertThatThrownBy (
49 () -> userService . createUser ( input )
50 ) . isInstanceOf ( DuplicateEmailException . class ) ;
51 }
52 }

Listing 11.1 – [Link]

11.3 Tests d’intégration avec GraphQlTester


Spring for GraphQL fournit GraphQlTester pour tester les requêtes :

1 @SpringBootTest
2 @AutoConfigureGraphQlTester
3 class UserControllerIntegrationTest {
4

5 @Autowired
6 private GraphQlTester graphQlTester ;
7

8 @Autowired
9 private UserRepository userRepository ;
10

11 @BeforeEach
12 void setUp () {
13 userRepository . deleteAll () ;
14 User user = User . builder ()
15 . name ( " Alice " )
16 . email ( " alice@test . com " )
17 . password ( " encoded " )
18 . role ( Role . USER )
19 . build () ;
20 userRepository . save ( user ) ;
21 }
22

23 @Test
24 void queryUser_shouldReturnUser () {
25 graphQlTester . document ( " " "
26 query {
27 user ( id : "1 " ) {

73
CHAPITRE 11. TESTS

28 name
29 email
30 role
31 }
32 }
33 """)
34 . execute ()
35 . path ( " user . name " ) . entity ( String . class )
36 . isEqualTo ( " Alice " )
37 . path ( " user . email " ) . entity ( String . class )
38 . isEqualTo ( " alice@test . com " )
39 . path ( " user . role " ) . entity ( String . class )
40 . isEqualTo ( " USER " ) ;
41 }
42

43 @Test
44 void queryUsers_shouldReturnPage () {
45 graphQlTester . document ( " " "
46 query {
47 users ( page : 0 , size : 10) {
48 content {
49 name
50 }
51 totalElements
52 hasNext
53 }
54 }
55 """)
56 . execute ()
57 . path ( " users . totalElements " )
58 . entity ( Integer . class ) . isEqualTo (1)
59 . path ( " users . hasNext " )
60 . entity ( Boolean . class ) . isEqualTo ( false )
61 . path ( " users . content [0]. name " )
62 . entity ( String . class ) . isEqualTo ( " Alice " ) ;
63 }
64 }

Listing 11.2 – Test d’intégration des queries

74
11.4. TESTS DES MUTATIONS

11.4 Tests des mutations

1 @SpringBootTest
2 @AutoConfigureGraphQlTester
3 class MutationIntegrationTest {
4

5 @Autowired
6 private GraphQlTester graphQlTester ;
7

8 @Test
9 void createUser_shouldReturnNewUser () {
10 graphQlTester . document ( " " "
11 mutation {
12 createUser ( input : {
13 name : " Bob "
14 email : " bob@test . com "
15 password : " securePass123 "
16 }) {
17 id
18 name
19 email
20 role
21 }
22 }
23 """)
24 . execute ()
25 . path ( " createUser . name " ) . entity ( String . class )
26 . isEqualTo ( " Bob " )
27 . path ( " createUser . role " ) . entity ( String . class )
28 . isEqualTo ( " USER " ) ;
29 }
30

31 @Test
32 void createUser_invalidEmail_shouldReturnError () {
33 graphQlTester . document ( " " "
34 mutation {
35 createUser ( input : {
36 name : " Test "
37 email : " invalid - email "
38 password : " pass "
39 }) {

75
CHAPITRE 11. TESTS

40 id
41 }
42 }
43 """)
44 . execute ()
45 . errors ()
46 . satisfy ( errors -> {
47 assertThat ( errors ) . isNotEmpty () ;
48 assertThat ( errors . get (0) . getMessage () )
49 . contains ( " validation " ) ;
50 }) ;
51 }
52 }

Listing 11.3 – Test des mutations

11.5 Tests avec HttpGraphQlTester (E2E)

1 @SpringBootTest ( webEnvironment =
2 SpringBootTest . WebEnvironment . RANDOM_PORT )
3 class E2EGraphQLTest {
4

5 @Autowired
6 private HttpGraphQlTester . Builder <? > builder ;
7

8 @Test
9 void authenticatedQuery_shouldWork () {
10 String token = obtainJwtToken () ;
11

12 HttpGraphQlTester tester = builder


13 . header ( " Authorization " , " Bearer " + token )
14 . build () ;
15

16 tester . document ( " ""


17 query {
18 me {
19 name
20 email
21 }
22 }

76
11.5. TESTS AVEC HTTPGRAPHQLTESTER (E2E)

23 """)
24 . execute ()
25 . path ( " me . name " ) . entity ( String . class )
26 . isEqualTo ( " Alice " ) ;
27 }
28 }

Listing 11.4 – Test E2E avec authentification

77
Chapitre 12

Déploiement et architecture avancée

12.1 Architecture de déploiement

Figure 12.1 – Architecture de déploiement typique

12.2 Containerisation avec Docker

# Phase de build
FROM eclipse - temurin :21 - jdk AS build
WORKDIR / app
COPY pom . xml .
COPY src ./ src
RUN ./ mvnw clean package - DskipTests

78
12.2. CONTAINERISATION AVEC DOCKER

# Phase de runtime
FROM eclipse - temurin :21 - jre
WORKDIR / app
COPY -- from = build / app / target /*. jar app . jar

EXPOSE 8080

ENTRYPOINT [" java " , " - jar " , " app . jar "]

Listing 12.1 – Dockerfile multi-stage

version : ' 3.8 '


services :
app :
build : .
ports :
- " 8080:8080 "
environment :
- SPRING_DATASOURCE_URL = jdbc : postgresql :// db :5432/ graphql_db
- SPRING_DATASOURCE_USERNAME = postgres
- SPRING_DATASOURCE_PASSWORD = postgres
depends_on :
- db
- redis

db :
image : postgres :16 - alpine
environment :
POSTGRES_DB : graphql_db
POSTGRES_USER : postgres
POSTGRES_PASSWORD : postgres
ports :
- " 5432:5432 "
volumes :
- pgdata :/ var / lib / postgresql / data

redis :
image : redis :7 - alpine
ports :
- " 6379:6379 "

volumes :

79
CHAPITRE 12. DÉPLOIEMENT ET ARCHITECTURE AVANCÉE

pgdata :

Listing 12.2 – [Link]

12.3 GraphQL dans une architecture microservices

Figure 12.2 – GraphQL comme gateway dans une architecture microservices

Dans une architecture microservices, GraphQL peut servir de gateway unifié. Deux
approches principales existent :

— Schema Stitching : le gateway combine les schémas de chaque microservice en un


schéma unifié.
— Apollo Federation : chaque microservice expose un sous-graphe, et le gateway les
compose automatiquement grâce à des directives spéciales (@key, @external, etc.).

12.4 Monitoring et observabilité

1 < dependency >


2 < groupId > org . springframework . boot </ groupId >

80
12.4. MONITORING ET OBSERVABILITÉ

3 < artifactId > spring - boot - starter - actuator </ artifactId >
4 </ dependency >
5 < dependency >
6 < groupId > io . micrometer </ groupId >
7 < artifactId > micrometer - registry - prometheus </ artifactId >
8 </ dependency >

Listing 12.3 – Dépendances pour le monitoring

management :
endpoints :
web :
exposure :
include : health , metrics , prometheus
metrics :
tags :
application : graphql - demo

spring :
graphql :
schema :
introspection :
enabled : false # Desactiver en production !

Listing 12.4 – Configuration Actuator pour GraphQL

1 @Component
2 @RequiredArgsConstructor
3 public class GraphQLMetricsInterceptor
4 implements WebGraphQlInterceptor {
5

6 private final MeterRegistry meterRegistry ;


7

8 @Override
9 public Mono < WebGraphQlResponse > intercept (
10 WebGraphQlRequest request , Chain chain ) {
11

12 long start = System . currentTimeMillis () ;


13 String operationName = request . getOperationName () ;
14

15 return chain . next ( request ) . doOnNext ( response -> {


16 long duration = System . currentTimeMillis () - start ;

81
CHAPITRE 12. DÉPLOIEMENT ET ARCHITECTURE AVANCÉE

17

18 meterRegistry . timer ( " graphql . request " ,


19 " operation " , operationName != null
20 ? operationName : " anonymous " ,
21 " status " , response . isValid ()
22 ? " success " : " error "
23 ) . record ( duration , TimeUnit . MILLISECONDS ) ;
24 }) ;
25 }
26 }

Listing 12.5 – Instrumentation personnalisée pour les métriques

12.5 Upload de fichiers

Figure 12.3 – Flux d’upload de fichiers via GraphQL

82
12.6. CONCEPTION DU SCHÉMA : BONNES PRATIQUES

12.6 Conception du schéma : bonnes pratiques

83
CHAPITRE 12. DÉPLOIEMENT ET ARCHITECTURE AVANCÉE

12.6.1 Règles de conception


1. Nommer clairement : utiliser des noms explicites (createUser plutôt que addUser).

2. Utiliser des types Input : toujours passer les arguments de mutation via un type
input.

3. Retourner les objets modifiés : une mutation doit retourner l’objet créé ou modifié.

4. Préférer les non-null : utiliser ! par défaut, rendre nullable uniquement si nécessaire.

5. Paginer les listes : ne jamais retourner de listes non bornées.

6. Versionner par dépréciation : utiliser @deprecated plutôt que créer un nouveau


schéma.

1 type User {
2 id : ID !
3 name : String !
4 fullName : String !
5 username : String @deprecated (
6 reason : " Utiliser 'name ' a la place "
7 )
8 }

Listing 12.6 – Utilisation de @deprecated

12.7 Résumé des bonnes pratiques

Table 12.1 – Résumé des bonnes pratiques GraphQL + Spring Boot

Domaine Recommandation

Performance Utiliser @BatchMapping systématiquement


Sécurité JWT + limitation profondeur + rate limiting
Erreurs DataFetcherExceptionResolver personnalisé
Pagination Cursor pour les listes dynamiques
Tests GraphQlTester pour chaque résolveur
Déploiement Docker + variables d’environnement
Monitoring Prometheus + métriques par opération
Schéma Types Input, non-null par défaut, dépréciation
Production Désactiver l’introspection et GraphiQL

84
12.7. RÉSUMÉ DES BONNES PRATIQUES

Conseil
Pour aller plus loin, explorez DGS Framework (Netflix), qui offre des fonctionnalités
avancées comme la génération de code à partir du schéma, ou Apollo Federation pour
les architectures microservices.

85

Vous aimerez peut-être aussi