# Colloquio Spring GraphQL: Resolver, DataLoader e Soluzioni al Problema N+1 > Preparazione ai colloqui Spring GraphQL con questa guida completa. Resolver, DataLoader, gestione del problema N+1, mutation e migliori pratiche per le domande tecniche. - Published: 2026-03-25 - Updated: 2026-05-04 - Author: SharpSkill - Tags: spring graphql, dataloaders, n+1 problem, interview, graphql java - Reading time: 14 min --- Spring for GraphQL semplifica l'integrazione di GraphQL nelle applicazioni Spring Boot. Questa tecnologia è diventata indispensabile per le API moderne e le domande di colloquio su questo argomento sono sempre più frequenti. Questa guida copre i concetti fondamentali: resolver, DataLoader, problema N+1 e pattern avanzati. > **Focus del colloquio** > > I selezionatori valutano in particolare la comprensione del problema N+1 e l'uso dei DataLoader. Questi due argomenti rappresentano il 60% delle domande nei colloqui Spring GraphQL. ## Cos'è Spring for GraphQL? Spring for GraphQL è il successore ufficiale di GraphQL Java Spring, integrato nativamente nel framework Spring dalla versione 2.7. Questa integrazione apporta diversi vantaggi: supporto delle annotazioni Spring, integrazione con Spring Security e uso trasparente di WebFlux o MVC. La configurazione di base richiede solo la dipendenza `spring-boot-starter-graphql` e uno schema GraphQL. ```java // build.gradle.kts dependencies { // Spring GraphQL starter con Spring MVC implementation("org.springframework.boot:spring-boot-starter-graphql") implementation("org.springframework.boot:spring-boot-starter-web") // Per i test GraphQL testImplementation("org.springframework.graphql:spring-graphql-test") } ``` Lo schema GraphQL si trova in `src/main/resources/graphql/` con l'estensione `.graphqls`. ```graphql # schema.graphqls type Query { # Recuperare un articolo dal suo identificatore article(id: ID!): Article # Lista paginata di articoli articles(page: Int = 0, size: Int = 10): [Article!]! } type Article { id: ID! title: String! content: String! # Relazione con l'autore author: Author! # Lista di commenti comments: [Comment!]! createdAt: String! } type Author { id: ID! name: String! email: String! # Articoli scritti da questo autore articles: [Article!]! } type Comment { id: ID! content: String! author: Author! createdAt: String! } ``` Questa architettura dichiara i tipi e le loro relazioni. GraphQL genera automaticamente la documentazione e valida le query lato server. ## Come funzionano i Resolver Spring GraphQL? I resolver costituiscono il cuore dell'esecuzione GraphQL. Ogni campo dello schema può avere un resolver dedicato. Spring utilizza l'annotazione `@QueryMapping` per le query root e `@SchemaMapping` per le relazioni. ```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 per Query.article(id) @QueryMapping public Article article(@Argument Long id) { return articleRepository.findById(id) .orElseThrow(() -> new ArticleNotFoundException(id)); } // Resolver per 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 per Article.author - chiamato per ogni articolo @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return authorRepository.findById(article.getAuthorId()) .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId())); } } ``` Il resolver `author` viene eseguito per ogni articolo restituito. Questa architettura flessibile carica i dati su richiesta ma introduce il problema N+1. > **Trappola classica del colloquio** > > Un candidato che implementa un resolver `@SchemaMapping` senza menzionare il problema N+1 dimostra una comprensione incompleta di GraphQL. I selezionatori si aspettano sistematicamente questa analisi. ## Cos'è il Problema N+1 in GraphQL? Il problema N+1 si verifica quando una query GraphQL innesca N query aggiuntive per caricare le relazioni. Questo pattern distruttivo si manifesta sistematicamente con resolver di base. Si consideri una query che recupera 50 articoli con i loro autori: ```graphql # Query GraphQL query { articles(size: 50) { id title author { name } } } ``` Con il resolver precedente, questa query esegue: - 1 query per i 50 articoli - 50 query per caricare ogni autore individualmente ```sql -- Query 1: recupero degli articoli SELECT * FROM articles LIMIT 50 -- Query 2-51: ogni autore individualmente SELECT * FROM authors WHERE id = 1 SELECT * FROM authors WHERE id = 2 SELECT * FROM authors WHERE id = 3 -- ... 47 query aggiuntive ``` Questa moltiplicazione di query degrada drasticamente le prestazioni. Un endpoint che risponde in 50ms può salire a 2 secondi con N+1. ## Come funzionano i DataLoader? I DataLoader raggruppano le query individuali in richieste batch. Invece di caricare ogni autore separatamente, il DataLoader raccoglie tutti gli ID richiesti ed esegue una singola query. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Metodo di caricamento batch public Mono> loadAuthors(Set authorIds) { // Query unica per tutti gli autori List authors = authorRepository.findAllById(authorIds); // Trasformazione in Map per ricerca rapida Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` La registrazione del DataLoader avviene tramite `BatchLoaderRegistry` in una configurazione dedicata. ```java // GraphQLConfig.java @Configuration public class GraphQLConfig { @Bean public BatchLoaderRegistry batchLoaderRegistry( AuthorBatchLoader authorBatchLoader) { return registry -> { // Registrazione del DataLoader per gli autori registry.forTypePair(Long.class, Author.class) .registerMappedBatchLoader((authorIds, env) -> authorBatchLoader.loadAuthors(authorIds)); }; } } ``` Il resolver modificato utilizza ora il DataLoader invece dell'accesso diretto: ```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 ottimizzato con DataLoader @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader authorDataLoader) { // Il DataLoader raggruppa automaticamente le chiamate return authorDataLoader.load(article.getAuthorId()); } } ``` Con questo approccio, la stessa query per 50 articoli esegue solo 2 query SQL: ```sql -- Query 1: recupero degli articoli SELECT * FROM articles LIMIT 50 -- Query 2: tutti gli autori in una volta SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50) ``` ## Come implementare un DataLoader con contesto? I DataLoader avanzati richiedono talvolta un contesto aggiuntivo, come un identificatore di tenant per applicazioni multi-tenant o filtri di sicurezza. ```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) { // Recuperare il contesto GraphQL GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Query filtrata per tenant List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` La configurazione del contesto avviene tramite un interceptor WebGraphQL: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono intercept( WebGraphQlRequest request, Chain chain) { // Estrarre il tenant dagli header String tenantId = request.getHeaders() .getFirst("X-Tenant-ID"); // Aggiungere al contesto GraphQL request.configureExecutionInput((input, builder) -> builder.graphQLContext(ctx -> ctx.put("tenantId", tenantId) ).build() ); return chain.next(request); } } ``` ## Quali sono le differenze tra @QueryMapping e @SchemaMapping? Questa domanda classica del colloquio verifica la comprensione della gerarchia dei resolver. | Annotazione | Utilizzo | Equivalente | |-------------|----------|-------------| | `@QueryMapping` | Campi root del tipo Query | `@SchemaMapping(typeName = "Query")` | | `@MutationMapping` | Campi root del tipo Mutation | `@SchemaMapping(typeName = "Mutation")` | | `@SubscriptionMapping` | Sottoscrizioni in tempo reale | `@SchemaMapping(typeName = "Subscription")` | | `@SchemaMapping` | Tutti i campi di qualsiasi tipo | Forma generica | ```java // EquivalenceDemo.java @Controller public class EquivalenceDemo { // Queste due dichiarazioni sono equivalenti @QueryMapping public Article article(@Argument Long id) { return findArticle(id); } @SchemaMapping(typeName = "Query", field = "article") public Article articleEquivalent(@Argument Long id) { return findArticle(id); } // Per i campi annidati, solo @SchemaMapping funziona @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return findAuthor(article.getAuthorId()); } // Sintassi alternativa con il tipo come parametro @SchemaMapping public Author author(Article article) { // Spring deduce typeName = "Article" dal parametro return findAuthor(article.getAuthorId()); } } ``` ## Come gestire le mutation con validazione? Le mutation GraphQL modificano i dati. Spring GraphQL si integra con Bean Validation per validare gli input. ```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 } ``` Il controller usa `@Valid` per attivare la validazione: ```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 validazione viene eseguita automaticamente 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 = "Il titolo è obbligatorio") @Size(min = 5, max = 200, message = "Il titolo deve avere tra 5 e 200 caratteri") String title, @NotBlank(message = "Il contenuto è obbligatorio") @Size(min = 100, message = "Il contenuto deve avere almeno 100 caratteri") String content, @NotNull(message = "L'autore è obbligatorio") Long authorId ) {} ``` > **Gestione degli errori** > > Gli errori di validazione restituiscono automaticamente un errore GraphQL strutturato con il percorso del campo non valido. Spring GraphQL formatta questi errori secondo la specifica GraphQL. ## Come ottimizzare le query con @BatchMapping? L'annotazione `@BatchMapping` semplifica la creazione di DataLoader direttamente nei controller. Questo approccio evita la configurazione esplicita del 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 per gli autori - elabora tutti gli articoli in una volta @BatchMapping public Map author(List
articles) { // Raccogliere gli ID univoci degli autori Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Query unica per tutti gli autori Map authorsById = authorRepository .findAllById(authorIds) .stream() .collect(Collectors.toMap(Author::getId, Function.identity())); // Associazione articolo -> autore return articles.stream() .collect(Collectors.toMap( Function.identity(), article -> authorsById.get(article.getAuthorId()) )); } // BatchMapping per i commenti @BatchMapping public Map> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Recupero batch dei commenti List allComments = commentRepository .findByArticleIdIn(articleIds); // Raggruppamento per articolo 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 una sintassi più concisa rispetto alla configurazione manuale dei DataLoader, mantenendo le stesse garanzie di prestazioni. ## Come testare i Resolver GraphQL? Spring GraphQL fornisce `GraphQlTester` per scrivere test espressivi e leggibili. ```java // ArticleControllerTest.java @SpringBootTest @AutoConfigureGraphQlTester class ArticleControllerTest { @Autowired private GraphQlTester graphQlTester; @Test void shouldReturnArticleById() { // Given: documento 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: "Nuovo titolo", content: "Contenuto di test sufficientemente lungo per validare i vincoli", authorId: 1 }) { id title } } """; graphQlTester.document(mutation) .execute() .path("createArticle.id").entity(String.class).isNotEmpty() .path("createArticle.title").entity(String.class) .isEqualTo("Nuovo titolo"); } @Test void shouldReturnErrorForInvalidInput() { String mutation = """ mutation { createArticle(input: { title: "", content: "corto", authorId: 1 }) { id } } """; graphQlTester.document(mutation) .execute() .errors() .expect(error -> error.getMessage() .contains("Il titolo è obbligatorio")); } @Test void shouldHandleVariables() { String query = """ query GetArticle($id: ID!) { article(id: $id) { id title } } """; graphQlTester.document(query) .variable("id", 42) .execute() .path("article").valueIsNull(); } } ``` ## Come gestire le sottoscrizioni in tempo reale? Le sottoscrizioni permettono di inviare dati in tempo reale ai client tramite 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)); } // Metodo chiamato alla creazione di un articolo public void publishArticle(Article article) { articleSink.tryEmitNext(article); } // Metodo chiamato all'aggiunta di un commento public void publishComment(Comment comment) { commentSink.tryEmitNext(comment); } } ``` La configurazione WebSocket richiede una dipendenza aggiuntiva: ```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(); } } ``` ## Quali sono le migliori pratiche di sicurezza GraphQL? La sicurezza GraphQL richiede più livelli di protezione contro attacchi denial-of-service e accessi non autorizzati. ```java // SecurityConfig.java @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Endpoint GraphQL protetto .requestMatchers("/graphql").authenticated() // GraphiQL accessibile solo in dev .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; } // Accesso ristretto agli utenti autenticati @QueryMapping @PreAuthorize("isAuthenticated()") public List
articles(@Argument int page, @Argument int size) { return articleRepository.findAll(PageRequest.of(page, size)) .getContent(); } // Solo gli amministratori possono eliminare @MutationMapping @PreAuthorize("hasRole('ADMIN')") public boolean deleteArticle(@Argument Long id) { articleRepository.deleteById(id); return true; } // Verifica granulare: solo l'autore può modificare @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(); } } ``` Protezione contro query complesse: ```yaml # application.yml spring: graphql: schema: introspection: # Disattivare l'introspezione in produzione 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 troppo complessa: " + complexity + " (max: " + MAX_COMPLEXITY + ")" ); } return super.beginExecution(parameters, state); } } ``` ## Conclusione Spring for GraphQL offre un'integrazione elegante di GraphQL nell'ecosistema Spring. La padronanza dei resolver, dei DataLoader e della gestione del problema N+1 costituisce la base di conoscenze attesa nei colloqui. **Checklist colloquio Spring GraphQL:** - ✅ Spiegare il ruolo delle annotazioni `@QueryMapping`, `@MutationMapping` e `@SchemaMapping` - ✅ Descrivere il problema N+1 e il suo impatto sulle prestazioni GraphQL - ✅ Implementare un DataLoader con `@BatchMapping` o BatchLoaderRegistry - ✅ Validare gli input delle mutation con Bean Validation - ✅ Mettere in sicurezza i resolver con `@PreAuthorize` e Spring Security - ✅ Testare i resolver con GraphQlTester - ✅ Configurare le sottoscrizioni per funzionalità in tempo reale - ✅ Proteggere l'API contro query troppo complesse --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1