Spring GraphQL Interview: Resolver, DataLoader und Lösungen für das N+1-Problem

Vorbereitung auf Spring GraphQL Interviews mit diesem vollständigen Leitfaden. Resolver, DataLoader, Umgang mit dem N+1-Problem, Mutationen und Best Practices für technische Fragen.

Technisches Spring GraphQL Interview mit Resolvern und DataLoadern

Spring for GraphQL vereinfacht die GraphQL-Integration in Spring Boot Anwendungen. Diese Technologie ist für moderne APIs unverzichtbar geworden, und Interviewfragen zu diesem Thema werden immer häufiger gestellt. Dieser Leitfaden behandelt die zentralen Konzepte: Resolver, DataLoader, das N+1-Problem und fortgeschrittene Patterns.

Interview-Schwerpunkt

Recruiter prüfen besonders das Verständnis des N+1-Problems und die Verwendung von DataLoadern. Diese beiden Themen machen 60% der Fragen in Spring GraphQL Interviews aus.

Was ist Spring for GraphQL?

Spring for GraphQL ist der offizielle Nachfolger von GraphQL Java Spring und seit Version 2.7 nativ in das Spring Framework integriert. Diese Integration bringt mehrere Vorteile: Unterstützung der Spring-Annotationen, Integration mit Spring Security und transparente Verwendung von WebFlux oder MVC.

Die Basiskonfiguration erfordert lediglich die Abhängigkeit spring-boot-starter-graphql und ein GraphQL-Schema.

build.gradle.ktsjava
dependencies {
    // Spring GraphQL Starter mit Spring MVC
    implementation("org.springframework.boot:spring-boot-starter-graphql")
    implementation("org.springframework.boot:spring-boot-starter-web")

    // Für GraphQL-Tests
    testImplementation("org.springframework.graphql:spring-graphql-test")
}

Das GraphQL-Schema befindet sich in src/main/resources/graphql/ mit der Endung .graphqls.

graphql
# schema.graphqls
type Query {
    # Artikel anhand seiner Kennung abrufen
    article(id: ID!): Article

    # Paginierte Artikelliste
    articles(page: Int = 0, size: Int = 10): [Article!]!
}

type Article {
    id: ID!
    title: String!
    content: String!
    # Beziehung zum Autor
    author: Author!
    # Liste der Kommentare
    comments: [Comment!]!
    createdAt: String!
}

type Author {
    id: ID!
    name: String!
    email: String!
    # Vom Autor verfasste Artikel
    articles: [Article!]!
}

type Comment {
    id: ID!
    content: String!
    author: Author!
    createdAt: String!
}

Diese Architektur deklariert Typen und ihre Beziehungen. GraphQL generiert automatisch die Dokumentation und validiert die Abfragen serverseitig.

Wie funktionieren Spring GraphQL Resolver?

Resolver bilden den Kern der GraphQL-Ausführung. Jedes Schema-Feld kann einen dedizierten Resolver haben. Spring verwendet die Annotation @QueryMapping für Root-Queries und @SchemaMapping für Beziehungen.

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 für Query.article(id)
    @QueryMapping
    public Article article(@Argument Long id) {
        return articleRepository.findById(id)
            .orElseThrow(() -> new ArticleNotFoundException(id));
    }

    // Resolver für 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 für Article.author - wird für jeden Artikel aufgerufen
    @SchemaMapping(typeName = "Article", field = "author")
    public Author author(Article article) {
        return authorRepository.findById(article.getAuthorId())
            .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
    }
}

Der author-Resolver wird für jeden zurückgegebenen Artikel ausgeführt. Diese flexible Architektur lädt Daten bei Bedarf, führt jedoch zum N+1-Problem.

Klassische Interview-Falle

Ein Kandidat, der einen @SchemaMapping-Resolver implementiert, ohne das N+1-Problem zu erwähnen, zeigt unvollständiges GraphQL-Verständnis. Recruiter erwarten diese Analyse systematisch.

Was ist das N+1-Problem in GraphQL?

Das N+1-Problem entsteht, wenn eine GraphQL-Abfrage N zusätzliche Abfragen zum Laden der Beziehungen auslöst. Dieses destruktive Muster tritt systematisch bei einfachen Resolvern auf.

