สัมภาษณ์ Spring GraphQL: Resolver, DataLoader และวิธีแก้ปัญหา N+1

เตรียมตัวสำหรับการสัมภาษณ์ Spring GraphQL ด้วยคู่มือที่ครบถ้วนนี้ Resolver, DataLoader, การจัดการปัญหา N+1, mutation และแนวปฏิบัติที่ดีที่สุดสำหรับคำถามทางเทคนิค

การสัมภาษณ์ทางเทคนิค Spring GraphQL พร้อม resolver และ DataLoader

Spring for GraphQL ทำให้การรวม GraphQL ในแอปพลิเคชัน Spring Boot ง่ายขึ้น เทคโนโลยีนี้กลายเป็นสิ่งจำเป็นสำหรับ API สมัยใหม่ และคำถามสัมภาษณ์เกี่ยวกับหัวข้อนี้พบได้บ่อยมากขึ้น คู่มือนี้ครอบคลุมแนวคิดหลัก: resolver, DataLoader, ปัญหา N+1 และรูปแบบขั้นสูง

จุดเน้นการสัมภาษณ์

ผู้สรรหาทดสอบความเข้าใจเกี่ยวกับปัญหา N+1 และการใช้ DataLoader เป็นพิเศษ สองหัวข้อนี้คิดเป็น 60% ของคำถามในการสัมภาษณ์ Spring GraphQL

Spring for GraphQL คืออะไร?

Spring for GraphQL เป็นผู้สืบทอดอย่างเป็นทางการของ GraphQL Java Spring โดยถูกรวมเข้ากับ Spring framework โดยตรงตั้งแต่เวอร์ชัน 2.7 การรวมนี้นำมาซึ่งข้อดีหลายประการ: การรองรับ annotation ของ Spring, การรวมกับ Spring Security และการใช้ WebFlux หรือ MVC อย่างโปร่งใส

การกำหนดค่าพื้นฐานต้องการเพียง dependency spring-boot-starter-graphql และ schema GraphQL

build.gradle.ktsjava
dependencies {
    // Spring GraphQL starter พร้อม Spring MVC
    implementation("org.springframework.boot:spring-boot-starter-graphql")
    implementation("org.springframework.boot:spring-boot-starter-web")

    // สำหรับการทดสอบ GraphQL
    testImplementation("org.springframework.graphql:spring-graphql-test")
}

Schema 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 จะสร้างเอกสารและตรวจสอบ query ที่ฝั่งเซิร์ฟเวอร์โดยอัตโนมัติ

Resolver ของ Spring GraphQL ทำงานอย่างไร?

Resolver เป็นหัวใจของการดำเนินการ GraphQL แต่ละฟิลด์ schema สามารถมี resolver เฉพาะได้ Spring ใช้ annotation @QueryMapping สำหรับ query ราก และ @SchemaMapping สำหรับความสัมพันธ์

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

    // Resolver สำหรับ Query.article(id)
    @QueryMapping
    public Article article(@Argument Long id) {
        return articleRepository.findById(id)
            .orElseThrow(() -> new ArticleNotFoundException(id));
    }

    // Resolver สำหรับ 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();
    }

    // Resolver สำหรับ Article.author - ถูกเรียกสำหรับแต่ละบทความ
    @SchemaMapping(typeName = "Article", field = "author")
    public Author author(Article article) {
        return authorRepository.findById(article.getAuthorId())
            .orElseThrow(() -> new AuthorNotFoundException(article.getAuthorId()));
    }
}

Resolver author ถูกดำเนินการสำหรับแต่ละบทความที่ส่งคืน สถาปัตยกรรมที่ยืดหยุ่นนี้โหลดข้อมูลตามต้องการแต่ทำให้เกิดปัญหา N+1

กับดักการสัมภาษณ์แบบคลาสสิก

ผู้สมัครที่ใช้ resolver @SchemaMapping โดยไม่กล่าวถึงปัญหา N+1 แสดงให้เห็นถึงความเข้าใจ GraphQL ที่ไม่สมบูรณ์ ผู้สรรหาคาดหวังการวิเคราะห์นี้อย่างเป็นระบบ

ปัญหา N+1 ใน GraphQL คืออะไร?

ปัญหา N+1 เกิดขึ้นเมื่อ query GraphQL กระตุ้น query เพิ่มเติม N รายการเพื่อโหลดความสัมพันธ์ รูปแบบที่ทำลายล้างนี้เกิดขึ้นอย่างเป็นระบบกับ resolver พื้นฐาน

พิจารณา query ที่ดึงบทความ 50 รายการพร้อมผู้เขียน:

