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.

Entrevista técnica Spring GraphQL com resolvers e DataLoaders

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.

build.gradle.ktsjava
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.

ArticleController.javajava
@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<Article> 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.

AuthorBatchLoader.javajava
@Component
public class AuthorBatchLoader {

    private final AuthorRepository authorRepository;

    public AuthorBatchLoader(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    // Método de carregamento em lote
    public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
        // Query única para todos os autores
        List<Author> authors = authorRepository.findAllById(authorIds);

        // Transformação em Map para busca rápida
        Map<Long, Author> 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.

GraphQLConfig.javajava
@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:

ArticleController.javajava
@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 otimizado com DataLoader
    @SchemaMapping(typeName = "Article", field = "author")
    public CompletableFuture<Author> author(
            Article article,
            DataLoader<Long, Author> 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)

Pronto para mandar bem nas entrevistas de Spring Boot?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

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.

SecuredAuthorBatchLoader.javajava
@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) {

        // Recuperar o contexto GraphQL
        GraphQLContext context = env.getContext();
        String tenantId = context.get("tenantId");

        // Query filtrada por 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);
    }
}

A configuração do contexto é feita por meio de um interceptor WebGraphQL:

TenantInterceptor.javajava
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> 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 |

EquivalenceDemo.javajava
@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:

ArticleMutationController.javajava
@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;
    }
}
CreateArticleInput.javajava
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.

OptimizedArticleController.javajava
@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 para os autores - processa todos os artigos de uma vez
    @BatchMapping
    public Map<Article, Author> author(List<Article> articles) {
        // Coletar os IDs únicos de autores
        Set<Long> authorIds = articles.stream()
            .map(Article::getAuthorId)
            .collect(Collectors.toSet());

        // Query única para todos os autores
        Map<Long, Author> 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<Article, List<Comment>> comments(List<Article> articles) {
        List<Long> articleIds = articles.stream()
            .map(Article::getId)
            .toList();

        // Recuperação em lote dos comentários
        List<Comment> allComments = commentRepository
            .findByArticleIdIn(articleIds);

        // Agrupamento por artigo
        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 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.

ArticleControllerTest.javajava
@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();
    }
}

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

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!
}
SubscriptionController.javajava
@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é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:

build.gradle.ktsjava
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-websocket")
}
WebSocketConfig.javajava
@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.

SecurityConfig.javajava
@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();
    }
}
SecuredArticleController.javajava
@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<Article> 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
QueryComplexityInstrumentation.javajava
@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 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

Tags

#spring graphql
#dataloaders
#n+1 problem
#interview
#graphql java

Compartilhar

Artigos relacionados