# Entrevista Spring GraphQL: Resolvers, DataLoaders e Soluções para o Problema N+1 > Prepare-se para entrevistas Spring GraphQL com este guia completo. Resolvers, DataLoaders, gestão do problema N+1, mutations e melhores práticas para perguntas técnicas. - 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 simplifica a integração do GraphQL em aplicações Spring Boot. Esta tecnologia tornou-se indispensável para APIs modernas, e as perguntas de entrevista sobre o assunto são cada vez mais frequentes. Este guia aborda os conceitos fundamentais: resolvers, DataLoaders, problema N+1 e padrões avançados. > **Foco da entrevista** > > Os recrutadores avaliam particularmente a compreensão do problema N+1 e o uso de DataLoaders. Esses dois temas representam 60% das perguntas em entrevistas Spring GraphQL. ## O que é Spring for GraphQL? Spring for GraphQL é o sucessor oficial do GraphQL Java Spring, integrado nativamente no framework Spring desde a versão 2.7. Esta integração traz várias vantagens: suporte às anotações Spring, integração com Spring Security e uso transparente de WebFlux ou MVC. A configuração básica requer apenas a dependência `spring-boot-starter-graphql` e um schema GraphQL. ```java // build.gradle.kts dependencies { // Spring GraphQL starter com Spring MVC implementation("org.springframework.boot:spring-boot-starter-graphql") implementation("org.springframework.boot:spring-boot-starter-web") // Para os testes GraphQL testImplementation("org.springframework.graphql:spring-graphql-test") } ``` O schema GraphQL fica em `src/main/resources/graphql/` com a extensão `.graphqls`. ```graphql # schema.graphqls type Query { # Recuperar um artigo pelo seu identificador article(id: ID!): Article # Lista paginada de artigos articles(page: Int = 0, size: Int = 10): [Article!]! } type Article { id: ID! title: String! content: String! # Relação com o autor author: Author! # Lista de comentários comments: [Comment!]! createdAt: String! } type Author { id: ID! name: String! email: String! # Artigos escritos por este autor articles: [Article!]! } type Comment { id: ID! content: String! author: Author! createdAt: String! } ``` Esta arquitetura declara os tipos e suas relações. O GraphQL gera automaticamente a documentação e valida as queries do lado do servidor. ## Como funcionam os Resolvers Spring GraphQL? Os resolvers constituem o coração da execução GraphQL. Cada campo do schema pode ter um resolver dedicado. Spring usa a anotação `@QueryMapping` para as queries raiz e `@SchemaMapping` para as relações. ```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 para Query.article(id) @QueryMapping public Article article(@Argument Long id) { return articleRepository.findById(id) .orElseThrow(() -> new ArticleNotFoundException(id)); } // Resolver para 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 para Article.author - chamado para cada artigo @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return authorRepository.findById(article.getAuthorId()) .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId())); } } ``` O resolver `author` é executado para cada artigo retornado. Esta arquitetura flexível carrega os dados sob demanda mas introduz o problema N+1. > **Armadilha clássica de entrevista** > > Um candidato que implementa um resolver `@SchemaMapping` sem mencionar o problema N+1 demonstra uma compreensão incompleta do GraphQL. Os recrutadores esperam sistematicamente esta análise. ## O que é o Problema N+1 no GraphQL? O problema N+1 surge quando uma query GraphQL desencadeia N queries adicionais para carregar as relações. Este padrão destrutivo ocorre sistematicamente com resolvers básicos. Considere uma query que recupera 50 artigos com seus autores: ```graphql # Query GraphQL query { articles(size: 50) { id title author { name } } } ``` Com o resolver anterior, esta query executa: - 1 query para os 50 artigos - 50 queries para carregar cada autor individualmente ```sql -- Query 1: recuperação de artigos SELECT * FROM articles LIMIT 50 -- Queries 2-51: cada autor individualmente SELECT * FROM authors WHERE id = 1 SELECT * FROM authors WHERE id = 2 SELECT * FROM authors WHERE id = 3 -- ... 47 queries adicionais ``` Esta multiplicação de queries degrada drasticamente o desempenho. Um endpoint que responde em 50ms pode saltar para 2 segundos com N+1. ## Como funcionam os DataLoaders? Os DataLoaders agrupam queries individuais em requisições em lote. Em vez de carregar cada autor separadamente, o DataLoader coleta todos os IDs solicitados e executa uma única query. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Método de carregamento em lote public Mono> loadAuthors(Set authorIds) { // Query única para todos os autores List authors = authorRepository.findAllById(authorIds); // Transformação em Map para busca rápida Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` O registro do DataLoader é feito via `BatchLoaderRegistry` em uma configuração dedicada. ```java // GraphQLConfig.java @Configuration public class GraphQLConfig { @Bean public BatchLoaderRegistry batchLoaderRegistry( AuthorBatchLoader authorBatchLoader) { return registry -> { // Registro do DataLoader para os autores registry.forTypePair(Long.class, Author.class) .registerMappedBatchLoader((authorIds, env) -> authorBatchLoader.loadAuthors(authorIds)); }; } } ``` O resolver modificado agora utiliza o DataLoader em vez de acesso direto: ```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 otimizado com DataLoader @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader authorDataLoader) { // O DataLoader agrupa automaticamente as chamadas return authorDataLoader.load(article.getAuthorId()); } } ``` Com esta abordagem, a mesma query para 50 artigos executa apenas 2 queries SQL: ```sql -- Query 1: recuperação de artigos SELECT * FROM articles LIMIT 50 -- Query 2: todos os autores de uma vez SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50) ``` ## Como implementar um DataLoader com contexto? DataLoaders avançados às vezes requerem contexto adicional, como um identificador de tenant para aplicações multi-tenant ou filtros de segurança. ```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) { // Recuperar o contexto GraphQL GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Query filtrada por tenant List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` A configuração do contexto é feita por meio de um interceptor WebGraphQL: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono intercept( WebGraphQlRequest request, Chain chain) { // Extrair o tenant dos headers String tenantId = request.getHeaders() .getFirst("X-Tenant-ID"); // Adicionar ao contexto GraphQL request.configureExecutionInput((input, builder) -> builder.graphQLContext(ctx -> ctx.put("tenantId", tenantId) ).build() ); return chain.next(request); } } ``` ## Quais são as diferenças entre @QueryMapping e @SchemaMapping? Esta pergunta clássica de entrevista verifica a compreensão da hierarquia de resolvers. | Anotação | Uso | Equivalente | |----------|-----|-------------| | `@QueryMapping` | Campos raiz do tipo Query | `@SchemaMapping(typeName = "Query")` | | `@MutationMapping` | Campos raiz do tipo Mutation | `@SchemaMapping(typeName = "Mutation")` | | `@SubscriptionMapping` | Subscriptions em tempo real | `@SchemaMapping(typeName = "Subscription")` | | `@SchemaMapping` | Todos os campos de qualquer tipo | Forma genérica | ```java // EquivalenceDemo.java @Controller public class EquivalenceDemo { // Estas duas declarações são equivalentes @QueryMapping public Article article(@Argument Long id) { return findArticle(id); } @SchemaMapping(typeName = "Query", field = "article") public Article articleEquivalent(@Argument Long id) { return findArticle(id); } // Para campos aninhados, apenas @SchemaMapping funciona @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return findAuthor(article.getAuthorId()); } // Sintaxe alternativa com o tipo como parâmetro @SchemaMapping public Author author(Article article) { // Spring infere typeName = "Article" do parâmetro return findAuthor(article.getAuthorId()); } } ``` ## Como gerenciar mutations com validação? As mutations GraphQL modificam os dados. Spring GraphQL integra-se com Bean Validation para validar as entradas. ```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 } ``` O controller usa `@Valid` para acionar a validação: ```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) { // A validação é executada 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 = "O título é obrigatório") @Size(min = 5, max = 200, message = "O título deve ter entre 5 e 200 caracteres") String title, @NotBlank(message = "O conteúdo é obrigatório") @Size(min = 100, message = "O conteúdo deve ter pelo menos 100 caracteres") String content, @NotNull(message = "O autor é obrigatório") Long authorId ) {} ``` > **Tratamento de erros** > > Os erros de validação retornam automaticamente um erro GraphQL estruturado com o caminho do campo inválido. Spring GraphQL formata esses erros de acordo com a especificação GraphQL. ## Como otimizar queries com @BatchMapping? A anotação `@BatchMapping` simplifica a criação de DataLoaders diretamente nos controllers. Esta abordagem evita a configuração explícita do 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 para os autores - processa todos os artigos de uma vez @BatchMapping public Map author(List
articles) { // Coletar os IDs únicos de autores Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Query única para todos os autores Map authorsById = authorRepository .findAllById(authorIds) .stream() .collect(Collectors.toMap(Author::getId, Function.identity())); // Associação artigo -> autor return articles.stream() .collect(Collectors.toMap( Function.identity(), article -> authorsById.get(article.getAuthorId()) )); } // BatchMapping para os comentários @BatchMapping public Map> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Recuperação em lote dos comentários List allComments = commentRepository .findByArticleIdIn(articleIds); // Agrupamento por artigo 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` oferece uma sintaxe mais concisa do que a configuração manual de DataLoaders, mantendo as mesmas garantias de desempenho. ## Como testar Resolvers GraphQL? Spring GraphQL fornece `GraphQlTester` para escrever testes expressivos e legíveis. ```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: "Novo título", content: "Conteúdo de teste suficientemente longo para validar as restrições", authorId: 1 }) { id title } } """; graphQlTester.document(mutation) .execute() .path("createArticle.id").entity(String.class).isNotEmpty() .path("createArticle.title").entity(String.class) .isEqualTo("Novo título"); } @Test void shouldReturnErrorForInvalidInput() { String mutation = """ mutation { createArticle(input: { title: "", content: "curto", authorId: 1 }) { id } } """; graphQlTester.document(mutation) .execute() .errors() .expect(error -> error.getMessage() .contains("O título é obrigatório")); } @Test void shouldHandleVariables() { String query = """ query GetArticle($id: ID!) { article(id: $id) { id title } } """; graphQlTester.document(query) .variable("id", 42) .execute() .path("article").valueIsNull(); } } ``` ## Como gerenciar subscriptions em tempo real? As subscriptions permitem enviar dados em tempo real aos clientes 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étodo chamado ao criar um artigo public void publishArticle(Article article) { articleSink.tryEmitNext(article); } // Método chamado ao adicionar um comentário public void publishComment(Comment comment) { commentSink.tryEmitNext(comment); } } ``` A configuração WebSocket requer uma dependência adicional: ```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(); } } ``` ## Quais são as melhores práticas de segurança GraphQL? A segurança GraphQL requer várias camadas de proteção contra ataques de negação de serviço e acessos não autorizados. ```java // SecurityConfig.java @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Endpoint GraphQL protegido .requestMatchers("/graphql").authenticated() // GraphiQL acessível apenas em 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; } // Acesso restrito a usuários autenticados @QueryMapping @PreAuthorize("isAuthenticated()") public List
articles(@Argument int page, @Argument int size) { return articleRepository.findAll(PageRequest.of(page, size)) .getContent(); } // Apenas administradores podem excluir @MutationMapping @PreAuthorize("hasRole('ADMIN')") public boolean deleteArticle(@Argument Long id) { articleRepository.deleteById(id); return true; } // Verificação granular: apenas o autor pode modificar @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(); } } ``` Proteção contra queries complexas: ```yaml # application.yml spring: graphql: schema: introspection: # Desativar a introspecção em produção 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 muito complexa: " + complexity + " (max: " + MAX_COMPLEXITY + ")" ); } return super.beginExecution(parameters, state); } } ``` ## Conclusão Spring for GraphQL oferece uma integração elegante do GraphQL no ecossistema Spring. O domínio dos resolvers, dos DataLoaders e da gestão do problema N+1 constitui a base de conhecimentos esperada em entrevistas. **Checklist de entrevista Spring GraphQL:** - ✅ Explicar o papel das anotações `@QueryMapping`, `@MutationMapping` e `@SchemaMapping` - ✅ Descrever o problema N+1 e seu impacto no desempenho GraphQL - ✅ Implementar um DataLoader com `@BatchMapping` ou BatchLoaderRegistry - ✅ Validar as entradas de mutation com Bean Validation - ✅ Proteger os resolvers com `@PreAuthorize` e Spring Security - ✅ Testar os resolvers com GraphQlTester - ✅ Configurar subscriptions para funcionalidades em tempo real - ✅ Proteger a API contra queries muito complexas --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1