graphql
# Query GraphQL
query {
    articles(size: 50) {
        id
        title
        author {
            name
        }
    }
}

ด้วย resolver ก่อนหน้า query นี้ดำเนินการ:

  • 1 query สำหรับบทความ 50 รายการ
  • 50 query เพื่อโหลดผู้เขียนแต่ละคนแยกกัน
sql
-- Query 1: ดึงบทความ
SELECT * FROM articles LIMIT 50

-- Query 2-51: ผู้เขียนแต่ละคนแยกกัน
SELECT * FROM authors WHERE id = 1
SELECT * FROM authors WHERE id = 2
SELECT * FROM authors WHERE id = 3
-- ... อีก 47 query

การเพิ่มจำนวน query นี้ลดประสิทธิภาพอย่างมาก endpoint ที่ตอบสนองใน 50ms อาจกระโดดเป็น 2 วินาทีด้วย N+1

DataLoader ทำงานอย่างไร?

DataLoader จัดกลุ่ม query แต่ละรายการเป็นคำขอแบบกลุ่ม แทนที่จะโหลดผู้เขียนแต่ละคนแยกกัน DataLoader จะรวบรวม ID ทั้งหมดที่ร้องขอและดำเนินการ query เพียงรายการเดียว

AuthorBatchLoader.javajava
@Component
public class AuthorBatchLoader {

    private final AuthorRepository authorRepository;

    public AuthorBatchLoader(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    // วิธีการโหลดแบบกลุ่ม
    public Mono<Map<Long, Author>> loadAuthors(Set<Long> authorIds) {
        // Query เดียวสำหรับผู้เขียนทั้งหมด
        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 ในการกำหนดค่าเฉพาะ

GraphQLConfig.javajava
@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));
        };
    }
}

Resolver ที่ปรับเปลี่ยนแล้วใช้ DataLoader แทนการเข้าถึงโดยตรง:

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

    // Resolver ที่ปรับให้เหมาะสมด้วย DataLoader
    @SchemaMapping(typeName = "Article", field = "author")
    public CompletableFuture<Author> author(
            Article article,
            DataLoader<Long, Author> authorDataLoader) {

        // DataLoader จัดกลุ่มการเรียกโดยอัตโนมัติ
        return authorDataLoader.load(article.getAuthorId());
    }
}

ด้วยวิธีการนี้ query เดียวกันสำหรับบทความ 50 รายการดำเนินการเพียง 2 query SQL:

sql
-- Query 1: ดึงบทความ
SELECT * FROM articles LIMIT 50

-- Query 2: ผู้เขียนทั้งหมดในครั้งเดียว
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ..., 50)

พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

วิธีการใช้งาน DataLoader พร้อม Context?

DataLoader ขั้นสูงบางครั้งต้องการ context เพิ่มเติม เช่น ตัวระบุ tenant สำหรับแอปพลิเคชันหลาย tenant หรือตัวกรองความปลอดภัย

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) {

        // ดึง context GraphQL
        GraphQLContext context = env.getContext();
        String tenantId = context.get("tenantId");

        // Query ที่กรองตาม tenant
        List<Author> authors = authorRepository
            .findAllByIdInAndTenantId(authorIds, tenantId);

        Map<Long, Author> authorMap = authors.stream()
            .collect(Collectors.toMap(Author::getId, Function.identity()));

        return Mono.just(authorMap);
    }
}

การกำหนดค่า context ทำผ่าน WebGraphQL interceptor:

TenantInterceptor.javajava
@Component
public class TenantInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(
            WebGraphQlRequest request,
            Chain chain) {

        // ดึง tenant จาก headers
        String tenantId = request.getHeaders()
            .getFirst("X-Tenant-ID");

        // เพิ่มใน context GraphQL
        request.configureExecutionInput((input, builder) ->
            builder.graphQLContext(ctx ->
                ctx.put("tenantId", tenantId)
            ).build()
        );

        return chain.next(request);
    }
}

ความแตกต่างระหว่าง @QueryMapping และ @SchemaMapping คืออะไร?

คำถามสัมภาษณ์แบบคลาสสิกนี้ตรวจสอบความเข้าใจในลำดับชั้นของ resolver

| Annotation | การใช้งาน | เทียบเท่า | |------------|-----------|-----------| | @QueryMapping | ฟิลด์รากของประเภท Query | @SchemaMapping(typeName = "Query") | | @MutationMapping | ฟิลด์รากของประเภท Mutation | @SchemaMapping(typeName = "Mutation") | | @SubscriptionMapping | Subscription แบบเรียลไทม์ | @SchemaMapping(typeName = "Subscription") | | @SchemaMapping | ทุกฟิลด์ของทุกประเภท | รูปแบบทั่วไป |

