Trong thế giới phát triển ứng dụng hiện đại, REST API đóng vai trò là lớp giao tiếp cơ bản, cho phép các hệ thống khác biệt trao đổi dữ liệu một cách liền mạch. Khi các ứng dụng phát triển về quy mô và độ phức tạp, lượng dữ liệu mà chúng xử lý cũng tăng theo. Việc yêu cầu toàn bộ tập dữ liệu, có khả năng chứa hàng triệu hoặc thậm chí hàng tỷ bản ghi, chỉ trong một lần gọi API là không hiệu quả, không đáng tin cậy và là một điểm nghẽn hiệu suất đáng kể. Đây là lúc một kỹ thuật quan trọng trong thiết kế và phát triển API phát huy tác dụng: phân trang REST API. Hướng dẫn này cung cấp cái nhìn tổng quan sâu sắc, toàn diện về việc triển khai phân trang trong REST API, bao gồm mọi thứ từ các khái niệm cơ bản đến triển khai thực tế nâng cao sử dụng các ngăn xếp công nghệ khác nhau như Node.js, Python và .NET.
Bạn muốn một nền tảng tích hợp, Tất cả trong Một để Đội ngũ Phát triển của bạn làm việc cùng nhau với năng suất tối đa?
Apidog đáp ứng mọi nhu cầu của bạn và thay thế Postman với mức giá phải chăng hơn nhiều!
Các Khái Niệm Cơ Bản về Phân trang REST API
Trước khi đi sâu vào các ví dụ mã phức tạp và các mẫu thiết kế, điều cần thiết là phải nắm vững phân trang là gì và tại sao nó là một khía cạnh không thể thiếu trong thiết kế API chuyên nghiệp.
Phân trang trong REST API là gì?
Về cốt lõi, phân trang REST API là một kỹ thuật được sử dụng để lấy phản hồi từ một điểm cuối (endpoint) REST API và phân đoạn nó thành các đơn vị nhỏ hơn, dễ quản lý hơn, thường được gọi là "trang". Thay vì cung cấp một tập dữ liệu khổng lồ tiềm năng trong một lần, API trả về một phần nhỏ, có thể dự đoán được của dữ liệu. Quan trọng là, phản hồi của API cũng bao gồm siêu dữ liệu (metadata) cho phép máy khách (client) lấy từng phần tiếp theo nếu họ cần thêm dữ liệu.
Quá trình này tương tự như các trang của một cuốn sách hoặc kết quả tìm kiếm trên Google. Bạn được hiển thị trang kết quả đầu tiên, cùng với các điều khiển để điều hướng đến trang thứ hai, thứ ba, v.v. Như các cộng đồng nhà phát triển như DEV Community và các nền tảng như Merge.dev đã chỉ ra, đây là quá trình chia nhỏ một tập dữ liệu lớn thành các phần nhỏ hơn, mà máy khách có thể lấy dần dần nếu họ thực sự muốn toàn bộ dữ liệu đó. Đây là một khái niệm nền tảng để xây dựng các ứng dụng mạnh mẽ và có khả năng mở rộng.
Tại sao Phân trang là Yêu cầu Cốt lõi trong Thiết kế API Hiện đại?
Động lực chính của phân trang là đảm bảo phản hồi API dễ xử lý hơn cho cả máy chủ (server) và máy khách (client). Nếu không có nó, các ứng dụng sẽ đối mặt với những hạn chế nghiêm trọng và trải nghiệm người dùng kém. Các lợi ích chính bao gồm:
- Cải thiện Hiệu suất và Giảm Độ trễ: Lợi thế đáng kể nhất là tốc độ. Việc truyền một payload JSON nhỏ gồm 25 bản ghi nhanh hơn rất nhiều lần so với việc truyền payload gồm 2,5 triệu bản ghi. Điều này mang lại cảm giác nhanh nhạy, phản hồi tốt cho người dùng cuối.
- Tăng cường Độ tin cậy của API: Các phản hồi HTTP lớn có xác suất thất bại giữa chừng cao hơn do hết thời gian chờ mạng, mất kết nối hoặc giới hạn bộ nhớ phía máy khách. Phân trang tạo ra các yêu cầu nhỏ hơn, đáng tin cậy hơn. Nếu một trang không tải được, máy khách chỉ cần thử lại yêu cầu cụ thể đó mà không cần bắt đầu lại toàn bộ quá trình truyền dữ liệu.
- Giảm Tải cho Máy chủ: Việc tạo ra một phản hồi khổng lồ có thể gây áp lực đáng kể lên tài nguyên của máy chủ. Truy vấn cơ sở dữ liệu có thể chậm và việc serialize hàng triệu bản ghi thành JSON tiêu tốn đáng kể CPU và bộ nhớ. Phân trang cho phép máy chủ thực hiện các truy vấn nhỏ hơn, hiệu quả hơn, cải thiện khả năng tổng thể và khả năng phục vụ nhiều máy khách cùng lúc.
- Xử lý Hiệu quả ở phía Máy khách: Đối với các ứng dụng máy khách, đặc biệt là những ứng dụng chạy trên thiết bị di động hoặc trong trình duyệt web, việc phân tích cú pháp (parsing) một đối tượng JSON khổng lồ có thể làm đóng băng giao diện người dùng và dẫn đến trải nghiệm khó chịu. Các phần dữ liệu nhỏ hơn dễ dàng phân tích cú pháp và hiển thị hơn, mang lại ứng dụng mượt mà hơn.
Các Chiến lược và Kỹ thuật Phân trang Phổ biến
Có một số cách để triển khai phân trang, nhưng hai chiến lược chính đã trở thành tiêu chuẩn thực tế trong ngành. Việc lựa chọn giữa chúng có ý nghĩa quan trọng đối với hiệu suất, tính nhất quán của dữ liệu và trải nghiệm người dùng.
Phân trang Dựa trên Offset: Cách Tiếp cận Nền tảng
Phân trang dựa trên offset, thường được gọi là "phân trang theo số trang", thường là cách tiếp cận đầu tiên mà các nhà phát triển tìm hiểu. Nó đơn giản về mặt khái niệm và được thấy trong nhiều ứng dụng web. Nó hoạt động bằng cách sử dụng hai tham số chính:
limit
(hoặcpage_size
): Số lượng kết quả tối đa trả về trên một trang.offset
(hoặcpage
): Số lượng bản ghi cần bỏ qua từ đầu tập dữ liệu. Nếu sử dụng tham sốpage
, offset thường được tính là(page - 1) * limit
.
Một yêu cầu điển hình trông như thế này: GET /api/products?limit=25&offset=50
Điều này sẽ chuyển thành một truy vấn SQL như sau:SQL
SELECT * FROM products ORDER BY created_at DESC LIMIT 25 OFFSET 50;
Truy vấn này bỏ qua 50 sản phẩm đầu tiên và lấy 25 sản phẩm tiếp theo (tức là sản phẩm 51-75).
Ưu điểm:
- Đơn giản: Phương pháp này dễ triển khai, như được trình bày trong nhiều hướng dẫn như "Node.js REST API: Offset Pagination Made Easy."
- Điều hướng Không trạng thái: Máy khách có thể dễ dàng nhảy đến bất kỳ trang nào trong tập dữ liệu mà không cần thông tin trước đó, làm cho nó lý tưởng cho giao diện người dùng có liên kết số trang.
Nhược điểm và Hạn chế:
- Hiệu suất kém trên Tập dữ liệu lớn: Nhược điểm chính là mệnh đề
OFFSET
của cơ sở dữ liệu. Đối với một yêu cầu có offset lớn (ví dụ:OFFSET 1000000
), cơ sở dữ liệu vẫn phải lấy tất cả 1.000.025 bản ghi từ đĩa, đếm qua một triệu bản ghi đầu tiên để bỏ qua chúng, và chỉ sau đó mới trả về 25 bản ghi cuối cùng. Điều này có thể trở nên cực kỳ chậm khi số trang tăng lên. - Tính nhất quán của dữ liệu kém (Trôi trang): Nếu các bản ghi mới được ghi vào cơ sở dữ liệu trong khi người dùng đang phân trang, toàn bộ tập dữ liệu sẽ bị dịch chuyển. Người dùng điều hướng từ trang 2 sang trang 3 có thể thấy một bản ghi bị lặp lại từ cuối trang 2, hoặc bỏ sót hoàn toàn một bản ghi. Đây là một vấn đề đáng kể đối với các ứng dụng thời gian thực và là chủ đề phổ biến trong các diễn đàn nhà phát triển như Stack Overflow khi thảo luận về cách đảm bảo tính nhất quán của dữ liệu.
Phân trang Dựa trên Con trỏ (Keyset): Giải pháp Có khả năng Mở rộng
Phân trang dựa trên con trỏ, còn được gọi là phân trang keyset hoặc seek, giải quyết các vấn đề về hiệu suất và tính nhất quán của phương pháp offset. Thay vì số trang, nó sử dụng một "con trỏ" (cursor), là một điểm đánh dấu ổn định, không rõ ràng, trỏ đến một bản ghi cụ thể trong tập dữ liệu.
Luồng hoạt động như sau:
- Máy khách thực hiện yêu cầu ban đầu cho một trang dữ liệu.
- Máy chủ trả về trang dữ liệu, cùng với một con trỏ trỏ đến mục cuối cùng trong tập hợp đó.
- Đối với trang tiếp theo, máy khách gửi lại con trỏ đó cho máy chủ.
- Máy chủ sau đó truy xuất các bản ghi nằm *sau* con trỏ cụ thể đó, thực tế là "tìm kiếm" (seeking) đến điểm đó trong tập dữ liệu.
Con trỏ thường là một giá trị được mã hóa xuất phát từ (các) cột đang được sắp xếp. Ví dụ, nếu sắp xếp theo created_at
(một dấu thời gian), con trỏ có thể là dấu thời gian của bản ghi cuối cùng. Để xử lý các trường hợp trùng lặp, một cột thứ hai, duy nhất (như id
của bản ghi) thường được bao gồm.
Một yêu cầu sử dụng con trỏ trông như thế này: GET /api/products?limit=25&after_cursor=eyJjcmVhdGVkX2F0IjoiMjAyNS0wNi0wN1QxODowMDowMC4wMDBaIiwiaWQiOjg0N30=
Điều này sẽ chuyển thành một truy vấn SQL hiệu quả hơn nhiều:SQL
SELECT * FROM products
WHERE (created_at, id) < ('2025-06-07T18:00:00.000Z', 847)
ORDER BY created_at DESC, id DESC
LIMIT 25;
Truy vấn này sử dụng một chỉ mục (index) trên (created_at, id)
để "tìm kiếm" (seek) ngay lập tức đến điểm bắt đầu chính xác, tránh quét toàn bộ bảng và làm cho nó nhanh chóng một cách nhất quán bất kể người dùng đang phân trang sâu đến mức nào.
Ưu điểm:
- Hiệu suất rất cao và Có khả năng Mở rộng: Hiệu suất cơ sở dữ liệu nhanh và không đổi, làm cho nó phù hợp với tập dữ liệu ở mọi kích thước.
- Tính nhất quán của Dữ liệu: Vì con trỏ được gắn với một bản ghi cụ thể, chứ không phải một vị trí tuyệt đối, dữ liệu mới được thêm vào hoặc xóa đi sẽ không gây ra việc bỏ sót hoặc lặp lại các mục giữa các trang.
Nhược điểm:
- Độ phức tạp Triển khai: Logic để tạo và phân tích cú pháp con trỏ phức tạp hơn so với tính toán offset đơn giản.
- Điều hướng Hạn chế: Máy khách chỉ có thể điều hướng đến trang "tiếp theo" hoặc "trước đó". Không thể nhảy trực tiếp đến một số trang cụ thể, làm cho nó kém phù hợp với một số mẫu giao diện người dùng nhất định.
- Yêu cầu Khóa Sắp xếp Ổ định: Việc triển khai gắn chặt với thứ tự sắp xếp và yêu cầu ít nhất một cột duy nhất, tuần tự.
So sánh Hai Loại Phân trang Chính
Việc lựa chọn giữa phân trang offset và cursor hoàn toàn phụ thuộc vào trường hợp sử dụng.
Tính năng | Phân trang Offset | Phân trang Con trỏ |
Hiệu suất | Kém đối với các trang sâu trong tập dữ liệu lớn. | Tuyệt vời và nhất quán ở mọi độ sâu. |
Tính nhất quán của Dữ liệu | Dễ bị thiếu/lặp dữ liệu (trôi trang). | Cao; dữ liệu mới không ảnh hưởng đến phân trang. |
Điều hướng | Có thể nhảy đến bất kỳ trang nào. | Hạn chế ở các trang tiếp theo/trước đó. |
Triển khai | Đơn giản và dễ dàng. | Phức tạp hơn; yêu cầu logic con trỏ. |
Trường hợp Sử dụng Lý tưởng | Tập dữ liệu nhỏ, tĩnh; giao diện người dùng quản trị. | Nguồn cấp dữ liệu cuộn vô hạn; tập dữ liệu lớn, động. |
Các Thực hành Tốt nhất để Triển khai Phân trang ở phía Máy chủ
Bất kể chiến lược đã chọn là gì, việc tuân thủ một bộ các thực hành tốt nhất sẽ tạo ra một API gọn gàng, dễ dự đoán và dễ sử dụng. Đây thường là một phần quan trọng để trả lời câu hỏi "Thực hành tốt nhất về phân trang phía máy chủ là gì?".
Thiết kế Payload Phản hồi Phân trang
Một lỗi phổ biến là chỉ trả về một mảng kết quả. Một **payload phản hồi phân trang** được thiết kế tốt nên là một đối tượng "bao bọc" dữ liệu và bao gồm siêu dữ liệu phân trang rõ ràng.JSON
{
"data": [
{ "id": 101, "name": "Product A" },
{ "id": 102, "name": "Product B" }
],
"pagination": {
"next_cursor": "eJjcmVhdGVkX2F0Ij...",
"has_next_page": true
}
}
Đối với phân trang offset, siêu dữ liệu sẽ trông khác:JSON
{
"data": [
// ... results
],
"metadata": {
"total_results": 8452,
"total_pages": 339,
"current_page": 3,
"per_page": 25
}
}
Cấu trúc này giúp máy khách dễ dàng biết liệu có còn dữ liệu để lấy hay không hoặc hiển thị các điều khiển giao diện người dùng.
Sử dụng Liên kết Siêu phương tiện để Điều hướng (HATEOAS)
Một nguyên tắc cốt lõi của REST là HATEOAS (Hypermedia as the Engine of Application State - Siêu phương tiện là Động cơ của Trạng thái Ứng dụng). Điều này có nghĩa là API nên cung cấp cho máy khách các liên kết để điều hướng đến các tài nguyên hoặc hành động khác. Đối với phân trang, điều này cực kỳ mạnh mẽ. Như được trình bày trong **Tài liệu GitHub**, một cách chuẩn hóa để làm điều này là sử dụng tiêu đề HTTP Link
.
Link: <https://api.example.com/items?page=3>; rel="next", <https://api.example.com/items?page=1>; rel="prev"
Ngoài ra, các liên kết này có thể được đặt trực tiếp trong phần thân phản hồi JSON, điều này thường dễ dàng hơn cho các máy khách JavaScript sử dụng:JSON
"pagination": {
"links": {
"next": "https://api.example.com/items?limit=25&offset=75",
"previous": "https://api.example.com/items?limit=25&offset=25"
}
}
Điều này giải phóng máy khách khỏi việc phải tự xây dựng URL theo cách thủ công.
Cho phép Máy khách Kiểm soát Kích thước Trang
Thực hành tốt là cho phép máy khách **yêu cầu các trang kết quả bổ sung cho các phản hồi đã phân trang** và cũng **thay đổi số lượng kết quả trả về trên mỗi trang**. Điều này thường được thực hiện bằng tham số truy vấn limit
hoặc per_page
. Tuy nhiên, máy chủ luôn phải áp đặt một giới hạn tối đa hợp lý (ví dụ: 100) để ngăn máy khách yêu cầu quá nhiều dữ liệu cùng một lúc và làm quá tải hệ thống.
Kết hợp Phân trang với Lọc và Sắp xếp
Các API thực tế hiếm khi chỉ phân trang; chúng cũng cần hỗ trợ lọc (filtering) và sắp xếp (sorting). Như được trình bày trong các hướng dẫn về các công nghệ như .NET, việc thêm các tính năng này là một yêu cầu phổ biến.
Một yêu cầu phức tạp có thể trông như thế này: GET /api/products?status=published&sort=-created_at&limit=50&page=2
Khi triển khai điều này, điều quan trọng là các tham số lọc và sắp xếp phải được coi là một phần của logic phân trang. Thứ tự sort
phải ổn định và xác định để phân trang hoạt động chính xác. Nếu thứ tự sắp xếp không duy nhất, bạn phải thêm một cột phụ duy nhất để phá vỡ sự trùng lặp (như id
) để đảm bảo thứ tự nhất quán giữa các trang.
Các Ví dụ Triển khai Thực tế
Hãy cùng khám phá cách triển khai các khái niệm này trong các framework phổ biến khác nhau.
Phân trang REST API trong Python với Django REST Framework
Một trong những kết hợp phổ biến nhất để xây dựng API là Python với **Django REST Framework (DRF)**. DRF cung cấp hỗ trợ phân trang mạnh mẽ, tích hợp sẵn, giúp việc bắt đầu trở nên cực kỳ dễ dàng. Nó cung cấp các lớp cho các chiến lược khác nhau:
PageNumberPagination
: Dành cho phân trang offset dựa trên số trang tiêu chuẩn.LimitOffsetPagination
: Dành cho triển khai offset linh hoạt hơn.CursorPagination
: Dành cho phân trang dựa trên con trỏ, hiệu suất cao.
Bạn có thể cấu hình kiểu phân trang mặc định trên toàn cục và sau đó chỉ cần sử dụng ListAPIView
chung, và DRF sẽ xử lý phần còn lại. Đây là một ví dụ điển hình về **phân trang rest api python**.
# In your settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 50
}
# In your views.py
class ProductListView(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
# DRF tự động xử lý toàn bộ logic phân trang!
Xây dựng REST API có Phân trang với Node.js, Express và TypeScript
Trong hệ sinh thái Node.js, bạn thường xây dựng logic phân trang theo cách thủ công, điều này mang lại cho bạn toàn quyền kiểm soát. Phần hướng dẫn này cung cấp cái nhìn tổng quan về khái niệm xây dựng phân trang với **Node.js, Express và TypeScript**.
Đây là một ví dụ đơn giản về việc triển khai phân trang con trỏ:TypeScript
// Trong bộ điều khiển Express của bạn
app.get('/products', async (req: Request, res: Response) =&