Các khái niệm cốt lõi của GraphQL

Trong chương này, bạn sẽ tìm hiểu về một số cấu trúc ngôn ngữ cơ bản của GraphQL. Điều đó bao gồm một cái nhìn đầu tiên về cú pháp để định nghĩa các kiểu dữ liệu cũng như gửi các Query (truy vấn)Mutation (sự thay đổi).

Ngôn ngữ định nghĩa lược đồ (SDL)

GraphQL có hệ thống kiểu dữ liệu riêng được sử dụng để định nghĩa lược đồ của API. Cú pháp để viết lược đồ được gọi là ngôn ngữ định nghĩa lược đồ (SDL).

Dưới đây là một ví dụ về cách chúng ta có thể sử dụng SDL để định nghĩa một kiểu dữ liệu đơn giản được gọi là Person:

type Person {
  name: String!
  age: Int!
}

Kiểu dữ liệu này có hai trường (field) có tên gọi là nameage tương ứng với kiểu dữ liệu StringInt. Ký tự ! theo sau kiểu dữ liệu có nghĩa là trường này là bắt buộc .

Cũng có thể tạo mối quan hệ giữa các kiểu dữ liệu. Trong ví dụ về ứng dụng viết blog, Person có thể được liên kết với Post như sau:

type Post {
  title: String!
  author: Person!
}

Ngược lại, đầu kia của mối quan hệ cần được đặt vào kiểu Person như sau:

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}
Lưu ý: chúng tôi vừa tạo một mối quan hệ một-nhiều giữa PersonPost vì trường posts trong Person là một mảng các bài đăng.

Lấy dữ liệu với Query

Khi làm việc với REST API, dữ liệu được tải từ các điểm cuối cụ thể. Mỗi điểm cuối có cấu trúc được định nghĩa cụ thể về thông tin mà nó trả về. Điều này có nghĩa là các yêu cầu dữ liệu của máy khách được mã hóa trong URL mà nó kết nối tới.

Cách tiếp cận được thực hiện trong GraphQL hoàn toàn khác. Thay vì có nhiều điểm cuối trả về các cấu trúc dữ liệu cố định, GraphQL API thường chỉ hiển thị một điểm cuối duy nhất.

Điều này hoạt động vì cấu trúc của dữ liệu trả về không cố định. Thay vào đó, nó hoàn toàn linh hoạt và cho phép máy khách quyết định dữ liệu nào thực sự cần thiết.

Điều đó có nghĩa là máy khách cần gửi thêm thông tin đến máy chủ để thể hiện nhu cầu dữ liệu của mình - thông tin này được gọi là Query (truy vấn).

Query cơ bản

Chúng ta hãy xem một Query mẫu mà máy khách có thể gửi đến máy chủ như sau:

{
  allPersons {
    name
  }
}

Trường allPersons trong Query này được gọi là trường gốc của Query. Tất cả mọi thứ theo trường gốc, được gọi là payload (tải trọng) của Query. Trường duy nhất được chỉ định trong payload của Query này là name.

Query này sẽ trả về một danh sách tất cả những người hiện đang được lưu trữ trong cơ sở dữ liệu. Đây là một ví dụ kết quả trả về:

{
  "allPersons": [
    { "name": "Johnny" },
    { "name": "Sarah" },
    { "name": "Alice" }
  ]
}

Lưu ý rằng mỗi người chỉ có trường name trong kết quả trả về, nhưng trường age không được máy chủ trả về. Đó là vì trường name là trường duy nhất được chỉ định trong Query.

Nếu máy khách hàng cần thông tin age, tất cả những gì phải làm là điều chỉnh một chút Query và đưa trường age vào payload của Query như sau:

{
  allPersons {
    name
    age
  }
}

Một trong những ưu điểm chính của GraphQL là nó cho phép truy vấn thông tin lồng nhau một cách tự nhiên. Ví dụ: nếu bạn muốn tải tất cả những posts do mộ Person đã viết, bạn chỉ cần tuân theo cấu trúc của các kiểu dữ liệu đã định nghĩa để yêu cầu thông tin này như sau:

{
  allPersons {
    name
    age
    posts {
      title
    }
  }
}

Query có đối số

Trong GraphQL, mỗi trường có thể có 0 hoặc nhiều đối số nếu được chỉ định trong lược đồ. Ví dụ: trường allPersons có thể có một tham số last chỉ định trả về số người cụ thể. Query tương ứng sẽ như thế này:

{
  allPersons(last: 2) {
    name
  }
}

Ghi dữ liệu với mutation

Bên cạnh các yêu cầu lấy thông tin từ máy chủ, phần lớn các ứng dụng cũng cần thực hiện một số thay đổi đối với dữ liệu được lưu trữ trong backend.

Với GraphQL, những thay đổi này được thực hiện bằng cách sử dụng cái gọi là mutation. Nhìn chung có ba loại mutation:

  • Tạo dữ liệu mới.
  • Cập nhật dữ liệu hiện có.
  • Xóa dữ liệu hiện có.

Mutation tuân theo cấu trúc cú pháp giống như các Query, nhưng chúng luôn cần bắt đầu với từ khóa mutation. Ví dụ sau đây chúng tôi tạo một Person mới:

mutation {
  createPerson(name: "Bob", age: 36) {
    name
    age
  }
}

Lưu ý rằng tương tự như Query chúng tôi đã viết trước đây, mutation cũng có trường gốc - trong trường hợp này nó được gọi createPerson.

Chúng ta cũng đã tìm hiểu về các đối số cho các trường ở phần trên. Trong trường hợp này, trường createPerson có hai đối số là nameage.