Betrachten wir eine Abfrage, die 50 Artikel mit ihren Autoren abruft:

graphql
# GraphQL-Abfrage
query {
    articles(size: 50) {
        id
        title
        author {
            name
        }
    }
}

Mit dem vorherigen Resolver führt diese Abfrage Folgendes aus:

  • 1 Abfrage für die 50 Artikel
  • 50 Abfragen, um jeden Autor einzeln zu laden
sql
-- Abfrage 1: Artikel abrufen
SELECT * FROM articles LIMIT 50

-- Abfragen 2-51: jeder Autor einzeln
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... 47 weitere Abfragen

Diese Vervielfachung der Abfragen verschlechtert die Performance drastisch. Ein Endpoint, der in 50ms antwortet, kann mit N+1 auf 2 Sekunden steigen.

Wie funktionieren DataLoader?

DataLoader bündeln einzelne Abfragen zu Batch-Anfragen. Statt jeden Autor separat zu laden, sammelt der DataLoader alle angeforderten IDs und führt eine einzige Abfrage aus.

AuthorBatchLoader.javajava
@Component
public class AuthorBatchLoader {

    private final AuthorRepository authorRepository;

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

    // Batch-Lademethode
    public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
        // Einzige Abfrage für alle Autoren
        List<Author> authors = authorRepository.findAllById(authorIds);

        // Umwandlung in Map für schnelle Suche
        Map<Long, Author> authorMap = authors.stream()
            .collect(Collectors.toMap(Author::getId, Function.identity()));

        return Mono.just(authorMap);
    }
}

Die DataLoader-Registrierung erfolgt über BatchLoaderRegistry in einer dedizierten Konfiguration.

GraphQLConfig.javajava
@Configuration
public class GraphQLConfig {

    @Bean
    public BatchLoaderRegistry batchLoaderRegistry(
            AuthorBatchLoader authorBatchLoader) {

        return registry -> {
            // Registrierung des DataLoaders für Autoren
            registry.forTypePair(Long.class, Author.class)
                .registerMappedBatchLoader((authorIds, env) ->
                    authorBatchLoader.loadAuthors(authorIds));
        };
    }
}

Der modifizierte Resolver verwendet nun den DataLoader statt direkten Zugriff:

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();
    }

    // Optimierter Resolver mit DataLoader
    @SchemaMapping(typeName = "Article", field = "author")
    public CompletableFuture<Author> author(
            Article article,
            DataLoader<Long, Author> authorDataLoader) {

        // Der DataLoader bündelt automatisch die Aufrufe
        return authorDataLoader.load(article.getAuthorId());
    }
}

Mit diesem Ansatz führt dieselbe Abfrage für 50 Artikel nur 2 SQL-Abfragen aus:

sql
-- Abfrage 1: Artikel abrufen
SELECT * FROM articles LIMIT 50

-- Abfrage 2: alle Autoren auf einmal
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Wie implementiert man einen DataLoader mit Kontext?

Fortgeschrittene DataLoader benötigen manchmal zusätzlichen Kontext, etwa eine Tenant-Kennung für Multi-Tenant-Anwendungen oder Sicherheitsfilter.

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) {

        // GraphQL-Kontext abrufen
        GraphQLContext context = env.getContext();
        String tenantId = context.get("tenantId");

        // Nach Tenant gefilterte Abfrage
        List<Author> authors = authorRepository
            .findAllByIdInAndTenantId(authorIds, tenantId);

        Map<Long, Author> authorMap = authors.stream()
            .collect(Collectors.toMap(Author::getId, Function.identity()));

        return Mono.just(authorMap);
    }
}

Die Kontextkonfiguration erfolgt über einen WebGraphQL-Interceptor:

TenantInterceptor.javajava
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(
            WebGraphQlRequest request,
            Chain chain) {

        // Tenant aus den Headern extrahieren
        String tenantId = request.getHeaders()
            .getFirst("X-Tenant-ID");

        // Zum GraphQL-Kontext hinzufügen
        request.configureExecutionInput((input, builder) ->
            builder.graphQLContext(ctx ->
                ctx.put("tenantId", tenantId)
            ).build()
        );

        return chain.next(request);
    }
}

Was sind die Unterschiede zwischen @QueryMapping und @SchemaMapping?

