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.

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.
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.
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.
# 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.
@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<Article> 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.
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:
# 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
-- 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 aggiuntiveQuesta 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.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Metodo di caricamento batch
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Query unica per tutti gli autori
List<Author> authors = authorRepository.findAllById(authorIds);
// Trasformazione in Map per ricerca rapida
Map<Long, Author> 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.
@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:
@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 ottimizzato con DataLoader
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> 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:
-- 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)Pronto a superare i tuoi colloqui su Spring Boot?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
@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) {
// Recuperare il contesto GraphQL
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Query filtrata per 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 configurazione del contesto avviene tramite un interceptor WebGraphQL:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> 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 |
@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.
# 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:
@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;
}
}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
) {}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.
@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 per gli autori - elabora tutti gli articoli in una volta
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Raccogliere gli ID univoci degli autori
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Query unica per tutti gli autori
Map<Long, Author> 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<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Recupero batch dei commenti
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Raggruppamento per articolo
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 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.
@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();
}
}Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Come gestire le sottoscrizioni in tempo reale?
Le sottoscrizioni permettono di inviare dati in tempo reale ai client tramite 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));
}
// 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:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@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.
@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();
}
}@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<Article> 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:
# application.yml
spring:
graphql:
schema:
introspection:
# Disattivare l'introspezione in produzione
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 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,@MutationMappinge@SchemaMapping - ✅ Descrivere il problema N+1 e il suo impatto sulle prestazioni GraphQL
- ✅ Implementare un DataLoader con
@BatchMappingo BatchLoaderRegistry - ✅ Validare gli input delle mutation con Bean Validation
- ✅ Mettere in sicurezza i resolver con
@PreAuthorizee Spring Security - ✅ Testare i resolver con GraphQlTester
- ✅ Configurare le sottoscrizioni per funzionalità in tempo reale
- ✅ Proteggere l'API contro query troppo complesse
Tag
Condividi
Articoli correlati

Colloquio Spring Boot: Propagazione delle Transazioni
Padroneggia la propagazione delle transazioni in Spring Boot: REQUIRED, REQUIRES_NEW, NESTED e altro. 12 domande di colloquio con codice e trappole comuni.

Soluzioni N+1 in Spring Data JPA nel 2026: Fetch Join e EntityGraph
Guida completa per rilevare e correggere il problema N+1 in Spring Data JPA. Fetch join, @EntityGraph, batch fetching e strategie di performance delle query.

30 Domande per Colloqui Spring Boot: Guida Completa per Sviluppatori Java
Prepara i tuoi colloqui Spring Boot con queste 30 domande essenziali su auto-configurazione, starter, Spring Data JPA, sicurezza e testing.