# 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. - 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, 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. > **Mülakat Odağı** > > İş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. ```java // build.gradle.kts 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. ```graphql # 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. ```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; } // 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
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. > **Klasik Mülakat Tuzağı** > > 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 # 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 ```sql -- 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 sorgu ``` Bu 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. ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // Toplu yükleme yöntemi public Mono> loadAuthors(Set authorIds) { // Tüm yazarlar için tek sorgu List authors = authorRepository.findAllById(authorIds); // Hızlı arama için Map'e dönüştürme Map 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. ```java // GraphQLConfig.java @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: ```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(); } // DataLoader ile optimize edilmiş resolver @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader 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: ```sql -- 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) ``` ## 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. ```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 bağlamını al GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // Tenant'a göre filtrelenmiş sorgu List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map 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: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono 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 | ```java // EquivalenceDemo.java @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. ```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 } ``` Controller, doğrulamayı tetiklemek için `@Valid` kullanır: ```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) { // 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; } } ``` ```java // CreateArticleInput.java 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 ) {} ``` > **Hata Yönetimi** > > 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. ```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(); } // Yazarlar için BatchMapping - tüm makaleleri bir kerede işler @BatchMapping public Map author(List
articles) { // Benzersiz yazar ID'lerini topla Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // Tüm yazarlar için tek sorgu Map 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> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // Yorumların toplu getirilmesi List allComments = commentRepository .findByArticleIdIn(articleIds); // Makaleye göre gruplama 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`, 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. ```java // ArticleControllerTest.java @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(); } } ``` ## Gerçek Zamanlı Abonelikler Nasıl Yönetilir? Abonelikler, WebSocket aracılığıyla istemcilere gerçek zamanlı veri göndermeyi sağlar. ```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)); } // 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: ```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(); } } ``` ## 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. ```java // SecurityConfig.java @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(); } } ``` ```java // SecuredArticleController.java @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
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: ```yaml # application.yml spring: graphql: schema: introspection: # Üretimde introspection'ı devre dışı bırak 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( "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`, `@MutationMapping` ve `@SchemaMapping` annotation'larının rolünü açıklamak - ✅ N+1 problemini ve GraphQL performansı üzerindeki etkisini tanımlamak - ✅ `@BatchMapping` veya BatchLoaderRegistry ile bir DataLoader uygulamak - ✅ Mutation girdilerini Bean Validation ile doğrulamak - ✅ Resolver'ları `@PreAuthorize` ve 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1