Spring GraphQL Mülakatı: Resolver'lar, DataLoader'lar ve N+1 Problemi Çözümleri
Bu kapsamlı kılavuzla Spring GraphQL mülakatlarına hazırlanın. Resolver'lar, DataLoader'lar, N+1 problemi yönetimi, mutation'lar ve teknik sorular için en iyi uygulamalar.

Spring for GraphQL, Spring Boot uygulamalarında GraphQL entegrasyonunu basitleştirir. Bu teknoloji modern API'lar için vazgeçilmez hale gelmiş ve bu konudaki mülakat soruları giderek daha sık sorulmaktadır. Bu kılavuz temel kavramları kapsar: resolver'lar, DataLoader'lar, N+1 problemi ve gelişmiş desenler.
İşe alım uzmanları özellikle N+1 probleminin anlaşılmasını ve DataLoader kullanımını test eder. Bu iki konu Spring GraphQL mülakat sorularının %60'ını oluşturur.
Spring for GraphQL Nedir?
Spring for GraphQL, GraphQL Java Spring'in resmi halefi olup 2.7 sürümünden itibaren Spring framework'üne yerel olarak entegre edilmiştir. Bu entegrasyon birçok avantaj sağlar: Spring annotation desteği, Spring Security entegrasyonu ve WebFlux veya MVC'nin şeffaf kullanımı.
Temel yapılandırma yalnızca spring-boot-starter-graphql bağımlılığını ve bir GraphQL şemasını gerektirir.
dependencies {
// Spring MVC ile Spring GraphQL starter
implementation("org.springframework.boot:spring-boot-starter-graphql")
implementation("org.springframework.boot:spring-boot-starter-web")
// GraphQL testleri için
testImplementation("org.springframework.graphql:spring-graphql-test")
}GraphQL şeması src/main/resources/graphql/ dizininde .graphqls uzantısıyla bulunur.
# schema.graphqls
type Query {
# Bir makaleyi tanımlayıcısına göre getir
article(id: ID!): Article
# Sayfalanmış makale listesi
articles(page: Int = 0, size: Int = 10): [Article!]!
}
type Article {
id: ID!
title: String!
content: String!
# Yazarla ilişki
author: Author!
# Yorum listesi
comments: [Comment!]!
createdAt: String!
}
type Author {
id: ID!
name: String!
email: String!
# Bu yazar tarafından yazılan makaleler
articles: [Article!]!
}
type Comment {
id: ID!
content: String!
author: Author!
createdAt: String!
}Bu mimari, türleri ve ilişkilerini bildirir. GraphQL otomatik olarak dokümantasyonu oluşturur ve sorguları sunucu tarafında doğrular.
Spring GraphQL Resolver'ları Nasıl Çalışır?
Resolver'lar GraphQL yürütmesinin kalbini oluşturur. Her şema alanının özel bir resolver'ı olabilir. Spring, kök sorgular için @QueryMapping ve ilişkiler için @SchemaMapping annotation'ını kullanır.
@Controller
public class ArticleController {
private final ArticleRepository articleRepository;
private final AuthorRepository authorRepository;
public ArticleController(ArticleRepository articleRepository,
AuthorRepository authorRepository) {
this.articleRepository = articleRepository;
this.authorRepository = authorRepository;
}
// Query.article(id) için resolver
@QueryMapping
public Article article(@Argument Long id) {
return articleRepository.findById(id)
.orElseThrow(() -> new ArticleNotFoundException(id));
}
// Query.articles(page, size) için resolver
@QueryMapping
public List<Article> articles(@Argument int page,
@Argument int size) {
Pageable pageable = PageRequest.of(page, size);
return articleRepository.findAll(pageable).getContent();
}
// Article.author için resolver - her makale için çağrılır
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return authorRepository.findById(article.getAuthorId())
.orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
}
}author resolver'ı her döndürülen makale için yürütülür. Bu esnek mimari verileri talep üzerine yükler ancak N+1 problemini ortaya çıkarır.
N+1 probleminden bahsetmeden bir @SchemaMapping resolver'ı uygulayan aday, eksik bir GraphQL anlayışı sergiler. İşe alım uzmanları sistematik olarak bu analizi bekler.
GraphQL'de N+1 Problemi Nedir?
N+1 problemi, bir GraphQL sorgusunun ilişkileri yüklemek için N ek sorgu tetiklediğinde ortaya çıkar. Bu yıkıcı desen, temel resolver'larla sistematik olarak meydana gelir.
50 makaleyi yazarlarıyla birlikte getiren bir sorguyu düşünün:
# GraphQL sorgusu
query {
articles(size: 50) {
id
title
author {
name
}
}
}Önceki resolver ile bu sorgu şunları yürütür:
- 50 makale için 1 sorgu
- Her yazarı ayrı ayrı yüklemek için 50 sorgu
-- Sorgu 1: makaleleri getir
SELECT * FROM articles LIMIT 50
-- Sorgular 2-51: her yazar ayrı ayrı
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... 47 ek sorguBu sorgu çoğalması performansı dramatik şekilde düşürür. 50ms'de yanıt veren bir endpoint, N+1 ile 2 saniyeye çıkabilir.
DataLoader'lar Nasıl Çalışır?
DataLoader'lar bireysel sorguları toplu isteklerde gruplandırır. Her yazarı ayrı yüklemek yerine, DataLoader istenen tüm ID'leri toplar ve tek bir sorgu yürütür.
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// Toplu yükleme yöntemi
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// Tüm yazarlar için tek sorgu
List<Author> authors = authorRepository.findAllById(authorIds);
// Hızlı arama için Map'e dönüştürme
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}DataLoader kaydı, özel bir yapılandırmada BatchLoaderRegistry aracılığıyla yapılır.
@Configuration
public class GraphQLConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(
AuthorBatchLoader authorBatchLoader) {
return registry -> {
// Yazarlar için DataLoader kaydı
registry.forTypePair(Long.class, Author.class)
.registerMappedBatchLoader((authorIds, env) ->
authorBatchLoader.loadAuthors(authorIds));
};
}
}Değiştirilen resolver artık doğrudan erişim yerine DataLoader kullanır:
@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();
}
// DataLoader ile optimize edilmiş resolver
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> authorDataLoader) {
// DataLoader çağrıları otomatik olarak gruplar
return authorDataLoader.load(article.getAuthorId());
}
}Bu yaklaşımla, 50 makale için aynı sorgu yalnızca 2 SQL sorgusu yürütür:
-- Sorgu 1: makaleleri getir
SELECT * FROM articles LIMIT 50
-- Sorgu 2: tüm yazarlar bir kerede
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)Spring Boot mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Bağlam ile DataLoader Nasıl Uygulanır?
Gelişmiş DataLoader'lar bazen ek bağlam gerektirir, örneğin çok kiracılı uygulamalar için tenant kimliği veya güvenlik filtreleri.
@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 bağlamını al
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// Tenant'a göre filtrelenmiş sorgu
List<Author> authors = authorRepository
.findAllByIdInAndTenantId(authorIds, tenantId);
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}Bağlam yapılandırması bir WebGraphQL interceptor aracılığıyla yapılır:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> intercept(
WebGraphQlRequest request,
Chain chain) {
// Tenant'ı header'lardan çıkar
String tenantId = request.getHeaders()
.getFirst("X-Tenant-ID");
// GraphQL bağlamına ekle
request.configureExecutionInput((input, builder) ->
builder.graphQLContext(ctx ->
ctx.put("tenantId", tenantId)
).build()
);
return chain.next(request);
}
}@QueryMapping ve @SchemaMapping Arasındaki Farklar Nelerdir?
Bu klasik mülakat sorusu resolver hiyerarşisinin anlaşılmasını doğrular.
| Annotation | Kullanım | Eşdeğer |
|------------|----------|---------|
| @QueryMapping | Query türünün kök alanları | @SchemaMapping(typeName = "Query") |
| @MutationMapping | Mutation türünün kök alanları | @SchemaMapping(typeName = "Mutation") |
| @SubscriptionMapping | Gerçek zamanlı abonelikler | @SchemaMapping(typeName = "Subscription") |
| @SchemaMapping | Herhangi bir türün tüm alanları | Genel form |
@Controller
public class EquivalenceDemo {
// Bu iki bildirim eşdeğerdir
@QueryMapping
public Article article(@Argument Long id) {
return findArticle(id);
}
@SchemaMapping(typeName = "Query", field = "article")
public Article articleEquivalent(@Argument Long id) {
return findArticle(id);
}
// İç içe alanlar için yalnızca @SchemaMapping çalışır
@SchemaMapping(typeName = "Article", field = "author")
public Author author(Article article) {
return findAuthor(article.getAuthorId());
}
// Tür parametre olarak ile alternatif sözdizimi
@SchemaMapping
public Author author(Article article) {
// Spring parametreden typeName = "Article" çıkarımı yapar
return findAuthor(article.getAuthorId());
}
}Mutation'lar Doğrulama ile Nasıl Yönetilir?
GraphQL mutation'ları verileri değiştirir. Spring GraphQL girdileri doğrulamak için Bean Validation ile entegre olur.
# 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
}Controller, doğrulamayı tetiklemek için @Valid kullanır:
@Controller
public class ArticleMutationController {
private final ArticleService articleService;
public ArticleMutationController(ArticleService articleService) {
this.articleService = articleService;
}
@MutationMapping
public Article createArticle(@Argument @Valid CreateArticleInput input) {
// Doğrulama otomatik olarak yürütülür
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 = "Başlık zorunludur")
@Size(min = 5, max = 200, message = "Başlık 5 ile 200 karakter arasında olmalıdır")
String title,
@NotBlank(message = "İçerik zorunludur")
@Size(min = 100, message = "İçerik en az 100 karakter olmalıdır")
String content,
@NotNull(message = "Yazar zorunludur")
Long authorId
) {}Doğrulama hataları otomatik olarak geçersiz alanın yolu ile yapılandırılmış bir GraphQL hatası döndürür. Spring GraphQL bu hataları GraphQL spesifikasyonuna göre biçimlendirir.
Sorgular @BatchMapping ile Nasıl Optimize Edilir?
@BatchMapping annotation'ı, DataLoader oluşturmayı doğrudan controller'larda basitleştirir. Bu yaklaşım, açık BatchLoaderRegistry yapılandırmasından kaçınır.
@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();
}
// Yazarlar için BatchMapping - tüm makaleleri bir kerede işler
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// Benzersiz yazar ID'lerini topla
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// Tüm yazarlar için tek sorgu
Map<Long, Author> authorsById = authorRepository
.findAllById(authorIds)
.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
// Makale -> yazar ilişkilendirmesi
return articles.stream()
.collect(Collectors.toMap(
Function.identity(),
article -> authorsById.get(article.getAuthorId())
));
}
// Yorumlar için BatchMapping
@BatchMapping
public Map<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// Yorumların toplu getirilmesi
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// Makaleye göre gruplama
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, manuel DataLoader yapılandırmasından daha öz bir sözdizimi sunarken aynı performans garantilerini korur.
GraphQL Resolver'ları Nasıl Test Edilir?
Spring GraphQL, anlamlı ve okunabilir testler yazmak için GraphQlTester sağlar.
@SpringBootTest
@AutoConfigureGraphQlTester
class ArticleControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldReturnArticleById() {
// Given: GraphQL belgesi
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: "Yeni başlık",
content: "Doğrulama kısıtlamalarını karşılayacak kadar uzun test içeriği",
authorId: 1
}) {
id
title
}
}
""";
graphQlTester.document(mutation)
.execute()
.path("createArticle.id").entity(String.class).isNotEmpty()
.path("createArticle.title").entity(String.class)
.isEqualTo("Yeni başlık");
}
@Test
void shouldReturnErrorForInvalidInput() {
String mutation = """
mutation {
createArticle(input: {
title: "",
content: "kısa",
authorId: 1
}) {
id
}
}
""";
graphQlTester.document(mutation)
.execute()
.errors()
.expect(error -> error.getMessage()
.contains("Başlık zorunludur"));
}
@Test
void shouldHandleVariables() {
String query = """
query GetArticle($id: ID!) {
article(id: $id) {
id
title
}
}
""";
graphQlTester.document(query)
.variable("id", 42)
.execute()
.path("article").valueIsNull();
}
}Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
Gerçek Zamanlı Abonelikler Nasıl Yönetilir?
Abonelikler, WebSocket aracılığıyla istemcilere gerçek zamanlı veri göndermeyi sağlar.
# 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));
}
// Bir makale oluşturulduğunda çağrılan yöntem
public void publishArticle(Article article) {
articleSink.tryEmitNext(article);
}
// Bir yorum eklendiğinde çağrılan yöntem
public void publishComment(Comment comment) {
commentSink.tryEmitNext(comment);
}
}WebSocket yapılandırması ek bir bağımlılık gerektirir:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}GraphQL Güvenlik En İyi Uygulamaları Nelerdir?
GraphQL güvenliği, hizmet reddi saldırılarına ve yetkisiz erişime karşı birden fazla koruma katmanı gerektirir.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// Korumalı GraphQL endpoint'i
.requestMatchers("/graphql").authenticated()
// GraphiQL yalnızca dev ortamında erişilebilir
.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;
}
// Erişim kimliği doğrulanmış kullanıcılarla sınırlı
@QueryMapping
@PreAuthorize("isAuthenticated()")
public List<Article> articles(@Argument int page,
@Argument int size) {
return articleRepository.findAll(PageRequest.of(page, size))
.getContent();
}
// Yalnızca yöneticiler silebilir
@MutationMapping
@PreAuthorize("hasRole('ADMIN')")
public boolean deleteArticle(@Argument Long id) {
articleRepository.deleteById(id);
return true;
}
// İnce taneli kontrol: yalnızca yazar değiştirebilir
@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();
}
}Karmaşık sorgulara karşı koruma:
# application.yml
spring:
graphql:
schema:
introspection:
# Üretimde introspection'ı devre dışı bırak
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(
"Sorgu çok karmaşık: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
);
}
return super.beginExecution(parameters, state);
}
}Sonuç
Spring for GraphQL, GraphQL'in Spring ekosistemine zarif bir entegrasyonunu sunar. Resolver'lar, DataLoader'lar ve N+1 problemi yönetimine hâkim olmak, mülakatlarda beklenen bilgi temelini oluşturur.
Spring GraphQL Mülakat Kontrol Listesi:
- ✅
@QueryMapping,@MutationMappingve@SchemaMappingannotation'larının rolünü açıklamak - ✅ N+1 problemini ve GraphQL performansı üzerindeki etkisini tanımlamak
- ✅
@BatchMappingveya BatchLoaderRegistry ile bir DataLoader uygulamak - ✅ Mutation girdilerini Bean Validation ile doğrulamak
- ✅ Resolver'ları
@PreAuthorizeve Spring Security ile güvenceye almak - ✅ Resolver'ları GraphQlTester ile test etmek
- ✅ Gerçek zamanlı işlevler için abonelikleri yapılandırmak
- ✅ API'yi aşırı karmaşık sorgulara karşı korumak
Etiketler
Paylaş
İlgili makaleler

Spring Boot Mülakatı: İşlem Yayılımı Açıklandı
Spring Boot işlem yayılımına hakim olun: REQUIRED, REQUIRES_NEW, NESTED ve daha fazlası. Kod örnekleri ve yaygın tuzaklarla 12 mülakat sorusu.

2026'da Spring Data JPA N+1 Sorgu Çözümleri: Fetch Join ve EntityGraph
Spring Data JPA'da N+1 sorununu tespit etme ve düzeltme rehberi. Fetch join, @EntityGraph, batch fetching ve sorgu performansı stratejileri.

30 Spring Boot Mülakat Sorusu: Java Geliştiricileri için Eksiksiz Kılavuz
Auto-configuration, starter'lar, Spring Data JPA, güvenlik ve test konularını kapsayan 30 temel soruyla Spring Boot mülakatlarına hazırlanın.