# 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. - 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 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. ```java // build.gradle.kts 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. ```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 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
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. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Batch-Lademethode public Mono> loadAuthors(Set authorIds) { // Einzige Abfrage für alle Autoren List authors = authorRepository.findAllById(authorIds); // Umwandlung in Map für schnelle Suche Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` Die DataLoader-Registrierung erfolgt über `BatchLoaderRegistry` in einer dedizierten Konfiguration. ```java // GraphQLConfig.java @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: ```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(); } // Optimierter Resolver mit DataLoader @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader 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) ``` ## 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. ```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) { // GraphQL-Kontext abrufen GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Nach Tenant gefilterte Abfrage List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` Die Kontextkonfiguration erfolgt über einen WebGraphQL-Interceptor: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono 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 | ```java // EquivalenceDemo.java @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: ```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) { // 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; } } ``` ```java // CreateArticleInput.java 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. ```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 für Autoren - verarbeitet alle Artikel auf einmal @BatchMapping public Map author(List
articles) { // Eindeutige Autor-IDs sammeln Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Einzige Abfrage für alle Autoren Map 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> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Batch-Abruf der Kommentare List allComments = commentRepository .findByArticleIdIn(articleIds); // Gruppierung nach Artikel 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` 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. ```java // ArticleControllerTest.java @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(); } } ``` ## 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! } ``` ```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)); } // 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: ```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(); } } ``` ## Was sind die Best Practices für GraphQL-Sicherheit? GraphQL-Sicherheit erfordert mehrere Schutzschichten gegen Denial-of-Service-Angriffe und unautorisierte Zugriffe. ```java // SecurityConfig.java @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(); } } ``` ```java // SecuredArticleController.java @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
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 ``` ```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( "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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1