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.

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.
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.
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.
# 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.
@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.
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-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
-- 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 AbfragenDiese 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.
@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.
@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:
@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:
-- 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.
@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:
@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 |
@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.
# 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:
@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;
}
}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
) {}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.
@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.
@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.
# 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));
}
// 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:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@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.
@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();
}
}@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:
# application.yml
spring:
graphql:
schema:
introspection:
# Introspection in Produktion deaktivieren
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(
"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,@MutationMappingund@SchemaMappingerklären - ✅ Das N+1-Problem und seine Auswirkungen auf die GraphQL-Performance beschreiben
- ✅ Einen DataLoader mit
@BatchMappingoder BatchLoaderRegistry implementieren - ✅ Mutation-Eingaben mit Bean Validation validieren
- ✅ Resolver mit
@PreAuthorizeund Spring Security absichern - ✅ Resolver mit GraphQlTester testen
- ✅ Subscriptions für Echtzeitfunktionen konfigurieren
- ✅ Die API gegen zu komplexe Abfragen schützen
Tags
Teilen
Verwandte Artikel

Spring Boot Interview: Transaktions-Propagation erklärt
Beherrsche die Spring Boot Transaktions-Propagation: REQUIRED, REQUIRES_NEW, NESTED und mehr. 12 Interview-Fragen mit Code und typischen Fallstricken.

Spring Data JPA N+1-Lösungen 2026: Fetch Join und EntityGraph
Vollständige Anleitung zur Erkennung und Behebung des N+1-Problems in Spring Data JPA. Fetch Join, @EntityGraph, Batch Fetching und Strategien für die Abfrageleistung.

30 Spring Boot Interviewfragen: Vollständiger Leitfaden für Java-Entwickler
Bereiten Sie sich auf Ihre Spring-Boot-Interviews mit diesen 30 essenziellen Fragen zu Auto-Konfiguration, Startern, Spring Data JPA, Sicherheit und Tests vor.