# 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. - Published: 2026-03-25 - Updated: 2026-03-31 - Author: SharpSkill - Tags: spring graphql, dataloaders, n+1 problem, interview, graphql java - Reading time: 14 min --- 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. > **Focus entretien** > > 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. ```java // build.gradle.kts 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`. ```graphql # 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. ```java // ArticleController.java @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
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. > **Piège classique en entretien** > > 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 : ```graphql # 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 ```sql -- 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émentaires ``` Cette 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. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Méthode de chargement par lot public Mono> loadAuthors(Set authorIds) { // Une seule requête pour tous les auteurs List authors = authorRepository.findAllById(authorIds); // Transformation en Map pour lookup rapide Map 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. ```java // GraphQLConfig.java @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 : ```java // ArticleController.java @Controller public class ArticleController { private final ArticleRepository articleRepository; public ArticleController(ArticleRepository articleRepository) { this.articleRepository = articleRepository; } @QueryMapping public List
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( Article article, DataLoader 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 : ```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) ``` ## 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é. ```java // SecuredAuthorBatchLoader.java @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> loadAuthorsForTenant( Set 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 authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` La configuration du contexte s'effectue via un intercepteur WebGraphQL : ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono 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 | ```java // EquivalenceDemo.java @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. ```graphql # 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 : ```java // ArticleMutationController.java @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; } } ``` ```java // CreateArticleInput.java 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 ) {} ``` > **Gestion des erreurs** > > 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. ```java // OptimizedArticleController.java @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
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 author(List
articles) { // Collecte des IDs d'auteurs uniques Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Une seule requête pour tous les auteurs Map 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> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Récupération groupée des commentaires List allComments = commentRepository .findByArticleIdIn(articleIds); // Groupement par article Map> 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. ```java // ArticleControllerTest.java @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(); } } ``` ## 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. ```graphql # schema.graphqls type Subscription { articleCreated: Article! commentAdded(articleId: ID!): Comment! } ``` ```java // SubscriptionController.java @Controller public class SubscriptionController { private final Sinks.Many
articleSink = Sinks.many().multicast().onBackpressureBuffer(); private final Sinks.Many commentSink = Sinks.many().multicast().onBackpressureBuffer(); @SubscriptionMapping public Flux
articleCreated() { return articleSink.asFlux(); } @SubscriptionMapping public Flux 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 : ```java // build.gradle.kts dependencies { implementation("org.springframework.boot:spring-boot-starter-websocket") } ``` ```java // WebSocketConfig.java @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. ```java // SecurityConfig.java @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(); } } ``` ```java // SecuredArticleController.java @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
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 : ```yaml # application.yml spring: graphql: schema: introspection: # Désactiver l'introspection en production enabled: false ``` ```java // QueryComplexityInstrumentation.java @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 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`, `@MutationMapping` et `@SchemaMapping` - ✅ Décrire le problème N+1 et son impact sur les performances GraphQL - ✅ Implémenter un DataLoader avec `@BatchMapping` ou BatchLoaderRegistry - ✅ Valider les entrées de mutation avec Bean Validation - ✅ Sécuriser les resolvers avec `@PreAuthorize` et 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1