Diese klassische Interviewfrage prüft das Verständnis der Resolver-Hierarchie.

| Annotation | Verwendung | Äquivalent | |------------|-----------|------------| | @QueryMapping | Root-Felder vom Typ Query | @SchemaMapping(typeName = "Query") | | @MutationMapping | Root-Felder vom Typ Mutation | @SchemaMapping(typeName = "Mutation") | | @SubscriptionMapping | Echtzeit-Subscriptions | @SchemaMapping(typeName = "Subscription") | | @SchemaMapping | Alle Felder beliebigen Typs | Generische Form |

EquivalenceDemo.javajava
@Controller
public class EquivalenceDemo {

    // Diese beiden Deklarationen sind äquivalent
    @QueryMapping
    public Article article(@Argument Long id) {
        return findArticle(id);
    }

    @SchemaMapping(typeName = "Query", field = "article")
    public Article articleEquivalent(@Argument Long id) {
        return findArticle(id);
    }

    // Für verschachtelte Felder funktioniert nur @SchemaMapping
    @SchemaMapping(typeName = "Article", field = "author")
    public Author author(Article article) {
        return findAuthor(article.getAuthorId());
    }

    // Alternative Syntax mit dem Typ als Parameter
    @SchemaMapping
    public Author author(Article article) {
        // Spring leitet typeName = "Article" aus dem Parameter ab
        return findAuthor(article.getAuthorId());
    }
}

Wie verwaltet man Mutationen mit Validierung?

GraphQL-Mutationen verändern die Daten. Spring GraphQL integriert sich mit Bean Validation, um Eingaben zu validieren.

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
}

Der Controller verwendet @Valid, um die Validierung auszulösen:

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) {
        // Die Validierung wird automatisch ausgeführt
        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 = "Titel ist erforderlich")
    @Size(min = 5, max = 200, message = "Titel muss zwischen 5 und 200 Zeichen lang sein")
    String title,

    @NotBlank(message = "Inhalt ist erforderlich")
    @Size(min = 100, message = "Inhalt muss mindestens 100 Zeichen lang sein")
    String content,

    @NotNull(message = "Autor ist erforderlich")
    Long authorId
) {}
Fehlerbehandlung

Validierungsfehler geben automatisch einen strukturierten GraphQL-Fehler mit dem Pfad des ungültigen Feldes zurück. Spring GraphQL formatiert diese Fehler gemäß der GraphQL-Spezifikation.

Wie optimiert man Abfragen mit @BatchMapping?

