Khi nói về tiêu chuẩn cho thiết kế API, cái tên đầu tiên mà mọi người nghĩ tới là REST(Representational State Transfer), một tiêu chuẩn truy cập dữ liệu trên máy chủ dựa trên các URL. Trong hơn một thập kỷ qua REST cung cấp những tiện tích tuyệt vời như stateless server, quyền truy cập vào các resources(tài nguyên), vì những lý do đấy mà nó được ứng dụng rộng rãi trên rất nhiều ứng dụng trong suốt thời gian qua. Tuy nhiên, phổ biến là vậy, nhưng REST không hề toàn năng, khi nhu cầu về dữ liệu ở phía client tăng lên, nó bắt đầu bộc lộ các điểm yếu như sau:
- Sự gia tăng các thiết bị di động đòi hỏi dữ liệu linh hoạt hơn.
- Nhiều ứng dụng phía client khác nhau: REST gây khó khăn do các API trả về là dữ liệu cố định.
- Việc tạo nhiều endpoint để đáp ứng nhu cầu dữ liệu gây chậm do phải thực hiện gọi quá nhiều lần.
Vì những lý do trên, vào năm 2012 GraphQL được Facebook giới thiệu như một sự thay thế hiện đại cho REST. GraphQL có thể trả về dữ liệu yêu cầu từ client, dựa trên một cấu trúc mẫu.
Bài viết này sẽ giới thiệu GraphQL, các thành phần bên trong nó và cách mà nó hoạt động.
GraphQL là gì ?
GraphQL, viết tắt của Graph Query Language, là một ngôn ngữ truy vấn dữ liệu cho API hiệu quả, mạnh mẽ và mang tính linh hoạt cao.
GraphQL mang lại sự linh hoạt và tốc độ bằng cách cho phép client yêu cầu dữ liệu và trả về chính xác dữ liệu đấy không hơn, không kém. Ta có luồng hoạt động của GraphQL trong HTTP như sau:
- Câu lệnh truy vấn GraphQL không phải là JSON, nó chỉ trông giống dạng JSON của dữ liệu mà bạn muốn lấy. Khi thực hiện POST request, ta đã gửi câu truy vấn GraphQL đến server.
- Server nhận về đối tượng JSON và chuỗi truy vấn trong đó. Tại đây dựa trên cú pháp GraphQL và mô hình đối tượng (trong GraphQL gọi là schema) nó sẽ xác thực truy vấn GraphQL có hợp lệ không.
- Sau đấy, nó thực hiện tương tự như REST gọi đến cơ sở dữ liệu hay các xử lý logic khác để tìm nạp dữ liệu dựa trên yêu cầu của client.
- Server sau khi có dữ liệu sẽ trả về cho client dưới dạng JSON.
So sánh với REST
Multiple endpoint & single endpoint
Sự khác biệt rõ ràng nhất giữa REST và GraphQL chính là số endpoint, với REST ứng với từng hành động ta sẽ nhận về một endpoint, việc mở rộng ứng dụng sẽ trở nên phức tạp khi càng ngày ta càng có nhiều endpoint. Trong khi với GraphQL tất cả mọi thứ đều nằm trên một endpoint duy nhất.
Overfetching & Underfetching
REST
Một vấn đề thường gặp phải với REST là Overfetching và Underfetching. Điều này xảy ra do client sử dụng nhiều endpoint để lấy dữ liệu về, và các endpoint thì trả về cấu trúc dữ liệu cố định. Thế nên tạo ra những khó khăn cho việc thiết kế API làm thế nào để có thể cung cấp cho client chính xác những gì mà client cần.
-
Overfetching (dư thừa dữ liệu): là khi một client lấy được nhiều thông tin hơn so với những gì nó cần. Ví dụ dễ hiểu một màn hình cần hiển thị danh sách các user chỉ với tên của các user đó. Nhưng với REST API, bạn gọi endpoint
/users
và nhận về mảng JSON các dữ liệu của user. Response có thể chứa nhiều thông tin của user như ngày sinh, địa chỉ, giới thiệu …, những thông này là vô dụng vì client chỉ cần hiển thị tên của các user. -
Underfetching (vấn đề n+1): là khi một endpoint cụ thể không cung cấp đủ thông tin yêu cầu cho client. Client phải thực hiện thêm những request khác để lấy thêm dữ liệu mà nó cần. Khi đó vấn đề n+1 xảy ra vì ban đầu client cần lấy về một danh sách các phần tử, nhưng sau đó đối với mỗi phần tử lại phải tạo thêm request để lấy dữ liệu yêu cầu của mỗi phần tử đó. Ví dụ với kịch bản ứng dụng cần hiển thị danh sách user, và mỗi user cần hiển thị thêm 3 followers gần nhất. Đầu tiên ứng dụng cần phải gọi 1 endpoint
/users
và sau đó ứng với mỗi user, chúng ta phải thực hiện một endpoint bổ sung là/users/<id>/followers
.
GraphQL
Việc chỉ sử dụng một endpoint, GraphQL sẽ định nghĩa tất cả các dạng dữ liệu trả về trong Schema, và tuỳ theo yêu cầu từ client mà nó sẽ trả về dữ liệu cho phù hợp. Vì thế nó có thể giải quyết được bài toán thừa thiếu dữ liệu ở REST.
Bên cạnh đó GraphQL còn có những ưu điểm khác so với REST về hiệu suất, khả năng mở rộng, … đồng thời cũng có các hạn chế về upload file hay cache. Xem bảng so sánh đầy đủ dưới đây.
Các khái niệm cốt lõi
GraphQL cho ra mới những khái niệm và tiêu chuẩn hoàn toàn mới mẻ về trao đổi dữ liệu, và nó khá là khó nhằn với những ai đã quen với nền tảng REST API. Ở bài viết này mình sẽ cố gắng mô tả thật đơn giản các thành phần quan trọng trong GraphQL và lấy ví dụ cho từng phần.
GraphQL Document
Phần nội dung trong các yêu cầu mà phía client gửi đến server được gọi là GraphQL document. Ví dụ:
{
human {
name
height
}
}
Chuỗi truy vấn tuân thủ cú pháp trên sẽ được phía server hoặc client tiến hành phân tích cú pháp và xác nhận hợp lệ trước khi thực hiện các yêu cầu.
Thành phần trong GraphQL Document
Trong GraphQL Document, dữ liệu cần fetching nằm trong các khối lệnh, các thành phần trong khối lệnh này bao gồm:
-
Field (trường) là tên của đối tượng dữ liệu hay thuộc tính của đối tượng đó mà ta cần fetching. Trong ví dụ trên thì đấy là đối tượng
human
với hai thuộc tính làname
vàheight
. -
Arguments (tham số) là những ràng buộc được đưa vào để lấy dữ liệu theo mong muốn. Có cấu trúc key-value với key là tên thuộc tính từ trường, và value là giá trị của thuộc tính đó. Các tham số có thể có ở bất kỳ trường nào ngay cả những trường nằm trong một trường khác. Ví dụ ở trên sẽ lấy
human
cóid=1000
, và tronghuman
lấy thuộc tínhheight
cóunit=FOOT
.
-
Operation Type (kiểu thao tác): trong GraphQL có 3 hình thức thao tác dữ liệu là là: query (lấy dữ liệu), mutation (thay đổi dữ liệu) và subscription (lắng nghe thay đổi dữ liệu theo sự kiện). Ở ví dụ trên là query, để lấy dữ liệu là
human
.
Ý nghĩa | GraphQL | REST |
---|---|---|
Lấy dữ liệu | query | GET |
Thay đổi dữ liệu | mutation | POST/PUT/PATCH/DELETE |
Lắng nghe thay đổi dữ liệu | subscription | - |
-
Operation Name (tên thao tác) là tên mà ta sẽ đặt cho các thao tác cần thực hiện, có thể xem nó tương đồng như tên url ở REST. Tên thao tác cần có sự đồng nhất ở phía server và phía client, nếu gọi sai tên thì sẽ sinh lỗi.
-
Variable Definition (biến định nghĩa) khi gửi một yêu cầu đến GraphQL Server, đôi lúc ta sẽ cần gửi các dữ liệu động như
id
,username
, … để có được sự thay đổi về yêu cầu nhưng vẫn giữ nguyên cấu trúc truy vấn, ta sử dụng các biến (khai báo biến bắt đầu bằng$
) . Ở ví dụ trên ta có lệnh$episode: Episode
khai báo biến$episode
có kiểu làEpisode
, sau đấy phần tham số truyền vào sẽ lấyhero
theo biến$episode
.
-
Selection Set (Tập lựa chọn) là tập hợp các trường được yêu cầu trong một thao tác cụ thể, hoặc được lồng trong một trường khác. Trong truy vấn GraphQL, các tập lựa chọn chỉ nằm trong các trường trả về kiểu đối tượng, nó không thể nằm trong các trường trả về kiểu
Int
hayString
.
Fragment
Trong GraphQL sẽ có những lúc ta gặp phải những tình huống mà ta cần phải truy vấn các trường giống nhau trong các câu truy vấn khác nhau. Khi đó ta có thế thấy các câu query của có nhiều các trường bị lặp lại tại nhiều vị trí khác nhau. Lúc này với hỗ trợ của GraphQL, ta có thể tóm chúng lại thành một đơn vị có thể tái sử dụng gọi là Fragment.
- Fragment Name (Tên fragment): Tên riêng biệt cho từng fragment, tên của fragment không được trùng.
- Type Condition (kiểu điều kiện): Kiểu của object trong GraphQL Schema mà fragment được tạo ra từ đó, hay được lồng trong đó.
Sử dụng fragment ứng với thao tác
-
Fragment spread: Khi sử dụng fragment trong các thao tác hay trong một fragment khác, ta cần thêm
...
vào phía trước tên fragment. Nó được gọi là fragment spread, nó có thể xuất hiện trong bất kỳ tập lựa chọn (selection set) trùng với kiểu điều kiện (type condition) của fragment. - Inline fragment: Trong trường hợp ta chỉ muốn thực thi một số trường tùy thuộc vào loại kết quả, nhưng không muốn tách trường đó thành một fragment riêng, ta có thể sử dụng inline fragment. Điều này hoạt động giống như một fragment thông thường, nhưng được viết như một phần của truy vấn. Điểm khác biệt giữa inline fragment và fragment là nó không bắt buộc có type condition.
Directives
Directives cho phép ta thay đổi cấu trúc query một cách linh hoạt sử dụng các biến. Cú pháp directives theo sau @
, trong GraphQL hỗ trợ 3 directive như sau:
-
@deprecated
đánh dấu trường là không khả dụng. -
@include
sẽ include một trường hay fragment khi tham số if là true. -
@skip
sẽ bỏ qua một trường hay fragment khi tham số if là false.
- Directives: Chú thích trên một trường, fragment, operation ảnh hưởng đến cách nó thực thi hoặc trả về.
- Directive arguments: các tham số tương tự như tham số của trường, nhưng chúng được xử lý tại bộ thực thi thay vì chuyển đến resolvers.
Hoạt động
Lấy dữ liệu với Query
Trong REST API, dữ liệu được tải theo các endpoint cụ thể. Mỗi endpoint sẽ định nghĩa cấu trúc dữ liệu mà nó trả về, vì vậy phía client có thể lấy về dữ liệu mong muốn thông qua URL cụ thể.
Ở GraphQL vì chỉ có một endpoint duy nhất, đồng nghĩa với việc tất cả dữ liệu sẽ cần nằm trong một URL. Vì cấu trúc dữ liệu không cố định, nó hoàn toàn linh hoạt cho phép client quyết định dữ liệu nào thực sự cần thiết.
Điều đó có nghĩa là client cần gửi thêm thông tin đến máy chủ để thể hiện nhu cầu dữ liệu của nó - thông tin này được gọi là query. Ta có cú pháp query như sau:
query {
users { # Object
id # Props
name # Props
}
}
Để dễ hiểu ta có bảng dữ liệu như sau:
Ở đây ta có 3 bảng là user
, todos
và online_user
. Khi gọi query như đoạn code trên ta có kết quả sau:
Mọi người test thử tại đây
Ta cũng có thể thử tương tự với các bảng khác như:
query {
online_users {
last_seen
user {
name
}
}
}
Thêm tham số cho query
Ở REST khi muốn thêm tham số để tuỳ chỉnh dữ liệu, ta có thể lấy từ req.query
hay req.params
như : GET /api/todos?limit=10
. Với GraphQL ta sẽ truyền tham số thông qua trường.
query {
todos(limit: 10) {
id
title
}
}
Chỉnh sửa dữ liệu với Mutation
Như đã giới thiệu ở trên, Mutation trong GraphQL sẽ thay thế các phương thức POST
, PUT
, PATCH
và DELETE
trong REST.
Về cú pháp, Mutation tương tự như Query. Nhưng bắt buộc mọi câu lệnh mutation đều phải có tham số và giá trị. Ví dụ
mutation {
insert_todos(objects: [{title: "new todo"}]) {
returning {
id
}
}
}
Chèn tham số động
Ta có thể truyền tham số vào mutation dạng biến như sau:
# The parameterised GraphQL mutation
mutation($todo: todos_insert_input!){
insert_todos(objects: [$todo]) {
returning {
id
}
}
}
Gán giá trị cho biến:
# As a query variable
{
"todo": {
"title": "A new dynamic todo"
}
}
Real-time với Subscription
GraphQL hỗ trợ một chức năng đặc biệt gọi là Subscription, nó cũng tương tự như Query, nhưng thay vì nhận dữ liệu trong mỗi lần gọi ở client, nó sẽ nhận dữ liệu mà server chủ động trả về.
GraphQL là một thành phần quan trọng cho việc thêm các tính năng realtime hay reactive cho ứng dụng. GraphQL server và client hỗ trợ subscription giúp bạn xây dựng những trải trải nghiệm tuyệt vời mà không cần dùng đến socket.
Ví dụ:
subscription {
online_users {
id
last_seen
user {
name
}
}
}
Hoạt động
GraphQL Subscription là một chuỗi truy vấn subscription được gửi đến websocket endpoint. Khi có bất kỳ sự thay đổi dữ liệu nào ở phía backend, dữ liệu mới sẽ được gửi thông qua websocket từ server về client.
Tổng kết
Ở trên là các thành phần và hoạt động của GraphQL, các bài tiếp theo ta sẽ tìm hiểu cách triển khai GraphQL với Apollo. Hy vọng bài viết có ích cho những ai đang cần nó.