Trong phát triển phần mềm hiện đại, các ứng dụng hiếm khi tồn tại độc lập. Chúng giao tiếp, trao đổi dữ liệu và kích hoạt các hành động lẫn nhau, tạo thành các hệ sinh thái rộng lớn, kết nối với nhau. Giao tiếp này được điều phối bởi Giao diện Lập trình Ứng dụng (API), định nghĩa các quy tắc và giao thức về cách các thành phần phần mềm khác nhau tương tác. Qua nhiều thập kỷ, một số kiểu kiến trúc và giao thức đã xuất hiện để tạo điều kiện cho giao tiếp giữa các dịch vụ này. Trong số những kiến trúc nổi bật nhất có Remote Procedure Calls (RPC), Representational State Transfer (REST) và GraphQL.
Hiểu ba mô hình này là rất quan trọng đối với bất kỳ nhà phát triển hoặc kiến trúc sư nào thiết kế hệ thống phân tán. Mỗi mô hình đi kèm với triết lý, điểm mạnh, điểm yếu và trường hợp sử dụng lý tưởng riêng. Bài viết này nhằm mục đích giải thích rõ ràng về RPC, REST và GraphQL, đi sâu vào các khái niệm cốt lõi, cơ chế hoạt động, lợi ích, hạn chế và các kịch bản mà mỗi mô hình tỏa sáng.
Bạn muốn một nền tảng tích hợp, Tất cả trong Một cho đội ngũ phát triển của mình làm việc cùng nhau với năng suất tối đa?
Apidog đáp ứng mọi yêu cầu của bạn và thay thế Postman với mức giá phải chăng hơn nhiều!
Nền tảng: Giao tiếp Client-Server
Trước khi đi sâu vào chi tiết cụ thể, điều cần thiết là phải nắm bắt mô hình cơ bản mà tất cả chúng phục vụ: giao tiếp client-server. Trong mô hình này, một client (ví dụ: trình duyệt web, ứng dụng di động, một máy chủ khác) yêu cầu một số dữ liệu hoặc muốn thực hiện một hành động. Đồng thời, một server (một máy hoặc quy trình từ xa) lưu trữ dữ liệu hoặc logic để thực hiện hành động đó. Client gửi một request (yêu cầu) đến server, và server gửi lại một response (phản hồi). Các cơ chế mà chúng ta sắp thảo luận – RPC, REST và GraphQL – là những cách khác nhau để cấu trúc các yêu cầu và phản hồi này.
RPC: Gọi Hàm Qua Mạng
RPC là gì?
Remote Procedure Call (Gọi Thủ tục Từ xa) đại diện cho một trong những mô hình trực tiếp và sớm nhất cho giao tiếp giữa các quy trình. Ý tưởng cơ bản là làm cho yêu cầu tới một máy chủ từ xa xuất hiện và hoạt động giống như một lệnh gọi hàm hoặc thủ tục cục bộ. Ứng dụng client gọi một hàm dường như là cục bộ (cái gọi là "thủ tục"), nhưng việc thực thi hàm này thực sự xảy ra trên một máy chủ từ xa. Sự phức tạp của giao tiếp mạng được trừu tượng hóa cẩn thận, mang lại cho lập trình phân tán một vẻ đơn giản tương tự như lập trình truyền thống, trên một máy đơn.
RPC hoạt động như thế nào:
Quá trình RPC diễn ra thông qua một chuỗi các bước phối hợp, được thiết kế để làm cho việc thực thi từ xa trở nên minh bạch. Ban đầu, client sở hữu một "stub" (đoạn mã giả) hoặc "proxy" (đại diện) cho thủ tục từ xa. Stub này mô phỏng chữ ký của thủ tục từ xa thực tế. Khi ứng dụng client gọi stub này, logic không được thực thi cục bộ. Thay vào đó, stub phía client lấy các tham số được truyền cho hàm và "marshals" hoặc "serializes" chúng. Bước quan trọng này chuyển đổi các tham số từ dạng biểu diễn trong bộ nhớ của chúng sang một định dạng phù hợp để truyền qua mạng, chẳng hạn như binary, XML hoặc JSON.
Sau khi marshalling, các tham số đã được serialize này, cùng với một định danh cho thủ tục cụ thể cần gọi, được gửi qua mạng đến server. Ở phía server, một "skeleton" (khung sườn) hoặc stub phía server chờ đợi và nhận yêu cầu đến. Skeleton phía server này sau đó thực hiện nhiệm vụ "unmarshalling" hoặc "deserializing" dữ liệu nhận được, chuyển đổi nó trở lại thành các tham số mà thủ tục server thực tế mong đợi.
Với các tham số đã được tái cấu trúc thành công, skeleton phía server gọi thủ tục được chỉ định trên server, truyền các tham số đã được unmarshalling cho nó. Khi thủ tục hoàn thành việc thực thi, giá trị trả về của nó, cùng với bất kỳ ngoại lệ nào gặp phải, sẽ được marshalling bởi skeleton phía server. Phản hồi đã được serialize này sau đó được truyền trở lại qua mạng đến stub phía client. Khi nhận được phản hồi, stub phía client unmarshalling nó, chuyển đổi dữ liệu trở lại thành giá trị trả về mà ứng dụng client có thể dễ dàng hiểu được. Cuối cùng, stub phía client trả về giá trị này cho mã gọi ban đầu, từ đó hoàn thành ảo giác rằng một lệnh gọi hàm cục bộ đã được thực hiện.
Các đặc điểm chính của RPC:
API RPC thường có tính hành động (action-oriented). Chúng được thiết kế xoay quanh các động từ hoặc lệnh, chẳng hạn như addUser(userDetails)
hoặc calculatePrice(itemId, quantity)
. Trọng tâm chính là "những hành động bạn có thể thực hiện".
Theo truyền thống, client và server trong hệ thống RPC thể hiện sự kết nối chặt chẽ (tight coupling). Client thường cần kiến thức rõ ràng về tên hàm cụ thể và chữ ký tham số chính xác có sẵn trên server. Do đó, các sửa đổi ở phía server thường đòi hỏi những thay đổi tương ứng ở phía client.
Các framework RPC thường cung cấp các công cụ để tạo client stub và server skeleton bằng nhiều ngôn ngữ lập trình khác nhau từ một Ngôn ngữ Định nghĩa Giao diện (Interface Definition Language - IDL) được chia sẻ. Ví dụ về IDL bao gồm CORBA IDL, Protocol Buffers (.proto files) hoặc Apache Thrift IDL. Khả năng tạo mã này tạo điều kiện cho khả năng tương tác.
Về hiệu quả (efficiency), nhiều giao thức RPC, đặc biệt là những giao thức sử dụng định dạng binary, được thiết kế để đạt hiệu suất tối ưu về kích thước dữ liệu và tốc độ xử lý.
Sự phát triển: gRPC
Trong khi các triển khai RPC trước đây như XML-RPC hoặc Java RMI có những hạn chế nhất định, mô hình RPC đã trải qua một sự hồi sinh đáng kể với sự ra đời của các framework hiện đại như gRPC (Google RPC). gRPC giới thiệu những cải tiến đáng kể. Nó chủ yếu sử dụng Protocol Buffers làm IDL và để serialize message. Protocol Buffers cung cấp một cơ chế độc lập ngôn ngữ, độc lập nền tảng, có thể mở rộng để serialize dữ liệu có cấu trúc, thường được mô tả là một giải pháp thay thế nhỏ gọn, nhanh hơn và đơn giản hơn so với XML.
Hơn nữa, gRPC hoạt động trên HTTP/2, cho phép các tính năng nâng cao như multiplexing (cho phép nhiều yêu cầu và phản hồi trên một kết nối duy nhất), khả năng server push và nén header. Những tính năng này cùng nhau góp phần cải thiện hiệu suất và giảm độ trễ.
Một điểm mạnh đáng chú ý của gRPC là hỗ trợ các chế độ streaming khác nhau. Chúng bao gồm unary (mô hình yêu cầu-phản hồi đơn giản), server streaming (nơi client gửi một yêu cầu và server phản hồi bằng một luồng message), client streaming (nơi client gửi một luồng message và server đưa ra một phản hồi duy nhất), và bidirectional streaming (nơi cả client và server đều có thể gửi một luồng message độc lập).
Cuối cùng, gRPC cung cấp các công cụ mạnh mẽ để tạo mã (code generation), cho phép tự động tạo mã client và server bằng nhiều ngôn ngữ lập trình phổ biến.
Ưu điểm của RPC (đặc biệt là RPC hiện đại như gRPC):
- Hiệu suất: Nó có thể đạt hiệu suất đặc biệt cao, đặc biệt khi sử dụng các giao thức binary như Protocol Buffers kết hợp với HTTP/2. Độ trễ thấp là một lợi ích nổi bật.
- Đơn giản (đối với nhà phát triển): Việc trừu tượng hóa một lệnh gọi từ xa như một hàm cục bộ có thể đơn giản hóa đáng kể nỗ lực phát triển, đặc biệt đối với các microservice nội bộ.
- Hợp đồng được định kiểu mạnh (Strongly Typed Contracts): IDL thực thi một hợp đồng rõ ràng, không mơ hồ giữa client và server, giúp phát hiện lỗi tích hợp trong quá trình biên dịch.
- Khả năng Streaming: Nó vượt trội trong các kịch bản yêu cầu luồng dữ liệu thời gian thực hoặc truyền các tập dữ liệu lớn.
- Tạo mã (Code Generation): Việc tự động tạo thư viện client và server stub làm giảm lượng mã lặp đi lặp lại mà nhà phát triển cần viết.
Nhược điểm của RPC:
- Kết nối chặt chẽ (Tight Coupling): Ngay cả khi sử dụng IDL, việc thay đổi chữ ký thủ tục thường đòi hỏi phải tạo lại và triển khai lại cả mã client và server.
- Khả năng khám phá (Discoverability): Không giống như REST, không có phương pháp chuẩn hóa để khám phá các thủ tục có sẵn hoặc cấu trúc của chúng mà không cần truy cập trước vào IDL hoặc tài liệu liên quan.
- Ít thân thiện với trình duyệt (Trong lịch sử): Các cơ chế RPC truyền thống không dễ dàng tích hợp trực tiếp với trình duyệt web so với REST. Mặc dù gRPC-Web nhằm mục đích thu hẹp khoảng cách này, nó thường yêu cầu một lớp proxy.
- Vượt tường lửa (Firewall Traversal): Các cơ chế RPC không dựa trên HTTP đôi khi có thể gặp khó khăn với tường lửa chủ yếu được cấu hình để cho phép lưu lượng HTTP. gRPC, bằng cách sử dụng HTTP/2, phần lớn giảm thiểu mối lo ngại này.
Khi nào nên sử dụng RPC:
Hãy cân nhắc sử dụng RPC cho giao tiếp microservice nội bộ, nơi hiệu suất và độ trễ thấp là mục tiêu thiết kế quan trọng. Nó cũng rất phù hợp cho các ứng dụng yêu cầu streaming dữ liệu phức tạp, hiệu suất cao. Nếu mong muốn một hợp đồng được định kiểu mạnh, được xác định rõ ràng giữa các dịch vụ, RPC mang lại những lợi thế đáng kể. Môi trường đa ngôn ngữ (polyglot environments), nơi việc tạo mã cho nhiều ngôn ngữ có thể hợp lý hóa quá trình phát triển, cũng hưởng lợi từ RPC. Cuối cùng, trong các môi trường mạng bị hạn chế, nơi hiệu quả về kích thước message là tối quan trọng, RPC, đặc biệt là gRPC với Protocol Buffers, là một ứng viên mạnh mẽ.
REST: Tài nguyên và Hypermedia
REST là gì?
REST, hay Representational State Transfer, không phải là một giao thức hay một tiêu chuẩn cứng nhắc, mà là một kiểu kiến trúc để thiết kế các ứng dụng mạng. Nó được Roy Fielding định nghĩa tỉ mỉ trong luận án tiến sĩ năm 2000 của ông. REST khéo léo tận dụng các tính năng và giao thức hiện có của HTTP, nhấn mạnh vào chế độ giao tiếp không trạng thái (stateless), client-server, có thể lưu trữ cache (cacheable). Khái niệm trung tâm xoay quanh các tài nguyên (thực thể dữ liệu) được xác định duy nhất bằng URL, và các tương tác với các tài nguyên này được thực hiện bằng các phương thức HTTP chuẩn.
Các nguyên tắc cốt lõi của REST (Các ràng buộc):
Kiểu kiến trúc REST được xác định bởi một số ràng buộc hướng dẫn:
Một nguyên tắc cơ bản là Kiến trúc Client-Server. Điều này đòi hỏi sự phân tách rõ ràng các mối quan tâm. Client chịu trách nhiệm về giao diện người dùng và các khía cạnh trải nghiệm người dùng, trong khi server quản lý lưu trữ dữ liệu, logic nghiệp vụ và việc cung cấp chính API đó.
Một ràng buộc quan trọng khác là Không trạng thái (Statelessness). Mỗi yêu cầu được gửi từ client đến server phải đóng gói tất cả thông tin cần thiết để server hiểu và xử lý yêu cầu đó. Server không giữ bất kỳ ngữ cảnh client nào (trạng thái phiên) giữa các yêu cầu riêng lẻ. Bất kỳ trạng thái nào liên quan đến một phiên đều được duy trì ở phía client.
Có thể lưu trữ cache (Cacheability) cũng là một nguyên tắc cốt lõi. Các phản hồi do server tạo ra phải tự định nghĩa rõ ràng là có thể lưu trữ cache hoặc không thể lưu trữ cache. Điều này cho phép client và các hệ thống trung gian (chẳng hạn như Mạng phân phối nội dung hoặc CDN) lưu trữ cache các phản hồi, điều này có thể nâng cao đáng kể hiệu suất và khả năng mở rộng.
Hệ thống REST được thiết kế như một Hệ thống phân lớp (Layered System). Điều này có nghĩa là client thường không thể xác định xem nó có kết nối trực tiếp với server cuối cùng hay với một hệ thống trung gian (như bộ cân bằng tải hoặc proxy) trên đường truyền thông. Các server trung gian có thể tăng cường khả năng mở rộng của hệ thống bằng cách tạo điều kiện cân bằng tải và cung cấp các cache được chia sẻ.
Ràng buộc Giao diện Đồng nhất (Uniform Interface) có lẽ là điều làm cho REST khác biệt nhất và phục vụ mục đích đơn giản hóa và tách rời kiến trúc. Ràng buộc này được chia nhỏ thành nhiều ràng buộc con.
Đầu tiên, Nhận dạng Tài nguyên (Identification of Resources): Tất cả các tài nguyên khái niệm được nhận dạng trong các yêu cầu bằng cách sử dụng URI (Uniform Resource Identifiers), thường là URL. Ví dụ, /users/123 xác định duy nhất một tài nguyên người dùng cụ thể.
Thứ hai, Thao tác Tài nguyên Thông qua Biểu diễn (Manipulation of Resources Through Representations): Client tương tác với tài nguyên không phải bằng cách trực tiếp gọi các phương thức trên chúng, mà bằng cách trao đổi các biểu diễn của các tài nguyên này. Một biểu diễn có thể ở nhiều định dạng khác nhau, chẳng hạn như JSON, XML hoặc HTML. Client chỉ định định dạng ưa thích của mình bằng cách sử dụng header Accept, trong khi server chỉ định định dạng của biểu diễn được gửi bằng header Content-Type.
Thứ ba, Message Tự mô tả (Self-Descriptive Messages): Mỗi message được trao đổi phải chứa đủ thông tin để mô tả cách nó nên được xử lý. Ví dụ, các header HTTP như Content-Type và Content-Length cung cấp metadata về nội dung message, và các mã trạng thái thông báo cho client về kết quả của yêu cầu của nó.
Thứ tư, Hypermedia là Công cụ của Trạng thái Ứng dụng (Hypermedia as the Engine of Application State - HATEOAS): Nguyên tắc này, thường được coi là khía cạnh phức tạp nhất và đôi khi ít được triển khai nhất của REST, quy định rằng các phản hồi của server nên bao gồm các liên kết (hypermedia). Các liên kết này hướng dẫn client bằng cách chỉ ra những hành động nào nó có thể thực hiện tiếp theo hoặc những tài nguyên liên quan nào nó có thể truy cập. Điều này cho phép client điều hướng API một cách động, thay vì dựa vào các URI được mã hóa cứng. Ví dụ, một phản hồi cho tài nguyên người dùng có thể bao gồm các liên kết để xem đơn đặt hàng của họ hoặc cập nhật chi tiết hồ sơ của họ.
Một ràng buộc tùy chọn là Mã theo yêu cầu (Code on Demand). Điều này cho phép server tạm thời mở rộng hoặc tùy chỉnh chức năng của client bằng cách truyền mã thực thi, chẳng hạn như các đoạn mã JavaScript.
REST hoạt động như thế nào:
Trong một hệ thống RESTful, mọi thứ được khái niệm hóa như một tài nguyên (resource) (ví dụ: người dùng, sản phẩm, đơn hàng). Mỗi tài nguyên được xác định duy nhất bằng một URI. Ví dụ, GET /users
có thể truy xuất danh sách người dùng, trong khi GET /users/123
truy xuất người dùng cụ thể có ID 123.
Các Phương thức HTTP chuẩn (Động từ) được sử dụng để thực hiện các hành động trên các tài nguyên này. GET
được sử dụng để truy xuất một tài nguyên. POST
thường tạo một tài nguyên mới hoặc có thể được sử dụng để kích hoạt một quy trình, với dữ liệu cho tài nguyên mới được gửi trong nội dung yêu cầu. PUT
được sử dụng để cập nhật một tài nguyên hiện có, thường yêu cầu thay thế hoàn toàn dữ liệu của tài nguyên, và là idempotent (nhiều yêu cầu giống hệt nhau có cùng hiệu quả như một yêu cầu duy nhất). DELETE
xóa một tài nguyên. PATCH
cho phép cập nhật một phần cho một tài nguyên hiện có. HEAD
truy xuất metadata về một tài nguyên, tương tự như GET
nhưng không có nội dung phản hồi. OPTIONS
được sử dụng để lấy thông tin về các tùy chọn giao tiếp cho tài nguyên đích.
Mã trạng thái (Status Codes), là một phần của tiêu chuẩn HTTP, được sử dụng để chỉ ra kết quả của một yêu cầu. Ví dụ bao gồm 200 OK
, 201 Created
cho việc tạo thành công, 400 Bad Request
cho lỗi client, 404 Not Found
khi tài nguyên không tồn tại, và 500 Internal Server Error
cho các vấn đề phía server.
Về Định dạng dữ liệu (Data Formats), JSON (JavaScript Object Notation) đã trở thành định dạng phổ biến nhất để trao đổi dữ liệu trong API REST do tính nhẹ nhàng và dễ phân tích cú pháp. Tuy nhiên, XML, HTML, hoặc thậm chí văn bản thuần túy cũng có thể được sử dụng.
Ưu điểm của REST:
- Đơn giản và quen thuộc: Nó tận dụng các tiêu chuẩn HTTP đã được hiểu rõ, giúp việc học, triển khai và sử dụng tương đối dễ dàng.
- Không trạng thái (Statelessness): Điều này đơn giản hóa thiết kế server và nâng cao khả năng mở rộng vì server không cần duy trì thông tin phiên client giữa các yêu cầu.
- Có thể lưu trữ cache (Cacheability): Cơ chế cache HTTP có thể được sử dụng trực tiếp và hiệu quả để cải thiện hiệu suất và giảm tải cho server.
- Tách rời (Decoupling): Client và server được tách rời. Miễn là URI tài nguyên và hợp đồng phương thức vẫn nhất quán, các triển khai cơ bản ở hai phía có thể phát triển độc lập.
- Khả năng khám phá (với HATEOAS): Khi HATEOAS được triển khai đúng cách, client có thể tự động khám phá các hành động có sẵn và điều hướng qua các tài nguyên, làm cho API linh hoạt và có thể phát triển hơn.
- Áp dụng rộng rãi và công cụ hỗ trợ: Có một hệ sinh thái rộng lớn các công cụ, thư viện, SDK client và gateway hỗ trợ API RESTful. Nó vốn đã thân thiện với trình duyệt.
- Dễ đọc đối với con người (Human-Readable): URI thường được thiết kế để con người dễ đọc, và các định dạng dữ liệu phổ biến như JSON dễ dàng cho nhà phát triển kiểm tra và gỡ lỗi.
Nhược điểm của REST:
- Lấy quá nhiều (Over-fetching) và lấy thiếu (Under-fetching): Đây là những vấn đề phổ biến.
- Over-fetching xảy ra khi một endpoint trả về nhiều dữ liệu hơn mức client thực sự cần cho một tác vụ cụ thể. Ví dụ, để hiển thị danh sách tên người dùng, một endpoint
/users
có thể trả về các đối tượng người dùng hoàn chỉnh bao gồm địa chỉ, số điện thoại và các chi tiết khác cho mọi người dùng, hầu hết trong số đó có thể không được sử dụng. - Under-fetching xảy ra khi client cần thực hiện nhiều yêu cầu đến các endpoint khác nhau để thu thập tất cả dữ liệu cần thiết cho một chế độ xem hoàn chỉnh. Ví dụ, để lấy chi tiết của người dùng và các bài đăng gần đây của họ, client có thể cần gọi
/users/{id}
trước và sau đó thực hiện một lệnh gọi riêng tới/users/{id}/posts
. - Nhiều lượt khứ hồi (Multiple Round Trips): Vấn đề under-fetching thường dẫn đến nhiều lượt khứ hồi mạng, điều này có thể làm tăng độ trễ và ảnh hưởng tiêu cực đến trải nghiệm người dùng, đặc biệt trên mạng di động hoặc mạng không ổn định.
- Thách thức về phiên bản (Versioning Challenges): Việc phát triển API REST mà không làm hỏng các client hiện có có thể là một thách thức. Các chiến lược phổ biến bao gồm phiên bản URI (ví dụ:
/v1/users
), sử dụng các header yêu cầu tùy chỉnh cho phiên bản, hoặc sử dụng phiên bản kiểu media (thông qua headerAccept
). Mỗi cách tiếp cận có những phức tạp và đánh đổi riêng. - HATEOAS thường bị bỏ qua: Mặc dù là một nguyên tắc cốt lõi của REST, HATEOAS thường không được triển khai đầy đủ. Điều này hạn chế khả năng khám phá thực sự và khả năng phát triển động mà REST hướng tới.
- Không có định kiểu mạnh sẵn có (No Strong Typing Out-of-the-Box): Không giống như các hệ thống RPC sử dụng IDL, REST dựa vào các quy ước và tài liệu bên ngoài (chẳng hạn như thông số kỹ thuật OpenAPI/Swagger) để định nghĩa hợp đồng API. Những điều này không phải lúc nào cũng được thực thi trong quá trình biên dịch, có khả năng dẫn đến các vấn đề tích hợp.
Khi nào nên sử dụng REST:
REST là một lựa chọn tuyệt vời cho các API công khai, nơi việc áp dụng rộng rãi, dễ tích hợp và khả năng tương tác là quan trọng. Nó rất phù hợp cho các ứng dụng tập trung vào tài nguyên, nơi các hoạt động CRUD (Tạo, Đọc, Cập nhật, Xóa) tiêu chuẩn trên các thực thể tạo thành chế độ tương tác chính. Nếu việc tận dụng cache HTTP là rất quan trọng đối với hiệu suất và khả năng mở rộng, sự phù hợp của REST với các tiêu chuẩn HTTP là một lợi thế đáng kể. Các tình huống yêu cầu không trạng thái và khả năng mở rộng theo chiều ngang cũng hưởng lợi rất nhiều từ kiểu kiến trúc RESTful. Hơn nữa, khi mong muốn khả năng khám phá thông qua hypermedia (HATEOAS) để cho phép client điều hướng API một cách động, REST cung cấp khuôn khổ cho điều đó.
GraphQL: Ngôn ngữ truy vấn cho API của bạn
GraphQL là gì?
GraphQL là một ngôn ngữ truy vấn được thiết kế đặc biệt cho API, và nó cũng bao gồm một runtime phía server để thực thi các truy vấn này bằng cách sử dụng một hệ thống kiểu mà bạn định nghĩa cho dữ liệu của mình. Ban đầu được Facebook phát triển và sau đó được mã nguồn mở vào năm 2015, GraphQL được hình thành để trực tiếp giải quyết một số hạn chế vốn có trong kiến trúc RESTful, đáng chú ý nhất là hai vấn đề over-fetching (lấy quá nhiều) và under-fetching (lấy thiếu) dữ liệu. Nó trao quyền cho client yêu cầu chính xác dữ liệu họ cần, không hơn không kém, thường trong một chu kỳ yêu cầu-phản hồi duy nhất.
Các khái niệm cốt lõi của GraphQL:
Kiến trúc của GraphQL được xây dựng dựa trên một số khái niệm nền tảng.
Một trụ cột là Ngôn ngữ Định nghĩa Schema (Schema Definition Language - SDL). API GraphQL được định nghĩa một cách chặt chẽ bởi một hệ thống kiểu mạnh. Server công bố một schema mô tả tỉ mỉ tất cả các loại dữ liệu mà client được phép truy vấn, cũng như các mối quan hệ phức tạp tồn tại giữa các loại dữ liệu này. Schema này đóng vai trò là một hợp đồng ràng buộc giữa client và server. Trong SDL, bạn định nghĩa các cấu trúc khác nhau:
- Types (Kiểu) mô tả các đối tượng bạn có thể lấy và các trường của chúng (ví dụ:
type User { id: ID!, name: String, email: String, posts: [Post] }
). - Queries (Truy vấn) định nghĩa các điểm vào cho các thao tác lấy dữ liệu (ví dụ:
type Query { user(id: ID!): User, posts: [Post] }
). - Mutations (Đột biến) định nghĩa các điểm vào cho các thao tác sửa đổi dữ liệu—tạo, cập nhật hoặc xóa dữ liệu (ví dụ:
type Mutation { createUser(name: String!, email: String!): User }
). - Subscriptions (Đăng ký) định nghĩa cách client có thể nhận các cập nhật thời gian thực khi dữ liệu cụ thể thay đổi trên server (ví dụ:
type Subscription { newPost: Post }
).
Một tính năng phân biệt khác là việc sử dụng điển hình một Endpoint Duy nhất (Single Endpoint). Không giống như REST, thường sử dụng nhiều URL để đại diện cho các tài nguyên và hoạt động khác nhau, API GraphQL thường chỉ hiển thị một endpoint duy nhất (ví dụ: /graphql
). Tất cả các yêu cầu, cho dù là truy vấn để lấy dữ liệu, đột biến để sửa đổi dữ liệu hay đăng ký để cập nhật thời gian thực, đều được gửi đến endpoint duy nhất này, nói chung là thông qua yêu cầu HTTP POST.
Trung tâm sức mạnh của GraphQL là khái niệm Truy vấn do Client chỉ định (Client-Specified Queries). Ứng dụng client xây dựng một chuỗi truy vấn chỉ định chính xác dữ liệu nào nó yêu cầu. Điều này có thể bao gồm không chỉ các trường trên một đối tượng chính mà còn cả các trường trên các đối tượng liên quan, đi qua các mối quan hệ dữ liệu phức tạp. Server sau đó xử lý truy vấn này và phản hồi bằng một đối tượng JSON có cấu trúc phản ánh chính xác cấu trúc của truy vấn do client gửi.
GraphQL hoạt động như thế nào:
Sự tương tác trong một hệ thống GraphQL tuân theo một luồng được xác định rõ ràng. Nó bắt đầu với Định nghĩa Schema (Schema Definition), nơi server định nghĩa khả năng dữ liệu của mình bằng cách sử dụng SDL của GraphQL.
Tiếp theo, Truy vấn của Client (Client Query) được xây dựng. Client tạo một chuỗi truy vấn GraphQL, chi tiết các trường dữ liệu cụ thể mà nó cần. Ví dụ:GraphQL
query GetUserDetails {
user(id: "123") {
id
name
email
posts { # Lấy các bài đăng liên quan
title
content
}
}
}
Truy vấn này sau đó được gửi trong một Yêu cầu đến Server (Request to Server), thường là dưới dạng payload JSON trong yêu cầu HTTP POST, nhắm mục tiêu đến endpoint GraphQL duy nhất.
Khi nhận được yêu cầu, Xử lý phía Server (Server Processing) bắt đầu. Điều này bao gồm một số bước con. Server đầu tiên Phân tích cú pháp & Xác thực (Parses & Validates) truy vấn đến, kiểm tra cú pháp của nó và đảm bảo nó tuân thủ schema đã định nghĩa. Nếu hợp lệ, truy vấn chuyển sang Thực thi (Execution). Server thực thi truy vấn bằng cách gọi các hàm "resolver". Mỗi trường được định nghĩa trong schema GraphQL đều được hỗ trợ bởi một hàm resolver tương ứng. Resolver là một đoạn mã chịu trách nhiệm lấy dữ liệu cho trường cụ thể của nó. Các resolver này có thể lấy dữ liệu từ nhiều nguồn khác nhau, chẳng hạn như cơ sở dữ liệu, các API nội bộ hoặc bên ngoài khác, hoặc thậm chí dữ liệu tĩnh.
Cuối cùng, server tạo và gửi một Phản hồi đến Client (Response to Client). Phản hồi này là một đối tượng JSON có cấu trúc phản ánh hình dạng của truy vấn gốc, chỉ chứa dữ liệu được yêu cầu rõ ràng. Đối với truy vấn ví dụ ở trên, phản hồi có thể trông như sau:JSON
{
"data": {
"user": {
"id": "123",
"name": "Alice Wonderland",
"email": "alice@example.com",
"posts": [
{
"title": "Bài viết đầu tiên của tôi",
"content": "Xin chào thế giới!"
},
{
"title": "GraphQL thật tuyệt",
"content": "Tìm hiểu về GraphQL..."
}
]
}
}
}
Ưu điểm của GraphQL:
- Không Over-fetching hoặc Under-fetching: Client yêu cầu chính xác dữ liệu họ cần, dẫn đến truyền dữ liệu hiệu quả cao và giảm lãng phí băng thông.
- Một yêu cầu cho nhiều tài nguyên: Dữ liệu liên quan, ngay cả trên các tài nguyên khái niệm khác nhau, có thể được lấy trong một truy vấn duy nhất, giảm đáng kể số lượt khứ hồi mạng.
- Schema được định kiểu mạnh: Schema đóng vai trò là một hợp đồng rõ ràng và có thẩm quyền giữa client và server. Việc định kiểu mạnh này cho phép các công cụ phát triển mạnh mẽ, chẳng hạn như tự động hoàn thành, phân tích tĩnh và xác thực truy vấn, và nó cũng hoạt động như một dạng tài liệu tự thân.
- Khả năng phát triển (Evolvability): Việc phát triển API trở nên dễ dàng hơn mà không cần dùng đến việc tạo phiên bản. Thêm các trường hoặc kiểu mới vào schema không làm hỏng các client hiện có, vì họ chỉ nhận dữ liệu mà họ yêu cầu rõ ràng. Việc ngừng sử dụng các trường cũ cũng đơn giản hơn.
- Dữ liệu thời gian thực với Subscriptions: GraphQL bao gồm hỗ trợ tích hợp sẵn cho các cập nhật thời gian thực thông qua subscriptions, cho phép client lắng nghe các thay đổi dữ liệu cụ thể trên server.
- Có khả năng tự kiểm tra (Introspective): API GraphQL vốn có khả năng tự kiểm tra. Client có thể truy vấn schema để khám phá các kiểu, trường, truy vấn, đột biến và subscriptions có sẵn, tạo điều kiện cho việc khám phá và tạo client động.
Nhược điểm của GraphQL:
- Độ phức tạp: Việc thiết lập và quản lý một server GraphQL có thể phức tạp hơn so với API REST đơn giản, đặc biệt liên quan đến việc triển khai logic resolver, thiết kế schema và tối ưu hóa hiệu suất.
- Caching: Cơ chế cache HTTP ít dễ dàng áp dụng trực tiếp trong GraphQL so với cache dựa trên tài nguyên của REST. Mặc dù các giải pháp cache phía client phức tạp (như Apollo Client hoặc Relay) tồn tại, cache phía server và trung gian yêu cầu các chiến lược khác nhau, và cache cấp trường có thể phức tạp.
- Tải lên tệp: Thông số kỹ thuật GraphQL không xử lý tải lên tệp một cách tự nhiên. Điều này thường yêu cầu các giải pháp tạm thời, chẳng hạn như sử dụng các endpoint REST riêng biệt để xử lý tệp hoặc triển khai các thư viện xử lý yêu cầu multipart cùng với GraphQL.
- Giới hạn tốc độ (Rate Limiting): Việc triển khai giới hạn tốc độ hiệu quả có thể phức tạp hơn vì "chi phí" hoặc cường độ tài nguyên của một truy vấn GraphQL không rõ ràng ngay lập tức chỉ bằng cách nhìn vào nó; một truy vấn lồng sâu hoặc phức tạp có thể rất tốn kém để thực thi. Điều này thường đòi hỏi các cơ chế phân tích chi phí truy vấn.
- Đường cong học tập: Có một đường cong học tập liên quan đến việc hiểu các khái niệm GraphQL, thành thạo các phương pháp hay nhất về thiết kế schema và trở nên thành thạo ngôn ngữ truy vấn.
- Cạm bẫy hiệu suất (Performance Pitfalls): Các resolver được viết kém hoặc các truy vấn lồng sâu, phức tạp quá mức có thể dẫn đến các vấn đề về hiệu suất, chẳng hạn như vấn đề N+1 (nơi việc lấy danh sách các mục và các mục con của chúng dẫn đến một truy vấn cho danh sách và N truy vấn bổ sung cho các mục con của mỗi mục), nếu không được xử lý cẩn thận bằng các kỹ thuật như data loaders.
Khi nào nên sử dụng GraphQL:
GraphQL đặc biệt phù hợp cho các ứng dụng có nhiều loại client khác nhau, chẳng hạn như ứng dụng web, ứng dụng di động và thiết bị IoT, tất cả đều có thể có các yêu cầu dữ liệu khác nhau. Nó tỏa sáng trong các tình huống mà việc giảm thiểu truyền dữ liệu và giảm số lượt khứ hồi mạng là rất quan trọng, ví dụ, trong các ứng dụng di động hoạt động trên mạng chậm hoặc không ổn định. Đối với các hệ thống phức tạp, nơi client cần lấy dữ liệu từ nhiều nguồn cơ bản khác nhau hoặc điều hướng các mối quan hệ tài nguyên lồng sâu, GraphQL cung cấp một giải pháp thanh lịch. Nếu một hợp đồng API được định kiểu mạnh, khả năng tự tài liệu mạnh mẽ và khả năng phát triển API được đánh giá cao, GraphQL cung cấp những lợi ích này. Các ứng dụng yêu cầu cập nhật thời gian thực thông qua subscriptions có thể tận dụng sự hỗ trợ tự nhiên của GraphQL cho tính năng này. Cuối cùng, GraphQL là một lựa chọn tuyệt vời khi bạn muốn trao cho client nhiều quyền lực và sự linh hoạt hơn trong cách họ lấy dữ liệu, điều chỉnh các yêu cầu theo nhu cầu chính xác của họ.
RPC vs. REST vs. GraphQL: So sánh nhanh
Tính năng | RPC (ví dụ: gRPC) | REST | GraphQL |
Mô hình | Hướng hành động/hàm | Hướng tài nguyên | Ngôn ngữ truy vấn dữ liệu |
Endpoint | Nhiều endpoint (một cho mỗi thủ tục) | Nhiều endpoint (một cho mỗi tài nguyên/tập hợp) | Thường là một endpoint duy nhất (/graphql ) |
Lấy dữ liệu | Server quyết định dữ liệu trả về cho mỗi thủ tục | Server quyết định dữ liệu trả về cho mỗi tài nguyên | Client quyết định chính xác dữ liệu nào cần |