# Spring GraphQL面接: リゾルバー、DataLoader、N+1問題の解決策 > この完全ガイドでSpring GraphQL面接の準備を進めます。リゾルバー、DataLoader、N+1問題への対処、ミューテーション、技術的質問のためのベストプラクティスを解説します。 - 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アプリケーションでのGraphQL統合を簡素化します。この技術はモダンなAPIにとって不可欠なものとなり、このトピックに関する面接質問はますます一般的になっています。本ガイドでは、リゾルバー、DataLoader、N+1問題、高度なパターンといった核心的な概念を網羅します。 > **面接の重点** > > 採用担当者は特にN+1問題の理解とDataLoaderの使用法を確認します。これら2つのトピックはSpring GraphQL面接質問の60%を占めています。 ## Spring for GraphQLとは何ですか? Spring for GraphQLはGraphQL Java Springの公式後継であり、バージョン2.7以降Springフレームワークにネイティブに統合されています。この統合により、Springアノテーションのサポート、Spring Securityとの統合、WebFluxまたはMVCの透過的な使用といった複数の利点が得られます。 基本構成には`spring-boot-starter-graphql`依存関係とGraphQLスキーマのみが必要です。 ```java // build.gradle.kts dependencies { // Spring MVCを使用するSpring GraphQLスターター implementation("org.springframework.boot:spring-boot-starter-graphql") implementation("org.springframework.boot:spring-boot-starter-web") // GraphQLテスト用 testImplementation("org.springframework.graphql:spring-graphql-test") } ``` GraphQLスキーマは`src/main/resources/graphql/`に`.graphqls`拡張子で配置されます。 ```graphql # schema.graphqls type Query { # 識別子で記事を取得 article(id: ID!): Article # ページ分けされた記事リスト articles(page: Int = 0, size: Int = 10): [Article!]! } type Article { id: ID! title: String! content: String! # 著者との関係 author: Author! # コメントのリスト comments: [Comment!]! createdAt: String! } type Author { id: ID! name: String! email: String! # この著者によって書かれた記事 articles: [Article!]! } type Comment { id: ID! content: String! author: Author! createdAt: String! } ``` このアーキテクチャは型とその関係を宣言します。GraphQLはドキュメントを自動生成し、サーバー側でクエリを検証します。 ## Spring GraphQLリゾルバーはどのように機能しますか? リゾルバーはGraphQL実行の中核を構成します。各スキーマフィールドは専用のリゾルバーを持つことができます。Springはルートクエリには`@QueryMapping`アノテーションを、関係には`@SchemaMapping`を使用します。 ```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)のリゾルバー @QueryMapping public Article article(@Argument Long id) { return articleRepository.findById(id) .orElseThrow(() -> new ArticleNotFoundException(id)); } // 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(); } // Article.authorのリゾルバー - 各記事に対して呼び出されます @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return authorRepository.findById(article.getAuthorId()) .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId())); } } ``` `author`リゾルバーは返される各記事に対して実行されます。この柔軟なアーキテクチャはオンデマンドでデータを読み込みますが、N+1問題を引き起こします。 > **古典的な面接の罠** > > N+1問題に言及せずに`@SchemaMapping`リゾルバーを実装する候補者は、不完全なGraphQL理解を示します。採用担当者は系統的にこの分析を期待しています。 ## GraphQLにおけるN+1問題とは何ですか? N+1問題は、GraphQLクエリが関係を読み込むためにN個の追加クエリをトリガーするときに発生します。この破壊的なパターンは、基本的なリゾルバーで系統的に発生します。 50件の記事を著者と共に取得するクエリを考えてみます: ```graphql # GraphQLクエリ query { articles(size: 50) { id title author { name } } } ``` 前述のリゾルバーでは、このクエリは以下を実行します: - 50件の記事に対する1クエリ - 各著者を個別に読み込む50クエリ ```sql -- クエリ1: 記事の取得 SELECT * FROM articles LIMIT 50 -- クエリ2-51: 各著者を個別に SELECT * FROM authors WHERE id = 1 SELECT * FROM authors WHERE id = 2 SELECT * FROM authors WHERE id = 3 -- ... 47個の追加クエリ ``` このクエリの増殖はパフォーマンスを劇的に低下させます。50msで応答するエンドポイントは、N+1で2秒に跳ね上がる可能性があります。 ## DataLoaderはどのように機能しますか? DataLoaderは個別のクエリをバッチリクエストにグループ化します。各著者を別々に読み込む代わりに、DataLoaderは要求されたすべてのIDを収集し、単一のクエリを実行します。 ```java // AuthorBatchLoader.java @Component public class AuthorBatchLoader { private final AuthorRepository authorRepository; public AuthorBatchLoader(AuthorRepository authorRepository) { this.authorRepository = authorRepository; } // バッチ読み込みメソッド public Mono> loadAuthors(Set authorIds) { // すべての著者に対する単一クエリ List authors = authorRepository.findAllById(authorIds); // 高速検索のためのMapへの変換 Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` DataLoaderの登録は専用構成内の`BatchLoaderRegistry`を介して行われます。 ```java // GraphQLConfig.java @Configuration public class GraphQLConfig { @Bean public BatchLoaderRegistry batchLoaderRegistry( AuthorBatchLoader authorBatchLoader) { return registry -> { // 著者用DataLoaderの登録 registry.forTypePair(Long.class, Author.class) .registerMappedBatchLoader((authorIds, env) -> authorBatchLoader.loadAuthors(authorIds)); }; } } ``` 変更されたリゾルバーは、直接アクセスの代わりにDataLoaderを使用するようになります: ```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で最適化されたリゾルバー @SchemaMapping(typeName = "Article", field = "author") public CompletableFuture author( Article article, DataLoader authorDataLoader) { // DataLoaderは呼び出しを自動的にバッチ化します return authorDataLoader.load(article.getAuthorId()); } } ``` このアプローチでは、50件の記事に対する同じクエリは2つのSQLクエリのみを実行します: ```sql -- クエリ1: 記事の取得 SELECT * FROM articles LIMIT 50 -- クエリ2: すべての著者を一度に SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50) ``` ## コンテキスト付きDataLoaderをどのように実装しますか? 高度なDataLoaderは、マルチテナントアプリケーションのテナント識別子やセキュリティフィルターなど、追加のコンテキストを必要とする場合があります。 ```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コンテキストの取得 GraphQLContext context = env.getContext(); String tenantId = context.get("tenantId"); // テナントでフィルタリングされたクエリ List authors = authorRepository .findAllByIdInAndTenantId(authorIds, tenantId); Map authorMap = authors.stream() .collect(Collectors.toMap(Author::getId, Function.identity())); return Mono.just(authorMap); } } ``` コンテキスト構成はWebGraphQLインターセプターを介して行われます: ```java // TenantInterceptor.java @Component public class TenantInterceptor implements WebGraphQlInterceptor { @Override public Mono intercept( WebGraphQlRequest request, Chain chain) { // ヘッダーからテナントを抽出 String tenantId = request.getHeaders() .getFirst("X-Tenant-ID"); // GraphQLコンテキストに追加 request.configureExecutionInput((input, builder) -> builder.graphQLContext(ctx -> ctx.put("tenantId", tenantId) ).build() ); return chain.next(request); } } ``` ## @QueryMappingと@SchemaMappingの違いは何ですか? この古典的な面接質問は、リゾルバー階層の理解を確認します。 | アノテーション | 用途 | 同等 | |----------------|------|------| | `@QueryMapping` | Query型のルートフィールド | `@SchemaMapping(typeName = "Query")` | | `@MutationMapping` | Mutation型のルートフィールド | `@SchemaMapping(typeName = "Mutation")` | | `@SubscriptionMapping` | リアルタイムサブスクリプション | `@SchemaMapping(typeName = "Subscription")` | | `@SchemaMapping` | 任意の型のすべてのフィールド | 汎用形式 | ```java // EquivalenceDemo.java @Controller public class EquivalenceDemo { // これら2つの宣言は同等です @QueryMapping public Article article(@Argument Long id) { return findArticle(id); } @SchemaMapping(typeName = "Query", field = "article") public Article articleEquivalent(@Argument Long id) { return findArticle(id); } // ネストされたフィールドには@SchemaMappingのみが機能します @SchemaMapping(typeName = "Article", field = "author") public Author author(Article article) { return findAuthor(article.getAuthorId()); } // 型をパラメータとして使用する代替構文 @SchemaMapping public Author author(Article article) { // SpringはパラメータからtypeName = "Article"を推論します return findAuthor(article.getAuthorId()); } } ``` ## 検証付きミューテーションをどのように管理しますか? GraphQLミューテーションはデータを変更します。Spring GraphQLは入力を検証するためにBean Validationと統合します。 ```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 } ``` コントローラーは検証をトリガーするために`@Valid`を使用します: ```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) { // 検証が自動的に実行されます 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 = "タイトルは必須です") @Size(min = 5, max = 200, message = "タイトルは5から200文字の間でなければなりません") String title, @NotBlank(message = "コンテンツは必須です") @Size(min = 100, message = "コンテンツは少なくとも100文字でなければなりません") String content, @NotNull(message = "著者は必須です") Long authorId ) {} ``` > **エラー処理** > > 検証エラーは、無効なフィールドのパスを含む構造化されたGraphQLエラーを自動的に返します。Spring GraphQLはこれらのエラーをGraphQL仕様に従ってフォーマットします。 ## @BatchMappingでクエリをどのように最適化しますか? `@BatchMapping`アノテーションは、コントローラー内で直接DataLoaderの作成を簡素化します。このアプローチは明示的な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 - すべての記事を一度に処理 @BatchMapping public Map author(List
articles) { // 一意の著者IDを収集 Set authorIds = articles.stream() .map(Article::getAuthorId) .collect(Collectors.toSet()); // すべての著者に対する単一クエリ Map authorsById = authorRepository .findAllById(authorIds) .stream() .collect(Collectors.toMap(Author::getId, Function.identity())); // 記事 -> 著者の関連付け return articles.stream() .collect(Collectors.toMap( Function.identity(), article -> authorsById.get(article.getAuthorId()) )); } // コメント用BatchMapping @BatchMapping public Map> comments(List
articles) { List articleIds = articles.stream() .map(Article::getId) .toList(); // コメントのバッチ取得 List allComments = commentRepository .findByArticleIdIn(articleIds); // 記事ごとにグループ化 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`は、同じパフォーマンス保証を維持しながら、手動DataLoader構成よりも簡潔な構文を提供します。 ## GraphQLリゾルバーをどのようにテストしますか? Spring GraphQLは、表現力豊かで読みやすいテストを書くために`GraphQlTester`を提供します。 ```java // ArticleControllerTest.java @SpringBootTest @AutoConfigureGraphQlTester class ArticleControllerTest { @Autowired private GraphQlTester graphQlTester; @Test void shouldReturnArticleById() { // Given: GraphQLドキュメント 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: "新しいタイトル", content: "検証制約を満たすのに十分な長さのテストコンテンツ", authorId: 1 }) { id title } } """; graphQlTester.document(mutation) .execute() .path("createArticle.id").entity(String.class).isNotEmpty() .path("createArticle.title").entity(String.class) .isEqualTo("新しいタイトル"); } @Test void shouldReturnErrorForInvalidInput() { String mutation = """ mutation { createArticle(input: { title: "", content: "短い", authorId: 1 }) { id } } """; graphQlTester.document(mutation) .execute() .errors() .expect(error -> error.getMessage() .contains("タイトルは必須です")); } @Test void shouldHandleVariables() { String query = """ query GetArticle($id: ID!) { article(id: $id) { id title } } """; graphQlTester.document(query) .variable("id", 42) .execute() .path("article").valueIsNull(); } } ``` ## リアルタイムサブスクリプションをどのように管理しますか? サブスクリプションは、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)); } // 記事作成時に呼び出されるメソッド public void publishArticle(Article article) { articleSink.tryEmitNext(article); } // コメント追加時に呼び出されるメソッド public void publishComment(Comment comment) { commentSink.tryEmitNext(comment); } } ``` WebSocket構成には追加の依存関係が必要です: ```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セキュリティのベストプラクティスは何ですか? GraphQLセキュリティには、サービス拒否攻撃や不正アクセスに対する複数の保護層が必要です。 ```java // SecurityConfig.java @Configuration @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // 保護されたGraphQLエンドポイント .requestMatchers("/graphql").authenticated() // GraphiQLは開発環境でのみアクセス可能 .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; } // 認証されたユーザーに制限されたアクセス @QueryMapping @PreAuthorize("isAuthenticated()") public List
articles(@Argument int page, @Argument int size) { return articleRepository.findAll(PageRequest.of(page, size)) .getContent(); } // 管理者のみが削除可能 @MutationMapping @PreAuthorize("hasRole('ADMIN')") public boolean deleteArticle(@Argument Long id) { articleRepository.deleteById(id); return true; } // 細かいチェック: 著者のみが変更可能 @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(); } } ``` 複雑なクエリに対する保護: ```yaml # application.yml spring: graphql: schema: introspection: # 本番環境ではイントロスペクションを無効化 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( "クエリが複雑すぎます: " + complexity + " (max: " + MAX_COMPLEXITY + ")" ); } return super.beginExecution(parameters, state); } } ``` ## 結論 Spring for GraphQLは、GraphQLをSpringエコシステムに優雅に統合します。リゾルバー、DataLoader、N+1問題の管理を習得することは、面接で期待される知識の基盤を構成します。 **Spring GraphQL面接チェックリスト:** - ✅ `@QueryMapping`、`@MutationMapping`、`@SchemaMapping`アノテーションの役割を説明する - ✅ N+1問題とそのGraphQLパフォーマンスへの影響を記述する - ✅ `@BatchMapping`またはBatchLoaderRegistryでDataLoaderを実装する - ✅ Bean Validationでミューテーション入力を検証する - ✅ `@PreAuthorize`とSpring Securityでリゾルバーを保護する - ✅ GraphQlTesterでリゾルバーをテストする - ✅ リアルタイム機能のためにサブスクリプションを構成する - ✅ APIを過度に複雑なクエリから保護する --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/spring-boot/spring-graphql-interview-resolvers-dataloaders-n1