Giống như với một Query, chúng tôi cũng có thể chỉ định payload cho một mutation trong đó chúng tôi có thể yêu cầu các trường khác nhau của đối tượng Person mới.

Trong trường hợp này chúng tôi yêu cầu hài trường là nameage - mặc dù phải thừa nhận rằng điều đó không hữu ích lắm trong ví dụ của chúng tôi vì rõ ràng chúng tôi đã biết chúng khi truyền chúng vào mutation.

Tuy nhiên, việc có thể truy xuất thông tin khi gửi mutation có thể là một công cụ rất mạnh cho phép bạn truy xuất thông tin mới từ máy chủ trong một truy vấn.

Phản hồi của máy chủ cho mutation trên sẽ như sau:

"createPerson": {
  "name": "Bob",
  "age": 36,
}

Bạn sẽ thường thấy là các kiểu dữ liệu sẽ có ID duy nhất được tạo bởi máy chủ khi các đối tượng mới được tạo. Chúng tôi sẽ bổ sung thêm trường id vào kiểu dữ liệu Person như sau:

type Person {
  id: ID!
  name: String!
  age: Int!
}

Bây giờ, khi một Person mới được tạo, bạn có thể trả về trường id, vì nó là thông tin không có sẵn trên máy khách trước đó:

mutation {
  createPerson(name: "Alice", age: 36) {
    id
  }
}

Cập nhật thời gian thực với Subscription

Một chức năng quan trọng khác đối với nhiều ứng dụng hiện nay là phải có kết nối thời gian thực với máy chủ để được thông báo ngay lập tức về các sự kiện quan trọng. Đối với trường hợp này, GraphQL cung cấp khái niệm Subscription (đăng ký).

Khi một máy khách đăng ký vào một sự kiện, nó sẽ khởi tạo và giữ một kết nối ổn định đến máy chủ. Bất cứ khi nào có sự kiện xảy ra, máy chủ sẽ đẩy dữ liệu tương ứng đến máy khách.

Không giống như các Query và Mutation tuân theo một chu trình gửi yêu cầu-nhận phản hồi thông thường, các Subscription đại diện cho một luồng dữ liệu được gửi từ máy chủ đến máy khách.

Subscription được tạo bằng cách sử dụng cú pháp tương tự như Query và Mutation. Ví dụ sau đây chúng tôi đăng ký sự kiện xảy ra trên kiểu dữ liệu Person:

subscription {
  newPerson {
    id
    name
    age
  }
}

Sau khi máy khách gửi đăng ký này đến máy chủ, một kết nối được mở giữa chúng. Sau đó, bất cứ khi nào một Mutation mới được thực hiện tạo ra một  Person mới, máy chủ sẽ gửi thông tin về người này cho máy khách:

{
  "newPerson": {
    "id": 123,
    "name": "Jane",
    "age": 23
  }
}

Định nghĩa lược đồ

Bây giờ bạn đã có hiểu biết cơ bản về Query, Mutations và Subscription trông như thế nào, hãy kết hợp tất cả lại và tìm hiểu cách bạn có thể tạo một lược đồ cho phép bạn thực hiện các ví dụ bạn đã thấy từ đầu đến giờ.

Lược đồ (schema) là một trong những khái niệm quan trọng nhất khi làm việc với GraphQL API. Nó định nghĩa các chức năng của API và định nghĩa cách máy khách có thể yêu cầu dữ liệu. Nó thường được xem như một contract (hợp đồng) giữa máy chủ và máy khách.

Nói chung, một lược đồ chỉ đơn giản là một tập hợp các kiểu dữ liệu của GraphQL. Tuy nhiên, khi viết lược đồ cho API, có một số kiểu dữ liệu gốc đặc biệt như sau:

type Query { ... }
type Mutation { ... }
type Subscription { ... }

Các kiểu dữ liệu Query, MutationSubscription là các điểm đầu vào (entry points) cho các yêu cầu được gửi bởi máy khách. Để kích hoạt allPersons - query mà chúng ta đã thấy trước đây, kiểu dữ liệu Query sẽ phải được viết như sau:

type Query {
  allPersons: [Person!]!
}

allPersons được gọi là trường gốc (root field) của API. Xem lại ví dụ chúng tôi đã thêm đối số last vào trường allPersons, chúng tôi sẽ phải viết Query như sau:

type Query {
  allPersons(last: Int): [Person!]!
}

Tương tự, đối với mutation createPerson, chúng ta sẽ phải thêm trường gốc vào kiểu dữ liệu Mutation như sau:

type Mutation {
  createPerson(name: String!, age: Int!): Person!
}

Lưu ý rằng trường gốc này cũng có hai đối số là nameage.

Cuối cùng, đối với Subscription, chúng tôi phải thêm trường gốc newPerson:

type Subscription {
  newPerson: Person!
}

Đặt tất cả lại với nhau, đây là lược đồ đầy đủ cho tất cả các Query và Mutation và Subscription mà bạn đã thấy trong chương này:

type Query {
  allPersons(last: Int): [Person!]!
}

type Mutation {
  createPerson(name: String!, age: Int!): Person!
}

type Subscription {
  newPerson: Person!
}

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}

type Post {
  title: String!
  author: Person!
}


Bài viết liên quan:

Giới thiệu hướng dẫn xây dựng API sử dụng GraphQL và NodeJS. Các mục tiêu của hướng dẫn và mã nguồn.

Hướng dẫn này sẽ trình bày qua 3 loại kiến ​​trúc khác nhau sử dụng máy chủ GraphQL.

Những lợi ích tuyệt vời của GraphQL giúp nó trở thành lựa chọn số một để thay thế cho REST API.