Viết query đầu tiên
Kết nối query GraphQL với resolver trong Spring for GraphQL và đọc execution result đầu tiên.
Một query GraphQL chỉ trả về dữ liệu khi ba phần khớp nhau: schema khai báo field, resolver cung cấp giá trị và engine thực thi query theo selection mà client yêu cầu. Bài này nối các phần đó bằng một field scalar đơn giản, sau đó mở rộng sang object và list.
Giả định của ví dụ
Các trang lân cận hiện mới là khung nội dung, vì vậy SDL dưới đây là schema tối thiểu để minh họa. Tên package com.example.graphql, port 8080 và endpoint POST /graphql cũng là giả định. Khi schema thực tế khác, hãy đổi đồng thời tên field, kiểu trả về và query kiểm tra.
Mục lục
- Từ schema đến execution
- Schema tối thiểu
- Đặt class và package
- Tạo resolver cho field đơn giản
- Gửi query và đọc kết quả
- Chạy và kiểm tra local
- Mở rộng sang object và list
- Lỗi thường gặp
- Bước tiếp theo
Từ schema đến execution
Schema là contract của API. Nó nói query root có field nào và mỗi field trả về kiểu gì. Resolver là phần code tính hoặc lấy giá trị cho một field. Trong Spring for GraphQL, một method trong @Controller được đăng ký thành handler của field thông qua annotation mapping.
Khi client gửi query, GraphQL xử lý theo chuỗi sau:
Client gửi query
│
▼
Schema kiểm tra field và kiểu dữ liệu
│
▼
Spring for GraphQL tìm handler của field
│
▼
Method resolver trả về giá trị Java
│
▼
GraphQL chọn field được yêu cầu và tạo JSON responseVới query root hello, có thể hình dung như sau:
- Schema khai báo
hello: String!. @QueryMappingnối fieldhellovới method Java tương ứng.- Method trả về một
String. - Execution engine đặt chuỗi đó vào
data.hello.
Annotation mapping không tự tạo field mới trong schema. Nếu schema không có hello, việc thêm method hello() vẫn không làm query hợp lệ.
Schema tối thiểu
Đặt schema minh họa tại src/main/resources/graphql/schema.graphqls:
type Query {
hello: String!
}Khoảng trắng đầu dòng trong SDL không có ý nghĩa; phần quan trọng là Query, hello và String!. Dấu ! nghĩa là field này không được trả về null. Vì vậy resolver phải luôn trả về một chuỗi hợp lệ.
Spring Boot thường quét schema trong classpath:graphql/**/. Nếu bài Thêm dependency và cấu hình đã đổi spring.graphql.schema.locations, hãy đặt file theo cấu hình thực tế thay vì chỉ dựa vào đường dẫn minh họa này.
Đặt class và package
Một cấu trúc nhỏ có thể bắt đầu như sau:
src/
└── main/
├── java/
│ └── com/example/graphql/
│ ├── GraphqlApplication.java
│ └── GreetingController.java
└── resources/
└── graphql/
└── schema.graphqlsGreetingController nằm trong package con của class có @SpringBootApplication để component scan phát hiện bean. Nếu package ứng dụng thực tế là com.acme.catalog, hãy thay toàn bộ com.example.graphql bằng package đó hoặc cấu hình component scan phù hợp.
Trong Spring for GraphQL, dùng @Controller cho controller GraphQL. Endpoint HTTP /graphql đã nhận request; method bên dưới không phải một REST endpoint riêng và không cần @GetMapping.
Tạo resolver cho field đơn giản
Resolver mặc định theo tên method
Tạo file src/main/java/com/example/graphql/GreetingController.java:
package com.example.graphql;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
public class GreetingController {
@QueryMapping
public String hello() {
return "Xin chào từ Spring for GraphQL!";
}
}@QueryMapping là shortcut cho mapping vào root type Query. Khi không chỉ rõ tên, Spring dùng tên method làm tên field. Vì vậy hello() khớp với hello trong schema và Java String được chuyển thành giá trị GraphQL kiểu String. Dấu ! là cam kết resolver không trả về null, nên method phải giữ cam kết đó.
Giữ method này nhỏ giúp nhìn rõ trách nhiệm của resolver. Trong ứng dụng thật, method thường gọi service hoặc repository thay vì giữ dữ liệu trực tiếp. Phần @QueryMapping, @MutationMapping, @SubscriptionMapping sẽ đi sâu hơn vào các loại operation mapping.
Mapping tường minh khi tên khác
Tên method không bắt buộc phải giống tên field nếu mapping được chỉ rõ. Ví dụ sau thay thế method hello() ở trên:
package com.example.graphql;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
@Controller
public class GreetingController {
@SchemaMapping(typeName = "Query", field = "hello")
public String greeting() {
return "Xin chào từ Spring for GraphQL!";
}
}Ở đây method là greeting() nhưng field GraphQL vẫn là hello. Không cần đặt cả @QueryMapping và @SchemaMapping lên cùng một method. Với query root đơn giản, ưu tiên @QueryMapping; dùng mapping tường minh khi tên Java và tên schema cần tách biệt.
Gửi query và đọc kết quả
Request GraphQL
Query GraphQL chỉ yêu cầu field mà client muốn đọc:
query FirstHello {
hello
}Nếu gửi qua HTTP, body là một GraphQL request JSON. Với giả định endpoint mặc định, request có thể là:
{
"query": "query FirstHello { hello }",
"operationName": "FirstHello"
}operationName hữu ích khi một document có nhiều operation. Với document chỉ có một operation, trường này có thể bỏ qua.
Response thành công
Resolver trả về chuỗi nên execution result có dạng:
{
"data": {
"hello": "Xin chào từ Spring for GraphQL!"
}
}data phản ánh selection set của query. Nếu query chỉ chọn hello, response không tự thêm các field khác. Đây là điểm khác với một endpoint REST thường trả về một DTO cố định.
Khi response có errors
GraphQL có thể trả errors khi query không hợp lệ hoặc resolver gặp lỗi. Ví dụ client gõ sai tên field:
query FirstHello {
helo
}Hình dạng response minh họa:
{
"errors": [
{
"message": "Cannot query field \"helo\" on type \"Query\"."
}
]
}Thông báo và dấu câu có thể khác theo phiên bản graphql-java. Lỗi validation này xảy ra trước khi hello() được gọi, vì helo không có trong schema. Với lỗi phát sinh trong lúc resolve, response có thể có cả data một phần và errors; field lỗi thường trở thành null nếu schema cho phép.
Chạy và kiểm tra local
Bài này giả định project đã có dependency và cấu hình theo Thêm dependency và cấu hình. Trong project Spring Boot thật, chọn lệnh tương ứng với build tool:
# Maven Wrapper
./mvnw spring-boot:run
# Hoặc Gradle Wrapper
./gradlew bootRunSau khi ứng dụng khởi động:
- Kiểm tra log không có lỗi đọc
schema.graphqls. - Mở GraphiQL nếu ứng dụng đã bật giao diện này, thường tại
http://localhost:8080/graphiql. - Chạy query
FirstHellotrong GraphiQL và đối chiếudata.hellovới giá trị method trả về. - Nếu không dùng GraphiQL, gửi request bằng
curl:
curl http://localhost:8080/graphql \
-H 'Content-Type: application/json' \
--data '{"query":"query FirstHello { hello }","operationName":"FirstHello"}'Đường dẫn và port chỉ đúng khi ứng dụng dùng cấu hình giả định ở đầu bài. Với Spring Boot 3.5+, cấu hình endpoint bằng spring.graphql.http.path; với Spring Boot 3.4 trở xuống, dùng spring.graphql.path. Nếu đã đổi key tương ứng, server.port hoặc bật bảo mật, hãy dùng giá trị thực tế. Xem GraphiQL và công cụ khám phá API để tìm hiểu thêm về schema documentation và execution result.
Mở rộng sang object và list
Field scalar hello là bước đầu để kiểm tra mapping. Dữ liệu ứng dụng thường là object hoặc danh sách object. Khi đó schema mô tả cả kiểu object, còn query phải chọn các subfield của object.
Schema cho object và list
Mở rộng schema.graphqls thành phiên bản sau:
type Query {
hello: String!
featuredBook: Book
books: [Book!]!
}
type Book {
id: ID!
title: String!
author: String!
publishedYear: Int!
}Trong schema này:
featuredBook: Booklà một object có thểnull, chẳng hạn khi chưa có sách nổi bật.[Book!]!là một list khôngnull, và mỗi phần tử trong list cũng khôngnull.- Các field
id,title,authorvàpublishedYearđều bắt buộc có giá trị.
Đây là contract của ví dụ, không phải yêu cầu mọi domain đều dùng các field này. Hãy đổi Book và các field theo schema thực tế của bạn.
Dữ liệu in-memory và resolver
Tạo model minh họa tại src/main/java/com/example/graphql/book/Book.java:
package com.example.graphql.book;
public class Book {
private final String id;
private final String title;
private final String author;
private final int publishedYear;
public Book(String id, String title, String author, int publishedYear) {
this.id = id;
this.title = title;
this.author = author;
this.publishedYear = publishedYear;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public int getPublishedYear() {
return publishedYear;
}
}Sau đó tạo src/main/java/com/example/graphql/book/BookController.java:
package com.example.graphql.book;
import java.util.List;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
public class BookController {
private final List<Book> books = List.of(
new Book("book-1", "Effective Java", "Joshua Bloch", 2018),
new Book("book-2", "Designing Data-Intensive Applications", "Martin Kleppmann", 2017)
);
@QueryMapping
public Book featuredBook() {
return books.isEmpty() ? null : books.get(0);
}
@QueryMapping
public List<Book> books() {
return books;
}
}Tên method featuredBook() và books() lần lượt ánh xạ vào hai field cùng tên trong Query. Getter của Book cung cấp các property id, title, author và publishedYear cho việc đọc subfield. List.of(...) chỉ là dữ liệu in-memory để học flow; mỗi lần khởi động ứng dụng, dữ liệu lại được tạo từ đầu.
Trong ứng dụng thật, thay list bằng service hoặc repository. Kiểu trả về của controller vẫn cần phù hợp với schema: Book cho một object và List<Book> cho list. Nếu repository có thể trả null, cần quyết định rõ nên đổi schema thành nullable hay chuẩn hóa giá trị trước khi trả về.
Query và response cho object
Object và list phải có selection set. Query sau đọc cả object nổi bật và danh sách sách:
query Books {
featuredBook {
id
title
author
publishedYear
}
books {
id
title
author
publishedYear
}
}Với dữ liệu in-memory ở trên, response tương ứng là:
{
"data": {
"featuredBook": {
"id": "book-1",
"title": "Effective Java",
"author": "Joshua Bloch",
"publishedYear": 2018
},
"books": [
{
"id": "book-1",
"title": "Effective Java",
"author": "Joshua Bloch",
"publishedYear": 2018
},
{
"id": "book-2",
"title": "Designing Data-Intensive Applications",
"author": "Martin Kleppmann",
"publishedYear": 2017
}
]
}
}Nếu chỉ viết books mà không có { id title }, GraphQL sẽ báo object field cần subfield. Ngược lại, scalar như hello không được viết kèm subfield.
Lỗi thường gặp
Lỗi mapping thường gặp
Nếu query đã hợp lệ theo schema nhưng method không được gọi, kiểm tra lần lượt @Controller, package component scan, annotation @QueryMapping và tên field. Thông báo cụ thể có thể khác theo phiên bản Spring for GraphQL, nhưng nguyên nhân thường nằm ở bước đăng ký handler chứ không phải ở JSON response.
Field không tồn tại hoặc sai tên
GraphQL phân biệt chữ hoa, chữ thường và không tự đoán tên gần giống. Các cặp sau là khác nhau:
hellovàHello.publishedYearvàpublished_year.featuredBookvàfeatured_book.
Lỗi Cannot query field ... on type "Query" thường cho biết field chưa được khai báo trong schema đang được load, query gõ sai, hoặc schema thực tế không phải file bạn vừa sửa. Hãy kiểm tra schema trong GraphiQL trước khi debug Java resolver.
Resolver không được đăng ký
Các nguyên nhân phổ biến:
- Class thiếu
@Controller. - Method thiếu
@QueryMapping, hoặc dùng annotation của loại operation khác. - Package controller nằm ngoài vùng component scan.
- Method có tên khác field nhưng không dùng mapping tường minh.
- File SDL nằm ngoài
spring.graphql.schema.locations.
Sửa từng điểm một rồi khởi động lại ứng dụng. Một method Java đúng kiểu nhưng không được Spring đăng ký vẫn không thể resolve field.
Nullability không khớp
Nullability là một phần của contract
String! và [Book!]! không cho phép giá trị null. Nếu resolver trả null, GraphQL sẽ thêm lỗi vào response và có thể làm data bị null theo quy tắc null bubbling. Nếu dữ liệu hợp lệ có thể vắng mặt, khai báo field nullable như featuredBook: Book hoặc xử lý trường hợp thiếu dữ liệu trước khi trả về.
Đừng chỉ sửa lỗi bằng cách đổi mọi field thành nullable. Hãy chọn nullability dựa trên domain: books thường có thể là list rỗng nhưng vẫn là list hợp lệ, còn featuredBook có thể không tồn tại. Sau đó kiểm tra lại Java return type, query và response cùng lúc.
Bước tiếp theo
Bạn đã nối một field trong Query với controller method và đọc execution result đầu tiên. Từ đây có thể đi tiếp theo các hướng:
- GraphiQL và công cụ khám phá API để chạy query, xem documentation và thử selection set.
- @QueryMapping, @MutationMapping, @SubscriptionMapping để phân biệt các root operation.
- @SchemaMapping và nested field để resolve field lồng trong
Bookhoặc object khác. - Binding arguments và input để truyền argument và variables thay vì trả toàn bộ dữ liệu mẫu.