Giới thiệu về GraphQL. Cách giải quyết những hạn chế của RESTful API

Bài viết được sự cho phép của tác giả Nguyễn Hữu Khanh

Khi làm việc với RESTful APIs, cho một đối tượng data, chúng ta thường phải expose nhiều request URLs khác nhau. Ví dụ, bạn đang làm việc với ứng dụng quản lý thông tin sinh viên, để provide thông tin sinh viên thông qua RESTful APIs, chúng ta có thể sẽ phải expose một số request URLs sau:

  • Danh sách toàn bộ sinh viên với đầy đủ các trường thông tin
  • Danh sách tên của tất cả sinh viên
  • Danh sách sinh viên của một lớp học nào đó

Cứ mỗi một nhu cầu lấy thông tin khác nhau của thông tin sinh viên này, chúng ta lại phải expose thêm mới một request URL. Thêm nữa, cho một request URL, ví dụ như request URL để lấy thông tin toàn bộ sinh viên với đầy đủ các trường thông tin, thì cũng không phải tất cả các trường thông tin của sinh viên đều được sử dụng, chúng ta có thể chỉ cần thông tin tên, tuổi của sinh viên để hiển thị, các thông tin khác như địa chỉ, lớp học thì không cần. Việc return các thông tin này là dư thừa và không cần thiết.

Làm thế nào để giải quyết những hạn chế của RESTful API ở trên? Các bạn có thể sử dụng GraphQL.

Giới thiệu GraphQL

GraphQL là ngôn ngữ dùng để thao tác và truy vấn dữ liệu cho API, cung cấp cho client 1 cách thức dễ dàng để request chính xác những gì họ cần, giúp việc phát triển API dễ dàng hơn.

So sánh RESTful API và GraphQL

Để thấy rõ sự khác nhau giữa RESTful API và GraphQL, mình sẽ tạo mới một Spring Boot project và implement cả RESTful API và GraphQL để thao tác với thông tin sinh viên mà mình đã đề cập ở trên:

Kết quả:

Ví dụ sử dụng RESTful API

Mình sẽ hiện thực RESTful API trước.

Mình sẽ định nghĩa các request URL trên sử dụng OpenAPI và sử dụng Maven plugin của OpenAPI để generate API contract. Nội dung tập tin chúng tôi trong thư mục src/main/resources/api như sau:

openapi

: 3.0.3

info

:

 

title

: Student Information Management System

 

version

: 1.0.0

servers

:

paths

:

 

/students

:

   

get

:

     

operationId

: getStudents

     

summary

: Get all students information

,

can be filtered by clazz name

     

parameters

:

     

- name

: clazz

       

in

: query

       

description

: Class of students

       

required

: false

       

schema

:

         

type

: string

     

responses

:

       

200

:

         

description

: Get all students information

         

content

:

           

application/json

:

             

schema

:

               

type

: array

               

items

:

                 

$

ref

: '#/components/schemas/Student'

             

example

:

             

- id

: 1

               

code

: '001'

               

name

: Khanh

               

age

: 30

               

address

: 'Binh Dinh'

               

clazz

: A

             

- id

: 2

               

code

: '002'

               

name

: Quan

               

age

: 25

               

address

: 'Ho Chi Minh'

               

clazz

: B

 

/students/names

:

   

get

:

     

operationId

: getStudentNames

     

summary

: Get student names

     

responses

:

       

200

:

         

description

: Get student information

         

content

:

           

application/json

:

             

schema

:

               

type

: array

               

items

:

                 

type

: string

                 

components

:

 

schemas

:

   

Student

:

     

type

: object

     

properties

:

       

id

:

         

type

: integer

         

format

: int64

       

code

:

         

type

: string

       

name

:

         

type

: string

       

age

:

         

type

: integer

         

format

: int64

       

address

:

         

type

: string

       

clazz

:

         

type

: string

Kết quả của mình như sau:

Để thao tác với table Student trong database với cấu trúc như sau:

CREATE

TABLE

student (

 

id

bigint

NOT NULL

,

 

code

varchar

(10)

NOT NULL

,

 

name

varchar

(50)

NOT NULL

,

 

age

bigint

NOT NULL

,

 

address

varchar

(100)

DEFAULT

NULL

,

 

class

varchar

(20)

NOT NULL

,

 

PRIMARY KEY

(id) )

mình sẽ cấu hình thông tin database trong tập tin application.properties như sau:

spring

.

datasource

.

url

=

jdbc

:

postgresql

:

//localhost:5432/example

spring

.

datasource

.

username

=

khanh

spring

.

datasource

.

password

=

1

Cùng với đó, mình cũng sẽ tạo một class StudentRepository:

package

com

.

huongdanjava

.

graphql

.

repository

;

import

java

.

util

.

List

;

import

org

.

springframework

.

data

.

jpa

.

repository

.

JpaRepository

;

import

com

.

huongdanjava

.

graphql

.

repository

.

model

.

StudentModel

;

  

interface

NamesOnly

{

   

String

getName

(

)

;

 

}

}

với class StudentModel có nội dung như sau:

package

com

.

huongdanjava

.

graphql

.

repository

.

model

;

import

javax

.

persistence

.

Column

;

import

javax

.

persistence

.

Entity

;

import

javax

.

persistence

.

Id

;

import

javax

.

persistence

.

Table

;

import

lombok

.

Data

;

@Data

@Entity

@Table

(

name

=

"student"

)

public

class

StudentModel

{

  

@Column

 

@Id

 

private

Long

id

;

  

@Column

 

private

String

code

;

  

@Column

 

private

String

name

;

  

@Column

 

private

Long

age

;

  

@Column

 

private

String

address

;

  

@Column

(

name

=

"class"

)

 

private

String

clazz

;

}

Bây giờ thì mình sẽ tạo mới một class StudentsApiDelegateImpl implement generated interface StudentsApiDelegate như sau”

package

com

.

huongdanjava

.

graphql

.

web

.

impl

;

import

java

.

util

.

ArrayList

;

import

java

.

util

.

List

;

import

org

.

springframework

.

beans

.

BeanUtils

;

import

org

.

springframework

.

beans

.

factory

.

annotation

.

Autowired

;

import

org

.

springframework

.

stereotype

.

Service

;

import

com

.

huongdanjava

.

graphql

.

dto

.

Student

;

import

com

.

huongdanjava

.

graphql

.

repository

.

StudentRepository

;

import

com

.

huongdanjava

.

graphql

.

repository

.

StudentRepository

.

NamesOnly

;

import

com

.

huongdanjava

.

graphql

.

repository

.

model

.

StudentModel

;

import

com

.

huongdanjava

.

graphql

.

web

.

StudentsApiDelegate

;

@Service

public

class

StudentsApiDelegateImpl

implements

StudentsApiDelegate

{

  

@Autowired

 

private

StudentRepository

studentRepository

;

  

@Override

    

for

(

StudentModel

sm

:

studentModels

)

{

     

Student

student

=

toStudent

(

sm

)

;

     

students

.

add

(

student

)

;

   

}

    

return

ResponseEntity

.

ok

(

students

)

;

 

}

  

private

Student

toStudent

(

StudentModel

sm

)

{

   

Student

student

=

new

Student

(

)

;

   

BeanUtils

.

copyProperties

(

sm

,

student

)

;

    

return

student

;

 

}

   

if

(

clazz

==

null

)

{

     

return

studentRepository

.

findAll

(

)

;

   

}

    

return

studentRepository

.

findByClazz

(

clazz

)

;

 

}

  

@Override

    

return

ResponseEntity

.

ok

(

studentNames

)

;

 

}

}

Giả sử bây giờ trong database, mình đang có những data như sau:

thì khi lấy thông tin tất cả sinh viên, kết quả sẽ như sau:

Chỉ lấy danh sách sinh viên của lớp A sẽ trả về kết quả như sau:

Danh sách tên của tất cả sinh viên sẽ trả về kết quả như sau:

Ví dụ sử dụng GraphQL

Để làm việc với GraphQL, điều đầu tiên chúng ta cần làm là định nghĩa một tập tin schema. Nói nôm na thì tập tin schema này định nghĩa những thông tin mà GraphQL server có thể cung cấp cho client truy vấn data. Nó cũng giống như việc chúng ta định nghĩa API specs sử dụng tập tin .yaml trong OpenAPI vậy các bạn!

Với Spring Boot application thì các bạn có thể định nghĩa một tập tin schema.graphqls nằm trong thư mục src/main/resources/graphql. Cho ví dụ của bài viết này, mình sẽ định nghĩa tập tin schema này với nội dung như sau:

type

Query

{

 

students

(

clazz

:

String

)

:

[

Student

]

}

type

Student

{

 

id

:

ID

 

code

:

String

 

name

:

String

 

age

:

Int

 

address

:

String

 

clazz

:

String

}

Trong tập tin schema của GraphQL, chúng ta sẽ định nghĩa nhiều loại type khác nhau. Ngoài các type định nghĩa cho các đối tượng data mà chúng ta sẽ provide cho client, trong ví dụ của mình là đối tượng Student, GraphQL còn có 3 type đặc biệt là Query, Mutation và Subscription. Type Query dùng để truy vấn data, type Mutation dùng để thêm, sửa, xoá data còn type Subscription thì tương tự như type Query nhưng kết quả trả về sẽ thay đổi theo thời gian (tương tự như Server Send Event đó các bạn). Trong ví dụ của mình, mình đã định nghĩa type Query với field là students cùng với tham số clazz để filter, kết quả trả về sẽ là một danh sách data với type là Student.

Chúng ta cần implement một Controller để định nghĩa cách mà Spring sẽ lấy data cho chúng ta như sau:

package

com

.

huongdanjava

.

graphql

;

import

java

.

util

.

List

;

import

org

.

springframework

.

beans

.

factory

.

annotation

.

Autowired

;

import

org

.

springframework

.

graphql

.

data

.

method

.

annotation

.

Argument

;

import

org

.

springframework

.

graphql

.

data

.

method

.

annotation

.

QueryMapping

;

import

org

.

springframework

.

stereotype

.

Controller

;

import

com

.

huongdanjava

.

graphql

.

repository

.

StudentRepository

;

import

com

.

huongdanjava

.

graphql

.

repository

.

model

.

StudentModel

;

@Controller

public

class

StudentGraphQLController

{

  

@Autowired

 

private

StudentRepository

studentRepository

;

  

@QueryMapping

   

if

(

clazz

==

null

)

{

     

return

studentRepository

.

findAll

(

)

;

   

}

    

return

studentRepository

.

findByClazz

(

clazz

)

;

 

}

}

Spring sẽ tự động mapping Query type với annotation @QueryMapping và tên của method chính là tên của query. Ở đây, các bạn còn có thể truyền argument của query sử dụng annotation @Argument.

Để hỗ trợ cho việc testing, Spring cung cấp cho chúng ta một GUI tên là GraphiQL để làm việc với GraphQL, nhưng mặc định GUI này bị disable. Các bạn có thể enable nó bằng cách cấu hình property spring.graphql.graphiql.enabled trong tập tin application.properties như sau:

spring

.

graphql

.

graphiql

.

enabled

=

true

Bây giờ thì các bạn có thể chạy ứng dụng của chúng ta lên và kiểm tra kết quả rồi!

Trong cửa sổ này, bên trái là nơi cho phép chúng ta viết câu truy vấn, còn bên phải là nơi sẽ hiển thị kết quả đó các bạn!

Chúng ta sẽ sử dụng GraphQL query để truy vấn dữ liệu. Một GraphQL query sẽ bắt đầu với “{” và chúng ta sẽ khai báo field mà chúng ta muốn truy vấn. Ví dụ để lấy thông tin tất cả sinh viên với GraphQL, mình sẽ viết query như sau:

{

 

students

{

   

id

   

code

   

name

   

age

   

address

   

clazz

 

}

}

Kết quả:

Để lấy danh sách sinh viên của lớp A, mình sẽ viết query như sau:

{

 

students

(

clazz

:

"A"

)

{

   

id

   

code

   

name

   

age

   

address

   

clazz

 

}

}

Kết quả:

Còn danh sách tên của tất cả sinh viên thì mình chỉ cần remove các sub-field khác, chỉ giữ lại sub-field name như sau:

{

 

students

(

clazz

:

"A"

)

{

   

name

 

}

}

Kết quả:

Như các bạn thấy, chỉ với một query mapping của GraphQL cho đối tượng data Student, chúng ta có thể lấy hết thông tin mà chúng ta muốn và thông tin trả về cũng có thể được giới hạn tuỳ theo nhu cầu.

Tin tuyển dụng IT mọi cấp độ trên TopDev đang chờ bạn ứng tuyển!