EquivalenceDemo.javajava
@Controller
public class EquivalenceDemo {

    // การประกาศสองแบบนี้เทียบเท่ากัน
    @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());
    }
}

วิธีการจัดการ Mutation พร้อมการตรวจสอบ?

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

Controller ใช้ @Valid เพื่อกระตุ้นการตรวจสอบ:

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) {
        // การตรวจสอบดำเนินการโดยอัตโนมัติ
        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 = "จำเป็นต้องมีชื่อเรื่อง")
    @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

วิธีปรับ Query ให้เหมาะสมด้วย @BatchMapping?

Annotation @BatchMapping ทำให้การสร้าง DataLoader ในตัว controller ง่ายขึ้นโดยตรง วิธีการนี้หลีกเลี่ยงการกำหนดค่า BatchLoaderRegistry อย่างชัดเจน

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

    // BatchMapping สำหรับผู้เขียน - ประมวลผลบทความทั้งหมดในครั้งเดียว
    @BatchMapping
    public Map<Article, Author> author(List<Article> articles) {
        // รวบรวม ID ผู้เขียนที่ไม่ซ้ำกัน
        Set<Long> authorIds = articles.stream()
            .map(Article::getAuthorId)
            .collect(Collectors.toSet());

        // Query เดียวสำหรับผู้เขียนทั้งหมด
        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 ด้วยตนเอง โดยยังคงรับประกันประสิทธิภาพเหมือนกัน

วิธีทดสอบ Resolver GraphQL?

Spring GraphQL ให้ GraphQlTester สำหรับการเขียนการทดสอบที่แสดงออกและอ่านง่าย

ArticleControllerTest.javajava
@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();
    }
}

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

วิธีจัดการ Subscription แบบเรียลไทม์?

Subscription ช่วยให้สามารถส่งข้อมูลแบบเรียลไทม์ไปยังไคลเอนต์ผ่าน WebSocket

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

    // เมธอดที่ถูกเรียกเมื่อสร้างบทความ
    public void publishArticle(Article article) {
        articleSink.tryEmitNext(article);
    }

    // เมธอดที่ถูกเรียกเมื่อเพิ่มความคิดเห็น
    public void publishComment(Comment comment) {
        commentSink.tryEmitNext(comment);
    }
}

การกำหนดค่า WebSocket ต้องใช้ dependency เพิ่มเติม:

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 คืออะไร?

ความปลอดภัยของ GraphQL ต้องมีการป้องกันหลายชั้นต่อการโจมตีปฏิเสธบริการและการเข้าถึงโดยไม่ได้รับอนุญาต

SecurityConfig.javajava
@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                // Endpoint GraphQL ที่ได้รับการป้องกัน
                .requestMatchers("/graphql").authenticated()
                // GraphiQL เข้าถึงได้เฉพาะใน dev เท่านั้น
                .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;
    }

    // จำกัดการเข้าถึงสำหรับผู้ใช้ที่ตรวจสอบสิทธิ์แล้ว
    @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();
    }
}

การป้องกันต่อ query ที่ซับซ้อน:

yaml
# application.yml
spring:
  graphql:
    schema:
      introspection:
        # ปิดใช้งาน introspection ในโปรดักชัน
        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(
                "Query ซับซ้อนเกินไป: " + complexity + " (max: " + MAX_COMPLEXITY + ")"
            );
        }

        return super.beginExecution(parameters, state);
    }
}

บทสรุป

Spring for GraphQL ให้การรวม GraphQL ที่หรูหราเข้ากับระบบนิเวศ Spring การเชี่ยวชาญ resolver, DataLoader และการจัดการปัญหา N+1 เป็นรากฐานของความรู้ที่คาดหวังในการสัมภาษณ์

รายการตรวจสอบการสัมภาษณ์ Spring GraphQL:

  • ✅ อธิบายบทบาทของ annotation @QueryMapping, @MutationMapping และ @SchemaMapping
  • ✅ อธิบายปัญหา N+1 และผลกระทบต่อประสิทธิภาพของ GraphQL
  • ✅ ใช้งาน DataLoader ด้วย @BatchMapping หรือ BatchLoaderRegistry
  • ✅ ตรวจสอบอินพุต mutation ด้วย Bean Validation
  • ✅ รักษาความปลอดภัยของ resolver ด้วย @PreAuthorize และ Spring Security
  • ✅ ทดสอบ resolver ด้วย GraphQlTester
  • ✅ กำหนดค่า subscription สำหรับฟังก์ชันแบบเรียลไทม์
  • ✅ ปกป้อง API จาก query ที่ซับซ้อนเกินไป

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง