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.

Resolver'lar ve DataLoader'lar ile Spring GraphQL teknik mülakatı

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.

build.gradle.ktsjava
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.

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

    // 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.

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.

AuthorBatchLoader.javajava
@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.

GraphQLConfig.javajava
@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:

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

    // 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:

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)

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.

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 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:

TenantInterceptor.javajava
@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 |

EquivalenceDemo.javajava
@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:

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) {
        // 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;
    }
}
CreateArticleInput.javajava
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.

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

    // 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.

ArticleControllerTest.javajava
@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.

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

    // 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:

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

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.

SecurityConfig.javajava
@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();
    }
}
SecuredArticleController.javajava
@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:

yaml
# application.yml
spring:
  graphql:
    schema:
      introspection:
        # Üretimde introspection'ı devre dışı bırak
        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(
                "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

Etiketler

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

Paylaş

İlgili makaleler