GraphQL (Graph Query Language) là một tiêu chuẩn API được công bố vào năm 2015 phát triển bởi chính gã khổng lồ Facebook. Về mặt định nghĩa, GraphQL là một ngôn ngữ dùng để truy vấn cho API, nó không bị ràng buộc với bất kỳ cơ sở dữ liệu hoặc công cụ lưu trữ cụ thể nào. Về bản chất, GraphQL chỉ có một endpoint, thường là HTTP POST request. Trong bài viết này ta sẽ tìm hiểu về kiến trúc của GraphQL bao gồm có thành phần, đặc điểm cùng với cách hoạt động để có được cái nhìn tổng quan cũng như hiểu được điều gì đã làm nên thế mạnh của GraphQL.
1. Những đặc điểm chính
- Strongly typed [1]: Một đặc điểm chính và nổi bật của GraphQL, đặc điểm này quy định bất kì field nào trong GraphQL cũng đều phải được khai báo kiểu dữ liệu. Đặc điểm này giúp cho GraphQL server có thể kiểm tra tính chính xác về cú pháp cũng như hợp lệ của câu query do phía client gửi tới dựa vào type system của nó trước khi query được thực thi
- Hierarchical data model [1]: Chữ Graph trong GraphQL chính là thể hiện cho đặc điểm này, dựa vào đặc điểm strongly typed ở trên đã tạo ra các nested type(xem thêm ở phần type system) hình thành nên mô hình phân cấp(hay tree). Chính việc mô hình hóa dưới dạng một phân cấp đồ thị mà GraphQL cho phép người dùng có thể lấy được dữ liệu từ nhiều nguồn (hay nhiều endpoint) chỉ trong một request
- Client-specified queries [2]: Với bản chất hỗ trợ truy vấn dữ liệu của mình, GraphQL cho phép phía client tự định nghĩa ra cấu trúc dữ liệu cần lấy. Query sẽ được encode ở phía client thay vì server và granularity(tính hạt hay mức độ chi tiết) của query là field-level
- Introspective [2]: cung cấp metadata giúp khám phá cấu trúc bên trong schema bao gồm các type, field, query, mutation cũng như cung cấp mô tả đến mức field. Hơn thế nữa, đặc điểm này còn cung cấp khả năng kiểm thử query với GraphQL server
2. Thành phần
Hai thành phần quan trọng để tạo nên GraphQL là Schema và Resolver function.
2.1 Schema
Schema là nền tảng của mọi GraphQL server và là thành phần đầu tiên phải được cài đặt khi xây dựng một GraphQL server vì nó hoạt động như một công cụ hỗ trợ giao tiếp giữa client và server trong GraphQL. Schema dùng type system để diễn giải các hoạt động trong GraphQL. Mỗi một schema đều bắt buộc phải khai báo root type đặc biệt có tên là Query và có thể khai báo hoặc không root type đặc biệt khác là Mutation. Schema document được parse tại thời điểm khởi động GraphQL server, thông tin sau khi parse sẽ bao gồm thông tin type và các field cũng như resolve function xử lý dữ liệu cho field đó [5].
Type system
Type system là các type được định nghĩa trong schema của GraphQL, tác giả Sayan Guha và công sự [4] đã liệt kê các kiểu dữ liệu GraphQL cung cấp và phân loại chúng như trong bảng dưới đây. Ngoài ra, GraphQL còn hỗ trợ thêm một số kiểu dữ liệu khác như union, interface,… mọi người có thể tham khảo thêm tại đây
Type | Type description | Includes | Definition | Primitive type | Advanced type | Heterogeneous type |
Data retrieval | Data modify |
Scalar | Kiểu dữ liệu vô hướng |
Int | Số nguyên có dấu 32-bit | x | ||||
Float | Số thực có dấu | x | ||||||
String | Chuỗi kí tự tuân theo chuẩn UTF-8 | x | ||||||
Boolean | Nhị phân | x | ||||||
ID | Định danh duy nhất và luôn được hiểu là một string | x | ||||||
Object | Object type bao gồm một tập hợp các field, với mỗi field ta phải xác định type cụ thể cho nó, có thể là scalar type, object type hoặc các type đặc biệt khác. Việc khai báo một object type cho field tạo ra nested type | Object type cùng với Scalar type có thể là thành phần cấu thành nên các Object type khác | x | x | ||||
Query | Là một root type trong GraphQL, query định nghĩa ra entry point cho mỗi query trong GraphQL(data-fetching, tương tự GET của REST) | Query phải được định nghĩa rõ các field cần thiết bên trong | x | x | x | |||
Mutation | Là một root type trong GraphQL, mutation định nghĩa ra các entry point giúp modify dữ liệu (data-manipulation, tương tự POST, PUT, DELETE của REST) | Cấu thành các Create hoặc Update type bên trong Type Mutation | x | x | x |
Trên thực tế, dữ liệu trong các ứng dụng có thể biểu diễn dưới dạng đồ thị bao gồm các node và cạnh, với node thường được hiểu là các object trong thực tế như Book hay User, còn cạnh đại diện cho relationship giữa các object. Với GraphQL, object type thường được xem như một object trong thực tế (node cha) với nhiều thuộc tính của object được thể hiện thông qua field (tương ứng node con dạng cây) và việc khai báo object type cho một field cũng giống như việc tạo ra relationship giữa hai object type, còn field có kiểu scalar thì được hiểu là node lá. Khi duyệt đồ thị, GraphQL server sẽ dễ dàng dựa vào các cạnh (relationship) để duyệt tới node tiếp theo và lấy dữ liệu trả về (Dạng nested) có thể hình dung quá trình như trong hình 2
2.2 Resolver Function
GraphQL server sẽ không biết phải làm gì với query đã khai báo trong schema ra trừ khi nó biết được resolver. Resolver nói cho GraphQL biết nơi và cách thức để lấy data cần cho query được yêu cầu.
3. Cách hoạt động
Tác giả Mr.Sayan Guha và các cộng sự [4] đã cung cấp mô hình khái niệm cách hoạt động của một GraphQL engine trong hình 3 và mô tả cách hoạt động như sau:
- Khi client gửi đến server một câu truy vấn thì GraphQL engine sẽ thực hiện query parsing và validate query dựa trên schema đã định nghĩa sẵn. Từ đây GraphQL engine sẽ biết được rằng câu query này có hợp lệ hay không và thực thi những bước tiếp theo. Cụ thể hơn về phần parse và validate, GraphQL server parse query string nhận được từ phía client thành một cây cú pháp trừu tượng (Abstract syntax tree hay AST) như trong hình 4 và trình biên dịch AST sẽ tiến hành validate cú pháp dựa trên AST đã có với schema [5]
- Nếu câu query hợp lệ, schema sẽ gọi đến các hàm resolver tương ứng để xử lý câu query và trả về dữ liệu cho người dùng, các resolver function có thể tương tác với các database hoặc gọi REST API sẵn có để lấy dữ liệu
Tính đồ thị của GraphQL
Query trong GraphQL có thể ví như một đường dẫn thư mục bắt đầu từ root type đến các sub type của nó cho đến khi chạm đến node lá (field có kiểu scalar). Nếu hình dung query như một cấu trúc cây thì việc khai báo kiểu scalar cho một field chứng tỏ node đó là node lá, còn việc khai báo object type cho field cũng tương tự việc node đó có thêm các node con, một query tree và thứ tự duyệt cây có thể hình dung thông qua hình 5
Trong khi xử lý đến từng node, nếu chạm đến các node (field) có kiểu scalar thì sẽ kết thúc xử lý, còn nếu chạm đến các node (field) có kiểu object thì sẽ tiến hành duyệt vào bên trong để xử lý các node con (sub field) và lặp lại quá trình trên. Sau khi các resolver function được thực thi và trả về dữ liệu cho các field được yêu cầu thì tất cả sẽ được merge và trở thành kết quả cuối cùng của query
Cài đặt thử nghiệm GraphQL với Python
Trong bài viết này mình sẽ tiến hành cài đặt thử nghiệm GraphQL với ngôn ngữ Python sử dụng thư viện ariadne [6]
Khai báo schema
Trong quá trình validate query nếu query được gửi đến sai cú pháp so với schema đã khai báo thì GraphQL server sẽ tự động báo lỗi lên cho người dùng. Nhưng nếu validate thành công mà trong quá trình thực thi resolver function lại xảy ra lỗi thì GraphQL sẽ không báo lỗi chỉ trả về trạng thái 200 thành công và dữ liệu rỗng, vậy nên, để bắt được lỗi trả về thì mình sẽ thêm một field error trong kết quả trả về của query
schema { query: Query mutation: Mutation } type KhachHang { makh: ID! hoten: String! ngaysinh: String! } type DonHang { madh: ID! tongtien: Float khachhang: KhachHang! } type ListDonHangResult { success: Boolean! #Thêm field success để thông báo cho phía client biết về trạng thái thực thi của câu query errors: [String] #Thêm field errors để nếu có lỗi thì chi tiết lỗi sẽ được liệt kê trong field này ldonhang: [DonHang]! } type Query { listDonHang: ListDonHangResult! } input KhachHangInput { makh: String! hoten: String! ngaysinh: String! } type Mutation { createKhachHang(input: KhachHangInput!): KhachHang! }
Xử lý dữ liệu trả về với Resolver Function
Đối với phần lấy dữ liệu, mình sử dụng thêm thư viện hỗ trợ tương tác với relational database ở đây là SQLAlchemy để đọc/ghi dữ liệu, ngoài ra như đã đề cập ở phần khai báo schema ta có thêm một field success chứa trạng thái thực thi và field error chứa lỗi trả ra, vậy nên trong logic code mình sẽ dùng try catch để bắt lỗi và trả về cho field error, cũng như cập nhật trạng thái thực thi tương ứng cho field success
def listDonHang_resolver(obj, info): try: ldonhang = [donhang.to_dict() for donhang in DonHang.query.all()] payload = { "success": True, "donhang": ldonhang } except Exception as error: payload = { "success": False, "errors": [str(error)] } return payload
def createKhachHang_resolver(obj, info, input): try: kh = KhachHang( MaKhachHang=input['makh'], HoTen_KH=input['hoten'], NgaySinh_KH=datetime.strptime(str(input['ngaysinh']),"%d-%m-%Y") ) db.session.add(kh) db.session.commit() payload = { "success": True, "post": kh.to_dict() } except ValueError: # date format errors payload = { "success": False, "errors": [f"Incorrect date format provided. Date should be in the format dd-mm-yyyy"] } return payload
Khởi động GraphQL server
Sau khi khai báo xong schema và resolver function, ta cần có một bước binding, và thư viện ariadne đã cung cấp sẵn API make_executable_schema làm chuyện đó cho chúng ta. Cuối cùng, khi query được gửi xuống API graphql_sync sẽ chịu trách nhiệm thực thi câu query dựa trên schema đã được parse một cách đồng bộ
from ariadne import load_schema_from_path, make_executable_schema, graphql_sync, snake_case_fallback_resolvers, ObjectType from api.queries import listDonHang_resolver from api.mutations import createKhachHang_resolver query = ObjectType("Query") mutation = ObjectType("Mutation") query.set_field("listDonHang", listDonHang_resolver) mutation.set_field("createKhachHang", createKhachHang_resolver) type_defs = load_schema_from_path("/schema.graphql") schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers) @app.route("/graphql", methods=["POST"]) def graphql_server(): data = request.get_json() success, result = graphql_sync( schema, data, context_value=request, #optional debug=app.debug ) return Response(json.dumps(result,indent=4))
References
[1] G. Brito, T. Mombach and M. T. Valente, “Migrating to GraphQL: A Practical Assessment,” 2019 IEEE 26th International Conference on Software Analysis, Evolution and Reengineering (SANER), 2019, pp. 140-150, doi: 10.1109/SANER.2019.8667986.
[2] Stemmler, Khalil, and Michael Watson. “Why You Should Disable GraphQL Introspection In Production – GraphQL Security.”, 20 May 2021, url: https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/. Accessed 30 June 2022.
[3] “Schemas and Types”, url: https://graphql.org/learn/schema/. Accessed 30 June 2022.
[4] Guha, S. & Majumder, S. (2020). A COMPARATIVE STUDY BETWEEN GRAPH-QL& RESTFUL SERVICES IN API MANAGEMENT OF STATELESS ARCHITECTURES. International Journal on Web Service Computing (IJWSC), 11, 01-16. doi: 10.5121/ijwsc.2020.11201
[5] Goodnews, Ogboada & Anireh, Vincent & Matthias, Daniel. (2022). A Model for Optimizing the Runtime of GraphQL Queries. 9. 11-39.
[6] “API reference”, url: https://ariadnegraphql.org/docs/api-reference.html. Accessed 30 June 2022.