Spring GraphQL面接: リゾルバー、DataLoader、N+1問題の解決策
この完全ガイドでSpring GraphQL面接の準備を進めます。リゾルバー、DataLoader、N+1問題への対処、ミューテーション、技術的質問のためのベストプラクティスを解説します。

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スキーマのみが必要です。
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拡張子で配置されます。
# 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を使用します。
@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<Article> 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クエリ
query {
articles(size: 50) {
id
title
author {
name
}
}
}前述のリゾルバーでは、このクエリは以下を実行します:
- 50件の記事に対する1クエリ
- 各著者を個別に読み込む50クエリ
-- クエリ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を収集し、単一のクエリを実行します。
@Component
public class AuthorBatchLoader {
private final AuthorRepository authorRepository;
public AuthorBatchLoader(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
// バッチ読み込みメソッド
public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
// すべての著者に対する単一クエリ
List<Author> authors = authorRepository.findAllById(authorIds);
// 高速検索のためのMapへの変換
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}DataLoaderの登録は専用構成内のBatchLoaderRegistryを介して行われます。
@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を使用するようになります:
@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で最適化されたリゾルバー
@SchemaMapping(typeName = "Article", field = "author")
public CompletableFuture<Author> author(
Article article,
DataLoader<Long, Author> authorDataLoader) {
// DataLoaderは呼び出しを自動的にバッチ化します
return authorDataLoader.load(article.getAuthorId());
}
}このアプローチでは、50件の記事に対する同じクエリは2つのSQLクエリのみを実行します:
-- クエリ1: 記事の取得
SELECT * FROM articles LIMIT 50
-- クエリ2: すべての著者を一度に
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)Spring Bootの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
コンテキスト付きDataLoaderをどのように実装しますか?
高度なDataLoaderは、マルチテナントアプリケーションのテナント識別子やセキュリティフィルターなど、追加のコンテキストを必要とする場合があります。
@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コンテキストの取得
GraphQLContext context = env.getContext();
String tenantId = context.get("tenantId");
// テナントでフィルタリングされたクエリ
List<Author> authors = authorRepository
.findAllByIdInAndTenantId(authorIds, tenantId);
Map<Long, Author> authorMap = authors.stream()
.collect(Collectors.toMap(Author::getId, Function.identity()));
return Mono.just(authorMap);
}
}コンテキスト構成はWebGraphQLインターセプターを介して行われます:
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {
@Override
public Mono<WebGraphQlResponse> 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 | 任意の型のすべてのフィールド | 汎用形式 |
@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と統合します。
# 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を使用します:
@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;
}
}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構成を回避します。
@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 - すべての記事を一度に処理
@BatchMapping
public Map<Article, Author> author(List<Article> articles) {
// 一意の著者IDを収集
Set<Long> authorIds = articles.stream()
.map(Article::getAuthorId)
.collect(Collectors.toSet());
// すべての著者に対する単一クエリ
Map<Long, Author> 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<Article, List<Comment>> comments(List<Article> articles) {
List<Long> articleIds = articles.stream()
.map(Article::getId)
.toList();
// コメントのバッチ取得
List<Comment> allComments = commentRepository
.findByArticleIdIn(articleIds);
// 記事ごとにグループ化
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は、同じパフォーマンス保証を維持しながら、手動DataLoader構成よりも簡潔な構文を提供します。
GraphQLリゾルバーをどのようにテストしますか?
Spring GraphQLは、表現力豊かで読みやすいテストを書くためにGraphQlTesterを提供します。
@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を介してクライアントにリアルタイムデータをプッシュすることを可能にします。
# 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));
}
// 記事作成時に呼び出されるメソッド
public void publishArticle(Article article) {
articleSink.tryEmitNext(article);
}
// コメント追加時に呼び出されるメソッド
public void publishComment(Comment comment) {
commentSink.tryEmitNext(comment);
}
}WebSocket構成には追加の依存関係が必要です:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
}@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}GraphQLセキュリティのベストプラクティスは何ですか?
GraphQLセキュリティには、サービス拒否攻撃や不正アクセスに対する複数の保護層が必要です。
@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();
}
}@Controller
public class SecuredArticleController {
private final ArticleRepository articleRepository;
public SecuredArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
// 認証されたユーザーに制限されたアクセス
@QueryMapping
@PreAuthorize("isAuthenticated()")
public List<Article> 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();
}
}複雑なクエリに対する保護:
# application.yml
spring:
graphql:
schema:
introspection:
# 本番環境ではイントロスペクションを無効化
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(
"クエリが複雑すぎます: " + 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を過度に複雑なクエリから保護する
タグ
共有
関連記事

Spring Boot 面接: トランザクション伝播の解説
Spring Boot のトランザクション伝播をマスターします。REQUIRED、REQUIRES_NEW、NESTED など。コード例と典型的な落とし穴を含む 12 の面接質問。

2026年Spring Data JPAのN+1問題解決法:Fetch JoinとEntityGraph
Spring Data JPAにおけるN+1問題の検出と修正のための完全ガイド。Fetch join、@EntityGraph、batch fetching、クエリパフォーマンス戦略を解説します。

Spring Boot面接質問30選: Java開発者のための完全ガイド
auto-configuration、スターター、Spring Data JPA、セキュリティ、テストを網羅した30問でSpring Boot面接に備えます。