Die Annotation @BatchMapping vereinfacht die Erstellung von DataLoadern direkt in den Controllern. Dieser Ansatz vermeidet die explizite Konfiguration der 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 für Autoren - verarbeitet alle Artikel auf einmal
    @BatchMapping
    public Map<Article, Author> author(List<Article> articles) {
        // Eindeutige Autor-IDs sammeln
        Set<Long> authorIds = articles.stream()
            .map(Article::getAuthorId)
            .collect(Collectors.toSet());

        // Einzige Abfrage für alle Autoren
        Map<Long, Author> authorsById = authorRepository
            .findAllById(authorIds)
            .stream()
            .collect(Collectors.toMap(Author::getId, Function.identity()));

        // Zuordnung Artikel -> Autor
        return articles.stream()
            .collect(Collectors.toMap(
                Function.identity(),
                article -> authorsById.get(article.getAuthorId())
            ));
    }

    // BatchMapping für Kommentare
    @BatchMapping
    public Map<Article, List<Comment>> comments(List<Article> articles) {
        List<Long> articleIds = articles.stream()
            .map(Article::getId)
            .toList();

        // Batch-Abruf der Kommentare
        List<Comment> allComments = commentRepository
            .findByArticleIdIn(articleIds);

        // Gruppierung nach Artikel
        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 bietet eine kompaktere Syntax als die manuelle DataLoader-Konfiguration und behält dieselben Performance-Garantien.

Wie testet man GraphQL-Resolver?

Spring GraphQL stellt GraphQlTester zur Verfügung, um aussagekräftige und lesbare Tests zu schreiben.

ArticleControllerTest.javajava
@SpringBootTest
@AutoConfigureGraphQlTester
class ArticleControllerTest {

    @Autowired
    private GraphQlTester graphQlTester;

    @Test
    void shouldReturnArticleById() {
        // Given: GraphQL-Dokument
        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: "Neuer Titel",
                    content: "Testinhalt, lang genug, um die Validierungseinschränkungen zu erfüllen",
                    authorId: 1
                }) {
                    id
                    title
                }
            }
            """;

        graphQlTester.document(mutation)
            .execute()
            .path("createArticle.id").entity(String.class).isNotEmpty()
            .path("createArticle.title").entity(String.class)
                .isEqualTo("Neuer Titel");
    }

    @Test
    void shouldReturnErrorForInvalidInput() {
        String mutation = """
            mutation {
                createArticle(input: {
                    title: "",
                    content: "kurz",
                    authorId: 1
                }) {
                    id
                }
            }
            """;

        graphQlTester.document(mutation)
            .execute()
            .errors()
            .expect(error -> error.getMessage()
                .contains("Titel ist erforderlich"));
    }

    @Test
    void shouldHandleVariables() {
        String query = """
            query GetArticle($id: ID!) {
                article(id: $id) {
                    id
                    title
                }
            }
            """;

        graphQlTester.document(query)
            .variable("id", 42)
            .execute()
            .path("article").valueIsNull();
    }
}

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Wie verwaltet man Echtzeit-Subscriptions?

Subscriptions ermöglichen das Senden von Echtzeitdaten an Clients über 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));
    }

    // Methode, die beim Erstellen eines Artikels aufgerufen wird
    public void publishArticle(Article article) {
        articleSink.tryEmitNext(article);
    }

    // Methode, die beim Hinzufügen eines Kommentars aufgerufen wird
    public void publishComment(Comment comment) {
        commentSink.tryEmitNext(comment);
    }
}

Die WebSocket-Konfiguration erfordert eine zusätzliche Abhängigkeit:

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();
    }
}

Was sind die Best Practices für GraphQL-Sicherheit?

GraphQL-Sicherheit erfordert mehrere Schutzschichten gegen Denial-of-Service-Angriffe und unautorisierte Zugriffe.

SecurityConfig.javajava
@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                // Geschützter GraphQL-Endpoint
                .requestMatchers("/graphql").authenticated()
                // GraphiQL nur in Dev zugänglich
                .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;
    }

    // Zugriff auf authentifizierte Benutzer beschränkt
    @QueryMapping
    @PreAuthorize("isAuthenticated()")
    public List<Article> articles(@Argument int page,
                                  @Argument int size) {
        return articleRepository.findAll(PageRequest.of(page, size))
            .getContent();
    }

    // Nur Administratoren können löschen
    @MutationMapping
    @PreAuthorize("hasRole('ADMIN')")
    public boolean deleteArticle(@Argument Long id) {
        articleRepository.deleteById(id);
        return true;
    }

    // Feinkörnige Prüfung: nur der Autor kann ändern
    @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();
    }
}

Schutz vor komplexen Abfragen:

yaml
# application.yml
spring:
  graphql:
    schema:
      introspection:
        # Introspection in Produktion deaktivieren
        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(
                "Abfrage zu komplex: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
            );
        }

        return super.beginExecution(parameters, state);
    }
}

Fazit

Spring for GraphQL bietet eine elegante GraphQL-Integration in das Spring-Ökosystem. Die Beherrschung von Resolvern, DataLoadern und der Umgang mit dem N+1-Problem bilden die Wissensgrundlage, die in Interviews erwartet wird.

Spring GraphQL Interview-Checkliste:

  • ✅ Die Rolle der Annotationen @QueryMapping, @MutationMapping und @SchemaMapping erklären
  • ✅ Das N+1-Problem und seine Auswirkungen auf die GraphQL-Performance beschreiben
  • ✅ Einen DataLoader mit @BatchMapping oder BatchLoaderRegistry implementieren
  • ✅ Mutation-Eingaben mit Bean Validation validieren
  • ✅ Resolver mit @PreAuthorize und Spring Security absichern
  • ✅ Resolver mit GraphQlTester testen
  • ✅ Subscriptions für Echtzeitfunktionen konfigurieren
  • ✅ Die API gegen zu komplexe Abfragen schützen

Tags

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

Teilen

Verwandte Artikel