Kiểm soát cache thông qua persisted queries
GraphQL thường hoạt động qua POST, thực thi tất cả các queries với một endpoint duy nhất và truyền tham số qua phần thân của yêu cầu. URL của endpoint duy nhất đó sẽ tạo ra các phản hồi khác nhau, có nghĩa là nó không thể được lưu cache (ít nhất là không dùng URL làm định danh).
Vì vậy, cách chuẩn để hỗ trợ caching trong GraphQL là ở tầng client, thông qua Apollo client và các thư viện tương tự, lưu cache các đối tượng trả về một cách độc lập với nhau, nhận dạng chúng bằng ID toàn cục duy nhất.
(Ngược lại, khi lưu cache ở phía server, chúng ta thường dùng URL làm định danh và lưu cache dữ liệu cho tất cả các thực thể trong phản hồi cùng một lúc.)
Nhưng giải pháp này có một số nhược điểm:
- Ứng dụng phải chạy nhiều JavaScript hơn ở phía client. Truy cập trang web qua điện thoại di động cấp thấp sẽ bị giảm hiệu suất
- Ứng dụng trở nên phức tạp hơn, với nhiều bộ phận chuyển động hơn, vì giờ chúng ta cũng phải lo triển khai tầng caching
- Không phải ai cũng hiểu JavaScript (ví dụ: trang web có thể được viết bằng PHP), nhưng giờ xử lý JS cũng trở thành một trách nhiệm
Một giải pháp tốt hơn nhiều là sử dụng HTTP caching. Hãy cùng xem các điều kiện tiên quyết cần thiết để điều này hoạt động.
Truy cập GraphQL qua GET
Sử dụng HTTP caching có nghĩa là chúng ta sẽ lưu cache phản hồi GraphQL bằng URL làm định danh. Điều này có 2 hệ quả:
- Chúng ta phải truy cập endpoint duy nhất của GraphQL qua
GET - Chúng ta phải truyền query và các biến dưới dạng tham số URL
Khi đó, nếu endpoint duy nhất là /graphql, thao tác GET có thể được thực thi với URL /graphql?query=...&variables=....
Điều này áp dụng cho việc lấy dữ liệu từ server (qua thao tác query). Để thay đổi dữ liệu (qua thao tác mutation), chúng ta vẫn phải dùng POST. Không có vấn đề gì ở đây, vì các mutations luôn được thực thi mới; chúng ta không thể lưu cache kết quả của một mutation, vì vậy chúng ta cũng sẽ không dùng HTTP caching với nó.
Cách tiếp cận này hoạt động được (và thậm chí được gợi ý trên trang chính thức), nhưng có một số điều cần lưu ý.
Viết mã GraphQL queries qua tham số URL
Một GraphQL query thường trải dài trên nhiều dòng. Ví dụ:
{
posts {
id
title
}
}Tuy nhiên, chúng ta không thể nhập chuỗi nhiều dòng này trực tiếp vào tham số URL.
Giải pháp là mã hóa nó. Ví dụ, client GraphiQL sẽ mã hóa query trên như sau:
%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D
Được rồi, điều này hoạt động. Nhưng trông không đẹp lắm phải không? Ai có thể hiểu được query đó?
Một trong những ưu điểm của GraphQL là các queries của nó rất dễ nắm bắt. Với một chút luyện tập, khi thấy query, chúng ta hiểu ngay lập tức. Nhưng một khi đã được mã hóa, tất cả điều đó biến mất, và chỉ máy móc mới có thể hiểu được; con người bị loại khỏi phương trình.
Một giải pháp khác có thể là thay thế tất cả các dấu xuống dòng trong query bằng một khoảng trắng, điều này hoạt động được vì dấu xuống dòng không thêm ý nghĩa ngữ nghĩa vào query. Khi đó, query trên có thể được biểu diễn như sau:
?query={ posts { id title } }
Điều này hoạt động tốt với các queries đơn giản. Nhưng nếu bạn có một query thực sự dài, mở và đóng nhiều { }, và thêm các đối số trường và chỉ thị, thì ngày càng khó hiểu.
Ví dụ, query này:
{
posts(limit:5) {
id
title @titleCase
excerpt @default(
value:"No title",
condition:IS_EMPTY
)
author {
name
}
tags {
id
name
}
comments(
limit:3,
order:"date|DESC"
) {
id
date(format:"d/m/Y")
author {
name
}
content
}
}
}Sẽ trở thành query một dòng này:
{ posts(limit:5) { id title @titleCase excerpt @default(value:"No title", condition:IS_EMPTY) author { name } tags { id name } comments(limit:3, order:"date|DESC") { id date(format:"d/m/Y") author { name } content } } }
Một lần nữa, việc thực thi query sẽ hoạt động, nhưng chúng ta sẽ không biết mình đang thực thi cái gì.
Và nếu query cũng chứa các fragments, thì hãy quên đi hoàn toàn, không có cách nào chúng ta có thể hiểu được nó.
Persisted queries đến giải cứu
Nếu truyền query trong URL không thỏa mãn, chúng ta có lựa chọn nào khác? Đó là không truyền query trong URL!
Đây là cách tiếp cận được gọi là "persisted query": Chúng ta lưu query trên server và dùng một định danh (chẳng hạn như một ID số, hoặc một chuỗi duy nhất được tạo ra bằng cách áp dụng thuật toán băm với query làm đầu vào) để truy xuất nó. Cuối cùng, chúng ta truyền định danh này dưới dạng tham số URL, thay vì query.
Ví dụ, query có thể được nhận dạng bằng ID 2908 (hoặc một hash như "50ac3e81"), và sau đó chúng ta thực thi thao tác GET với URL /graphql?id=2908. Server GraphQL sau đó sẽ truy xuất query tương ứng với ID này, thực thi nó và trả về kết quả.
Gato GraphQL làm cho điều này thậm chí đơn giản hơn: một persisted query được triển khai như một loại bài đăng tùy chỉnh, vì vậy chúng ta có thể tạo và xuất bản nó như bất kỳ bài đăng thông thường nào, và slug chúng ta chọn (theo mặc định dựa trên tiêu đề chúng ta nhập) sẽ trở thành định danh của nó. Persisted queries làm cho việc triển khai HTTP caching trở nên đơn giản.
Tính toán giá trị max-age
HTTP Caching hoạt động bằng cách gửi header Cache-Control trong phản hồi, với giá trị max-age chỉ ra khoảng thời gian phản hồi phải được lưu cache, hoặc no-store để chỉ ra không lưu cache.
Server GraphQL sẽ tính toán giá trị max-age cho query như thế nào, xem xét rằng các trường khác nhau có thể có các giá trị max-age khác nhau?
Câu trả lời là: Lấy giá trị max-age cho tất cả các trường được yêu cầu trong query, và tìm ra giá trị thấp nhất. Đó sẽ là max-age của phản hồi.
Ví dụ, giả sử chúng ta có một thực thể thuộc loại User. Theo hành vi được gán cho thực thể này, chúng ta có thể chỉ định trường tương ứng có thể được lưu cache trong bao lâu:
🛠 ID của nó sẽ không bao giờ thay đổi ⇒ Chúng ta gán cho trường id một max-age là 1 năm
🛠 URL của nó sẽ được cập nhật rất hiếm khi (nếu có) ⇒ Chúng ta gán cho trường url một max-age là 1 ngày
🛠 Tên của người dùng có thể thay đổi đôi khi (ví dụ: để thêm trạng thái, hoặc để nói "Milton (đeo khẩu trang)") ⇒ Chúng ta gán cho trường name một max-age là 1 giờ
🛠 Karma của người dùng trên trang web có thể thay đổi bất cứ lúc nào (ví dụ: sau khi ai đó bình chọn bình luận của họ) ⇒ Chúng ta gán cho trường karma một max-age là 1 phút
🛠 Nếu truy vấn dữ liệu từ người dùng đang đăng nhập, thì phản hồi không thể được lưu cache (bất kể trường nào chúng ta đang lấy) ⇒ max-age phải là no-store
Kết quả là, phản hồi cho các GraphQL queries sau sẽ có các giá trị max-age như sau (trong ví dụ này, chúng ta bỏ qua max-age cho trường Root.users, nhưng trong thực tế nó cũng sẽ được tính đến):
| Query | Giá trị max-age |
|---|---|
| 1 năm |
| 1 ngày |
| 1 giờ |
| 1 phút |
| no-store (không lưu cache) |
Tạo Cache Control List
Sau khi đã xác định max-age cho từng trường, chúng ta nhập thông tin này thông qua một Cache Control List:

Gato GraphQL sau đó sẽ tự động tính toán giá trị max-age của phản hồi và gửi lại dưới dạng header HTTP Cache-Control.