Spring GraphQL en entretien : resolvers, DataLoaders et gestion du problème N+1
Préparez vos entretiens Spring GraphQL avec ce guide complet. Resolvers, DataLoaders, gestion N+1, mutations et bonnes pratiques pour réussir vos questions techniques.

Spring for GraphQL simplifie l'intégration de GraphQL dans les applications Spring Boot. Cette technologie devient incontournable pour les APIs modernes, et les questions d'entretien sur ce sujet se multiplient. Ce guide couvre les concepts essentiels : resolvers, DataLoaders, problème N+1 et patterns avancés.
Les recruteurs testent particulièrement la compréhension du problème N+1 et l'utilisation des DataLoaders. Ces deux sujets représentent 60% des questions Spring GraphQL en entretien.
Qu'est-ce que Spring for GraphQL ?
Spring for GraphQL constitue le successeur officiel de GraphQL Java Spring, intégré nativement au framework Spring depuis la version 2.7. Cette intégration apporte plusieurs avantages : support des annotations Spring, intégration avec Spring Security, et utilisation transparente de WebFlux ou MVC.
La configuration de base nécessite uniquement la dépendance spring-boot-starter-graphql et un schéma GraphQL.
dependencies {
// Starter Spring GraphQL avec Spring MVC
implementation("org.springframework.boot:spring-boot-starter-graphql")
implementation("org.springframework.boot:spring-boot-starter-web")
// Pour les tests GraphQL
testImplementation("org.springframework.graphql:spring-graphql-test")
}Le schéma GraphQL se place dans src/main/resources/graphql/ avec l'extension .graphqls.
# schema.graphqls
type Query {
# Récupération d'un article par son identifiant
article(id: ID!): Article
# Liste paginée des articles
articles(page: Int = 0, size: Int = 10): [Article!]!
}
type Article {
id: ID!
title: String!
content: String!
# Relation vers l'auteur
author: Author!
# Liste des commentaires
comments: [Comment!]!
createdAt: String!
}
type Author {
id: ID!
name: String!
email: String!
# Articles écrits par cet auteur
articles: [Article!]!
}
type Comment {
id: ID!
content: String!
author: Author!
createdAt: String!
}Cette architecture permet de déclarer les types et leurs relations. GraphQL génère automatiquement la documentation et valide les requêtes côté serveur.
Comment fonctionnent les resolvers Spring GraphQL ?
Les resolvers représentent le cœur de l'exécution GraphQL. Chaque champ du schéma peut avoir un resolver dédié. Spring utilise l'annotation @QueryMapping pour les requêtes racines et @SchemaMapping pour les relations.
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
public ArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
}
// Resolver pour Query.article(id)
@QueryMapping
public Article article(@Argument Long id) {
return articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}
// Resolver pour Query.articles(page, size)
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Resolver pour Article.author - appelé pour chaque article
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return authorRepository.findById(article.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
}
}Le resolver author s'exécute pour chaque article retourné. Cette architecture flexible permet de charger les données à la demande, mais introduit le problème N+1.
Un candidat qui implémente un resolver @SchemaMapping sans mentionner le problème N+1 montre une compréhension incomplète de GraphQL. Les recruteurs attendent systématiquement cette analyse.
Qu'est-ce que le problème N+1 en GraphQL ?
Le problème N+1 survient lorsqu'une requête GraphQL déclenche N requêtes supplémentaires pour charger les relations. Ce pattern destructeur se produit systématiquement avec les resolvers basiques.
Prenons une requête récupérant 50 articles avec leurs auteurs :
# Requête GraphQL
query {
articles(size: 50) {
id
title
author {
name
}
}
}Avec le resolver précédent, cette requête exécute :
- 1 requête pour les 50 articles
- 50 requêtes pour charger chaque auteur individuellement
-- Requête 1 : récupération des articles
SELECT * FROM articles LIMIT 50
-- Requêtes 2 à 51 : chaque auteur individuellement
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... 47 requêtes supplémentairesCette multiplication des requêtes dégrade drastiquement les performances. Un endpoint répondant en 50ms peut passer à 2 secondes avec le N+1.
Comment fonctionnent les DataLoaders ?
Les DataLoaders regroupent les requêtes individuelles en requêtes par lots (batching). Au lieu de charger chaque auteur séparément, le DataLoader collecte tous les IDs demandés et exécute une seule requête.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Méthode de chargement par lot
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Une seule requête pour tous les auteurs
List<Author> authors = authorRepository.findAllById(authorIds);
// Transformation en Map pour lookup rapide
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}L'enregistrement du DataLoader s'effectue via BatchLoaderRegistry dans une configuration dédiée.
@Configuration
public class GraphQLConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(
AuthorBatchLoader authorBatchLoader) {
return registry -> {
// Enregistrement du DataLoader pour les auteurs
registry.forTypePair(Long.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
authorBatchLoader.loadAuthors(authorIds));
};
}
}Le resolver modifié utilise maintenant le DataLoader au lieu d'un accès direct :
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
public ArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Resolver optimisé avec DataLoader
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> authorDataLoader) {
// Le DataLoader regroupe automatiquement les appels
return authorDataLoader.load(article.getAuthorId());
}
}Avec cette approche, la même requête pour 50 articles exécute seulement 2 requêtes SQL :
-- Requête 1 : récupération des articles
SELECT * FROM articles LIMIT 50
-- Requête 2 : tous les auteurs en une fois
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)Prêt à réussir tes entretiens Spring Boot ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Comment implémenter un DataLoader avec contexte ?
Les DataLoaders avancés nécessitent parfois un contexte additionnel, comme le tenant ID pour une application multi-tenant ou des filtres de sécurité.
@Component
public class SecuredAuthorBatchLoader {
private final AuthorRepository authorRepository;
private final SecurityContextHolder securityContext;
public SecuredAuthorBatchLoader(AuthorRepository authorRepository,
SecurityContextHolder securityContext) {
this.authorRepository = authorRepository;
this.securityContext = securityContext;
}
public Mono<Map<Long, Author>> loadAuthorsForTenant(
Set<Long> authorIds,
BatchLoaderEnvironment env) {
// Récupération du contexte GraphQL
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Requête filtrée par tenant
List<Author> authors = authorRepository
.findAllByIdInAndTenantId(authorIds, tenantId);
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}La configuration du contexte s'effectue via un intercepteur WebGraphQL :
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request,
Chain chain) {
// Extraction du tenant depuis les headers
String tenantId = request.getHeaders()
.getFirst("X-Tenant-ID");
// Ajout au contexte GraphQL
request.configureExecutionInput((input, builder) ->
builder.graphQLContext(ctx ->
ctx.put("tenantId", tenantId)
).build()
);
return chain.next(request);
}
}Quelles sont les différences entre @QueryMapping et @SchemaMapping ?
Cette question classique d'entretien vérifie la compréhension de la hiérarchie des resolvers.
| Annotation | Usage | Équivalent |
|------------|-------|------------|
| @QueryMapping | Champs racine du type Query | @SchemaMapping(typeName = "Query") |
| @MutationMapping | Champs racine du type Mutation | @SchemaMapping(typeName = "Mutation") |
| @SubscriptionMapping | Abonnements temps réel | @SchemaMapping(typeName = "Subscription") |
| @SchemaMapping | Tous les champs de n'importe quel type | Forme générique |
@Controller
public class EquivalenceDemo {
// Ces deux déclarations sont équivalentes
@QueryMapping
public Article article(@Argument Long id) {
return findArticle(id);
}
@SchemaMapping(typeName = "Query", field = "article")
public Article articleEquivalent(@Argument Long id) {
return findArticle(id);
}
// Pour les champs imbriqués, seul @SchemaMapping fonctionne
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return findAuthor(article.getAuthorId());
}
// Syntaxe alternative avec le type en paramètre
@SchemaMapping
public Author author(Article article) {
// Spring déduit typeName = "Article" depuis le paramètre
return findAuthor(article.getAuthorId());
}
}Comment gérer les mutations avec validation ?
Les mutations GraphQL modifient les données. Spring GraphQL s'intègre avec Bean Validation pour valider les entrées.
# schema.graphqls
type Mutation {
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Article!
deleteArticle(id: ID!): Boolean!
}
input CreateArticleInput {
title: String!
content: String!
authorId: ID!
}
input UpdateArticleInput {
title: String
content: String
}Le controller utilise @Valid pour déclencher la validation :
@Controller
public class ArticleMutationController {
private final ArticleService articleService;
public ArticleMutationController(ArticleService articleService) {
this.articleService = articleService;
}
@MutationMapping
public Article createArticle(@Argument @Valid CreateArticleInput input) {
// La validation s'exécute automatiquement
return articleService.create(input);
}
@MutationMapping
public Article updateArticle(@Argument Long id,
@Argument @Valid UpdateArticleInput input) {
return articleService.update(id, input);
}
@MutationMapping
public boolean deleteArticle(@Argument Long id) {
articleService.delete(id);
return true;
}
}public record CreateArticleInput(
@NotBlank(message = "Le titre est obligatoire")
@Size(min = 5, max = 200, message = "Le titre doit contenir entre 5 et 200 caractères")
String title,
@NotBlank(message = "Le contenu est obligatoire")
@Size(min = 100, message = "Le contenu doit contenir au moins 100 caractères")
String content,
@NotNull(message = "L'auteur est obligatoire")
Long authorId
) {}Les erreurs de validation retournent automatiquement une erreur GraphQL structurée avec le chemin du champ invalide. Spring GraphQL formate ces erreurs selon la spécification GraphQL.
Comment optimiser les requêtes avec @BatchMapping ?
L'annotation @BatchMapping simplifie la création de DataLoaders directement dans les controllers. Cette approche évite la configuration explicite du BatchLoaderRegistry.
@Controller
public class OptimizedArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
private final CommentRepository commentRepository;
public OptimizedArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository,
CommentRepository commentRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
this.commentRepository = commentRepository;
}
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// BatchMapping pour les auteurs - traite tous les articles en une fois
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Collecte des IDs d'auteurs uniques
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Une seule requête pour tous les auteurs
Map<Long, Author> authorsById = authorRepository
.findAllById(authorIds)
.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
// Association article -> auteur
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> authorsById.get(article.getAuthorId())
));
}
// BatchMapping pour les commentaires
@BatchMapping
public Map<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Récupération groupée des commentaires
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Groupement par article
Map<Long, List<Comment>> commentsByArticle = allComments.stream()
.collect(Collectors.groupingBy(Comment::getArticleId));
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> commentsByArticle
.getOrDefault(article.getId(), List.of())
));
}
}@BatchMapping offre une syntaxe plus concise que la configuration manuelle des DataLoaders, tout en fournissant les mêmes garanties de performance.
Comment tester les resolvers GraphQL ?
Spring GraphQL fournit GraphQlTester pour écrire des tests expressifs et lisibles.
@SpringBootTest
@AutoConfigureGraphQlTester
class ArticleControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldReturnArticleById() {
// Given : document GraphQL
String query = """
query {
article(id: 1) {
id
title
author {
name
}
}
}
""";
// When & Then
graphQlTester.document(query)
.execute()
.path("article.id").entity(String.class).isEqualTo("1")
.path("article.title").entity(String.class).isNotEmpty()
.path("article.author.name").entity(String.class).isNotEmpty();
}
@Test
void shouldCreateArticle() {
String mutation = """
mutation {
createArticle(input: {
title: "Nouveau titre",
content: "Contenu de test suffisamment long pour validation",
authorId: 1
}) {
id
title
}
}
""";
graphQlTester.document(mutation)
.execute()
.path("createArticle.id").entity(String.class).isNotEmpty()
.path("createArticle.title").entity(String.class)
.isEqualTo("Nouveau titre");
}
@Test
void shouldReturnErrorForInvalidInput() {
String mutation = """
mutation {
createArticle(input: {
title: "",
content: "court",
authorId: 1
}) {
id
}
}
""";
graphQlTester.document(mutation)
.execute()
.errors()
.expect(error -> error.getMessage()
.contains("Le titre est obligatoire"));
}
@Test
void shouldHandleVariables() {
String query = """
query GetArticle($id: ID!) {
article(id: $id) {
id
title
}
}
""";
graphQlTester.document(query)
.variable("id", 42)
.execute()
.path("article").valueIsNull();
}
}Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Comment gérer les subscriptions temps réel ?
Les subscriptions permettent de pousser des données en temps réel vers les clients via WebSocket.
# schema.graphqls
type Subscription {
articleCreated: Article!
commentAdded(articleId: ID!): Comment!
}@Controller
public class SubscriptionController {
private final Sinks.Many<Article> articleSink =
Sinks.many().multicast().onBackpressureBuffer();
private final Sinks.Many<Comment> commentSink =
Sinks.many().multicast().onBackpressureBuffer();
@SubscriptionMapping
public Flux<Article> articleCreated() {
return articleSink.asFlux();
}
@SubscriptionMapping
public Flux<Comment> commentAdded(@Argument Long articleId) {
return commentSink.asFlux()
.filter(comment -> comment.getArticleId().equals(articleId));
}
// Méthode appelée lors de la création d'un article
public void publishArticle(Article article) {
articleSink.tryEmitNext(article);
}
// Méthode appelée lors de l'ajout d'un commentaire
public void publishComment(Comment comment) {
commentSink.tryEmitNext(comment);
}
}La configuration WebSocket requiert une dépendance supplémentaire :
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}Quelles sont les bonnes pratiques de sécurité GraphQL ?
La sécurité GraphQL nécessite plusieurs couches de protection contre les attaques par déni de service et les accès non autorisés.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// Endpoint GraphQL protégé
.requestMatchers("/graphql").authenticated()
// GraphiQL accessible en dev uniquement
.requestMatchers("/graphiql").permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}@Controller
public class SecuredArticleController {
private final ArticleRepository articleRepository;
public SecuredArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
// Accès restreint aux utilisateurs authentifiés
@QueryMapping
@PreAuthorize("isAuthenticated()")
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// Seuls les admins peuvent supprimer
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public boolean deleteArticle(@Argument Long id) {
articleRepository.deleteById(id);
return true;
}
// Vérification fine : seul l'auteur peut modifier
@MutationMapping
@PreAuthorize("@articleSecurity.isAuthor(#id, authentication)")
public Article updateArticle(@Argument Long id,
@Argument UpdateArticleInput input) {
return articleRepository.findById(id)
.map(article -> {
article.setTitle(input.title());
article.setContent(input.content());
return articleRepository.save(article);
})
.orElseThrow();
}
}Protection contre les requêtes complexes :
# application.yml
spring:
graphql:
schema:
introspection:
# Désactiver l'introspection en production
enabled: false@Component
public class QueryComplexityInstrumentation extends SimplePerformantInstrumentation {
private static final int MAX_COMPLEXITY = 100;
@Override
public InstrumentationState createState(InstrumentationCreateStateParameters parameters) {
return new ComplexityState();
}
@Override
public InstrumentationContext<ExecutionResult> beginExecution(
InstrumentationExecutionParameters parameters,
InstrumentationState state) {
int complexity = calculateComplexity(parameters.getDocument());
if (complexity > MAX_COMPLEXITY) {
throw new GraphQLException(
"Query too complex: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
);
}
return super.beginExecution(parameters, state);
}
}Conclusion
Spring for GraphQL offre une intégration élégante de GraphQL dans l'écosystème Spring. La maîtrise des resolvers, des DataLoaders et de la gestion du problème N+1 constitue le socle de connaissances attendu en entretien.
Checklist entretien Spring GraphQL :
- ✅ Expliquer le rôle des annotations
@QueryMapping,@MutationMappinget@SchemaMapping - ✅ Décrire le problème N+1 et son impact sur les performances GraphQL
- ✅ Implémenter un DataLoader avec
@BatchMappingou BatchLoaderRegistry - ✅ Valider les entrées de mutation avec Bean Validation
- ✅ Sécuriser les resolvers avec
@PreAuthorizeet Spring Security - ✅ Tester les resolvers avec GraphQlTester
- ✅ Configurer les subscriptions pour le temps réel
- ✅ Protéger l'API contre les requêtes excessivement complexes
Tags
Partager
Articles similaires

Spring Data JPA en 2026 : résoudre le problème N+1 avec fetch join et EntityGraph
Guide complet pour détecter et corriger le problème N+1 en Spring Data JPA. Fetch join, @EntityGraph, batch fetching et stratégies de performance pour les requêtes.

30 questions d'entretien Spring Boot : Guide complet pour développeurs Java
Préparez vos entretiens Spring Boot avec ces 30 questions essentielles couvrant l'auto-configuration, les starters, Spring Data JPA, la sécurité et les tests.

Spring Boot logging en 2026 : logs structurés pour la production avec Logback et JSON
Guide complet des logs structurés Spring Boot. Configuration Logback JSON, MDC pour le tracing, best practices production et intégration ELK Stack.