Khi xây dựng các API REST trả về danh sách tài nguyên, điều quan trọng là phải xem xét cách xử lý các tập dữ liệu lớn. Việc trả về hàng nghìn hoặc thậm chí hàng triệu bản ghi trong một phản hồi API duy nhất là không thực tế và có thể dẫn đến các vấn đề hiệu suất đáng kể, tiêu thụ bộ nhớ cao cho cả máy chủ và máy khách, và trải nghiệm người dùng kém. Phân trang (Pagination) là giải pháp tiêu chuẩn cho vấn đề này. Nó bao gồm việc chia một tập dữ liệu lớn thành các phần nhỏ hơn, dễ quản lý hơn gọi là "trang" (pages), sau đó được phục vụ tuần tự. Hướng dẫn này sẽ đưa bạn qua các bước kỹ thuật để triển khai các chiến lược phân trang khác nhau trong các API REST của bạn.
Bạn muốn một nền tảng tích hợp, All-in-One cho Độ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 tất cả nhu cầu của bạn và thay thế Postman với mức giá phải chăng hơn nhiều!
Tại sao Phân trang Lại Cần Thiết?
Trước khi đi sâu vào chi tiết triển khai, hãy cùng điểm qua lý do tại sao phân trang là một tính năng không thể thiếu đối với các API xử lý các bộ sưu tập tài nguyên:
- Hiệu suất: Yêu cầu và truyền lượng lớn dữ liệu có thể chậm. Phân trang giảm kích thước tải trọng của mỗi yêu cầu, dẫn đến thời gian phản hồi nhanh hơn và giảm tải cho máy chủ.
- Tiêu thụ Tài nguyên: Các phản hồi nhỏ hơn tiêu thụ ít bộ nhớ hơn trên máy chủ tạo ra chúng và trên máy khách phân tích cú pháp chúng. Điều này đặc biệt quan trọng đối với các máy khách di động hoặc môi trường có tài nguyên hạn chế.
- Giới hạn Tốc độ và Hạn ngạch (Rate Limiting and Quotas): Nhiều API áp dụng giới hạn tốc độ. Phân trang giúp máy khách tuân thủ các giới hạn này bằng cách tìm nạp dữ liệu theo từng phần nhỏ theo thời gian, thay vì cố gắng lấy tất cả cùng một lúc.
- Trải nghiệm Người dùng: Đối với giao diện người dùng sử dụng API, việc trình bày dữ liệu theo trang thân thiện với người dùng hơn nhiều so với việc làm người dùng choáng ngợp với một danh sách khổng lồ hoặc một cuộn rất dài.
- Hiệu quả Cơ sở dữ liệu: Tìm nạp một tập hợp con dữ liệu nói chung ít gây áp lực lên cơ sở dữ liệu hơn so với việc truy xuất toàn bộ bảng, đặc biệt nếu việc lập chỉ mục (indexing) phù hợp đã được thực hiện.
Các Chiến lược Phân trang Phổ biến
Có một số chiến lược phổ biến để triển khai phân trang, mỗi chiến lược đều có những đánh đổi riêng. Chúng ta sẽ khám phá những chiến lược phổ biến nhất: offset/limit (thường được gọi là phân trang theo trang) và cursor-based (còn được gọi là keyset hoặc seek pagination).
1. Phân trang theo Offset/Limit (hoặc theo Trang)
Đây có lẽ là phương pháp phân trang đơn giản và được áp dụng rộng rãi nhất. Nó hoạt động bằng cách cho phép máy khách chỉ định hai tham số chính:
offset
: Số lượng bản ghi cần bỏ qua từ đầu tập dữ liệu.limit
: Số lượng bản ghi tối đa được trả về trong một trang duy nhất.
Ngoài ra, máy khách có thể chỉ định:
page
: Số trang mà họ muốn truy xuất.pageSize
(hoặcper_page
,limit
): Số lượng bản ghi trên mỗi trang.
Tham số offset
có thể được tính từ page
và pageSize
bằng công thức: offset = (page - 1) * pageSize
.
Các Bước Triển khai Kỹ thuật:
Giả sử chúng ta có một điểm cuối API /items
trả về danh sách các mục.
a. Tham số Yêu cầu API:
Máy khách sẽ thực hiện yêu cầu như:GET /items?offset=20&limit=10
(tìm nạp 10 mục, bỏ qua 20 mục đầu tiên)
hoặcGET /items?page=3&pageSize=10
(tìm nạp trang thứ 3, với 10 mục trên mỗi trang, tương đương với offset=20, limit=10).
Nên đặt các giá trị mặc định cho các tham số này (ví dụ: limit=20
, offset=0
hoặc page=1
, pageSize=20
) nếu máy khách không cung cấp chúng. Đồng thời, áp đặt một giới hạn tối đa cho limit
hoặc pageSize
để ngăn máy khách yêu cầu số lượng bản ghi quá lớn, có thể gây căng thẳng cho máy chủ.
b. Logic Backend (Khái niệm):
Khi máy chủ nhận được yêu cầu này, nó cần dịch các tham số này thành một truy vấn cơ sở dữ liệu.
// Ví dụ trong Java với Spring Boot
@GetMapping("/items")
public ResponseEntity<PaginatedResponse<Item>> getItems(
@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "20") int limit
) {
// Xác thực giới hạn để ngăn chặn lạm dụng
if (limit > 100) {
limit = 100; // Áp đặt giới hạn tối đa
}
List<Item> items = itemRepository.findItemsWithOffsetLimit(offset, limit);
long totalItems = itemRepository.countTotalItems(); // Dành cho metadata
// Xây dựng và trả về phản hồi phân trang
// ...
}
c. Truy vấn Cơ sở dữ liệu (Ví dụ SQL):
Hầu hết các cơ sở dữ liệu quan hệ đều hỗ trợ trực tiếp các mệnh đề offset và limit.
Đối với PostgreSQL hoặc MySQL:
SELECT *
FROM items
ORDER BY created_at DESC -- Sắp xếp nhất quán là rất quan trọng cho phân trang ổn định
LIMIT 10 -- Đây là tham số 'limit'
OFFSET 20; -- Đây là tham số 'offset'
Đối với SQL Server (các phiên bản cũ hơn có thể sử dụng ROW_NUMBER()
):
SELECT *
FROM items
ORDER BY created_at DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;
Đối với Oracle:
SELECT *
FROM (
SELECT i.*, ROWNUM rnum
FROM (
SELECT *
FROM items
ORDER BY created_at DESC
) i
WHERE ROWNUM <= 20 + 10 -- offset + limit
)
WHERE rnum > 20; -- offset
Lưu ý Quan trọng về Sắp xếp: Để phân trang offset/limit đáng tin cậy, tập dữ liệu cơ bản phải được sắp xếp theo một khóa nhất quán và duy nhất (hoặc gần duy nhất), hoặc một sự kết hợp các khóa. Nếu thứ tự của các mục có thể thay đổi giữa các yêu cầu (ví dụ: các mục mới được chèn hoặc các mục hiện có được cập nhật theo cách ảnh hưởng đến thứ tự sắp xếp của chúng), người dùng có thể thấy các mục trùng lặp hoặc bỏ lỡ các mục khi điều hướng giữa các trang. Một lựa chọn phổ biến là sắp xếp theo dấu thời gian tạo (creation timestamp) hoặc ID chính (primary ID).
d. Cấu trúc Phản hồi API:
Một phản hồi phân trang tốt không chỉ bao gồm dữ liệu cho trang hiện tại mà còn cả metadata để giúp máy khách điều hướng.
{
"data": [
// mảng các mục cho trang hiện tại
{ "id": "item_21", "name": "Item 21", ... },
{ "id": "item_22", "name": "Item 22", ... },
// ... tối đa 'limit' mục
{ "id": "item_30", "name": "Item 30", ... }
],
"pagination": {
"offset": 20,
"limit": 10,
"totalItems": 5000, // Tổng số mục có sẵn
"totalPages": 500, // Tính bằng ceil(totalItems / limit)
"currentPage": 3 // Tính bằng (offset / limit) + 1
},
"links": { // Liên kết HATEOAS để điều hướng
"self": "/items?offset=20&limit=10",
"first": "/items?offset=0&limit=10",
"prev": "/items?offset=10&limit=10", // Null nếu ở trang đầu tiên
"next": "/items?offset=30&limit=10", // Null nếu ở trang cuối cùng
"last": "/items?offset=4990&limit=10"
}
}
Việc cung cấp các liên kết HATEOAS (Hypermedia as the Engine of Application State) (self
, first
, prev
, next
, last
) là một thực hành tốt nhất của REST. Nó cho phép máy khách điều hướng qua các trang mà không cần phải tự xây dựng URL.
Ưu điểm của Phân trang Offset/Limit:
- Đơn giản: Dễ hiểu và triển khai.
- Điều hướng theo Trạng thái (Stateful Navigation): Cho phép điều hướng trực tiếp đến bất kỳ trang cụ thể nào (ví dụ: "chuyển đến trang 50").
- Được Hỗ trợ Rộng rãi: Hỗ trợ cơ sở dữ liệu cho
OFFSET
vàLIMIT
là phổ biến.
Nhược điểm của Phân trang Offset/Limit:
- Hiệu suất Giảm sút với Offset Lớn: Khi giá trị
offset
tăng lên, cơ sở dữ liệu có thể trở nên chậm hơn. Cơ sở dữ liệu thường vẫn phải quét qua tất cả các hàngoffset + limit
trước khi loại bỏ các hàngoffset
. Điều này có thể không hiệu quả đối với các trang sâu. - Lệch Dữ liệu/Mục Bị Bỏ lỡ: Nếu các mục mới được thêm hoặc các mục hiện có bị xóa khỏi tập dữ liệu trong khi người dùng đang phân trang, "cửa sổ" dữ liệu có thể bị dịch chuyển. Điều này có thể khiến người dùng thấy cùng một mục trên hai trang khác nhau hoặc bỏ lỡ hoàn toàn một mục. Điều này đặc biệt có vấn đề với các tập dữ liệu được cập nhật thường xuyên. Ví dụ, nếu bạn đang ở trang 2 (các mục 11-20) và một mục mới được thêm vào đầu danh sách, khi bạn yêu cầu trang 3, mục trước đây là 21 giờ là mục 22. Bạn có thể bỏ lỡ mục mới 21 hoặc thấy các mục trùng lặp tùy thuộc vào thời điểm chính xác và các mẫu xóa.
2. Phân trang theo Con trỏ (Cursor-Based) (Keyset/Seek)
Phân trang theo con trỏ giải quyết một số hạn chế của offset/limit, đặc biệt là hiệu suất với các tập dữ liệu lớn và các vấn đề về tính nhất quán của dữ liệu. Thay vì dựa vào một offset tuyệt đối, nó sử dụng một "con trỏ" (cursor) trỏ đến một mục cụ thể trong tập dữ liệu. Máy khách sau đó yêu cầu các mục "sau" hoặc "trước" con trỏ này.
Con trỏ thường là một chuỗi mờ (opaque string) mã hóa giá trị của khóa sắp xếp của mục cuối cùng được truy xuất trên trang trước.
Các Bước Triển khai Kỹ thuật:
a. Tham số Yêu cầu API:
Máy khách sẽ thực hiện yêu cầu như:GET /items?limit=10
(cho trang đầu tiên)
Và cho các trang tiếp theo:GET /items?limit=10&after_cursor=opaquestringrepresentinglastitemid
Hoặc, để phân trang ngược (ít phổ biến hơn nhưng có thể):GET /items?limit=10&before_cursor=opaquestringrepresentingfirstitemid
Tham số limit
vẫn xác định kích thước trang.
b. Con trỏ là gì?
Một con trỏ nên:
- Mờ đối với máy khách: Máy khách không cần hiểu cấu trúc nội bộ của nó. Nó chỉ nhận con trỏ từ một phản hồi và gửi lại trong yêu cầu tiếp theo.
- Dựa trên (các) cột duy nhất và được sắp xếp tuần tự: Thông thường, đây là ID chính (nếu nó tuần tự như UUIDv1 hoặc một chuỗi cơ sở dữ liệu) hoặc một cột dấu thời gian. Nếu một cột duy nhất không đủ độc nhất (ví dụ: nhiều mục có thể có cùng dấu thời gian), một sự kết hợp các cột sẽ được sử dụng (ví dụ:
timestamp
+id
). - Có thể Mã hóa và Giải mã: Thường được mã hóa Base64 để đảm bảo an toàn cho URL. Nó có thể đơn giản là chính ID, hoặc một đối tượng JSON
{ "last_id": 123, "last_timestamp": "2023-10-27T10:00:00Z" }
sau đó được mã hóa Base64.
c. Logic Backend (Khái niệm):
// Ví dụ trong Java với Spring Boot
@GetMapping("/items")
public ResponseEntity<CursorPaginatedResponse<Item>> getItems(
@RequestParam(defaultValue = "20") int limit,
@RequestParam(required = false) String afterCursor
) {
// Xác thực giới hạn
if (limit > 100) {
limit = 100;
}
// Giải mã con trỏ để lấy thuộc tính của mục cuối cùng đã thấy
// ví dụ: LastSeenItemDetails lastSeen = decodeCursor(afterCursor);
// Nếu afterCursor là null, đó là trang đầu tiên.
List<Item> items;
if (afterCursor != null) {
DecodedCursor decoded = decodeCursor(afterCursor); // ví dụ: { lastId: "some_uuid", lastCreatedAt: "timestamp" }
items = itemRepository.findItemsAfter(decoded.getLastCreatedAt(), decoded.getLastId(), limit);
} else {
items = itemRepository.findFirstPage(limit);
}
String nextCursor = null;
if (!items.isEmpty() && items.size() == limit) {
// Giả sử các mục được sắp xếp, mục cuối cùng trong danh sách được sử dụng để tạo con trỏ tiếp theo
Item lastItemOnPage = items.get(items.size() - 1);
nextCursor = encodeCursor(lastItemOnPage.getCreatedAt(), lastItemOnPage.getId());
}
// Xây dựng và trả về phản hồi phân trang theo con trỏ
// ...
}
// Phương thức trợ giúp để mã hóa/giải mã con trỏ
// private DecodedCursor decodeCursor(String cursor) { ... }
// private String encodeCursor(Timestamp createdAt, String id) { ... }
d. Truy vấn Cơ sở dữ liệu (Ví dụ SQL):
Điểm mấu chốt là sử dụng mệnh đề WHERE
để lọc các bản ghi dựa trên khóa sắp xếp từ con trỏ. Mệnh đề ORDER BY
phải phù hợp với cấu tạo của con trỏ.
Giả sử sắp xếp theo created_at
(giảm dần) và sau đó theo id
(giảm dần) làm khóa phụ để đảm bảo thứ tự ổn định nếu created_at
không duy nhất:
Đối với trang đầu tiên:
SELECT *
FROM items
ORDER BY created_at DESC, id DESC
LIMIT 10;
Đối với các trang tiếp theo, nếu con trỏ được giải mã thành last_created_at_from_cursor
và last_id_from_cursor
:
SELECT *
FROM items
WHERE (created_at, id) < (CAST('last_created_at_from_cursor' AS TIMESTAMP), CAST('last_id_from_cursor' AS UUID)) -- Hoặc các kiểu dữ liệu phù hợp
-- Đối với thứ tự tăng dần, sẽ là >
-- So sánh tuple (created_at, id) < (val1, val2) là một cách viết ngắn gọn cho:
-- WHERE created_at < 'last_created_at_from_cursor'
-- OR (created_at = 'last_created_at_from_cursor' AND id < 'last_id_from_cursor')
ORDER BY created_at DESC, id DESC
LIMIT 10;
Loại truy vấn này rất hiệu quả, đặc biệt nếu có chỉ mục trên (created_at, id)
. Cơ sở dữ liệu có thể trực tiếp "tìm kiếm" (seek) đến điểm bắt đầu mà không cần quét các hàng không liên quan.
e. Cấu trúc Phản hồi API:
{
"data": [
// mảng các mục cho trang hiện tại
{ "id": "item_N", "createdAt": "2023-10-27T10:05:00Z", ... },
// ... tối đa 'limit' mục
{ "id": "item_M", "createdAt": "2023-10-27T10:00:00Z", ... }
],
"pagination": {
"limit": 10,
"hasNextPage": true, // boolean cho biết có còn dữ liệu nữa không
"nextCursor": "base64encodedcursorstringforitem_M" // chuỗi mờ
// Có thể có "prevCursor" nếu hỗ trợ con trỏ hai chiều
},
"links": {
"self": "/items?limit=10&after_cursor=current_request_cursor_if_any",
"next": "/items?limit=10&after_cursor=base64encodedcursorstringforitem_M" // Null nếu không có trang tiếp theo
}
}
Lưu ý rằng phân trang theo con trỏ thường không cung cấp totalPages
hoặc totalItems
vì việc tính toán chúng sẽ yêu cầu quét toàn bộ bảng, làm mất đi một số lợi ích về hiệu suất. Nếu những thông tin này thực sự cần thiết, có thể cung cấp một điểm cuối riêng biệt hoặc một ước tính.
Ưu điểm của Phân trang theo Con trỏ:
- Hiệu suất trên Tập dữ liệu Lớn: Nói chung hoạt động tốt hơn offset/limit đối với phân trang sâu, vì cơ sở dữ liệu có thể tìm kiếm hiệu quả đến vị trí con trỏ bằng cách sử dụng chỉ mục.
- Ổn định trong Tập dữ liệu Động: Ít bị ảnh hưởng bởi các mục bị bỏ lỡ hoặc trùng lặp khi dữ liệu thường xuyên được thêm hoặc xóa, vì con trỏ neo vào một mục cụ thể. Nếu một mục trước con trỏ bị xóa, nó không ảnh hưởng đến các mục tiếp theo.
- Phù hợp với Cuộn Vô hạn (Infinite Scrolling): Mô hình "trang tiếp theo" phù hợp tự nhiên với giao diện người dùng cuộn vô hạn.
Nhược điểm của Phân trang theo Con trỏ:
- Không có "Chuyển đến Trang": Người dùng không thể điều hướng trực tiếp đến một số trang tùy ý (ví dụ: "trang 5"). Điều hướng hoàn toàn theo tuần tự (tiếp theo/trước đó).
- Triển khai Phức tạp hơn: Việc định nghĩa và quản lý con trỏ, đặc biệt với nhiều cột sắp xếp hoặc thứ tự sắp xếp phức tạp, có thể phức tạp hơn.
- Hạn chế Sắp xếp: Thứ tự sắp xếp phải cố định và dựa trên các cột được sử dụng cho con trỏ. Thay đổi thứ tự sắp xếp linh hoạt với con trỏ là phức tạp.
Chọn Chiến lược Phù hợp
Việc lựa chọn giữa phân trang offset/limit và cursor-based phụ thuộc vào các yêu cầu cụ thể của bạn:
- Offset/Limit thường đủ nếu:
- Tập dữ liệu tương đối nhỏ hoặc không thay đổi thường xuyên.
- Khả năng chuyển đến các trang tùy ý là một tính năng quan trọng.
- Sự đơn giản trong triển khai là ưu tiên cao.
- Hiệu suất cho các trang rất sâu không phải là mối quan tâm lớn.
- Cursor-Based nói chung được ưu tiên nếu:
- Bạn đang xử lý các tập dữ liệu rất lớn, thường xuyên thay đổi.
- Hiệu suất ở quy mô lớn và tính nhất quán của dữ liệu trong quá trình phân trang là tối quan trọng.
- Điều hướng tuần tự (như cuộn vô hạn) là trường hợp sử dụng chính.
- Bạn không cần hiển thị tổng số trang hoặc cho phép chuyển đến các trang cụ thể.
Trong một số hệ thống, một cách tiếp cận kết hợp cũng được sử dụng, hoặc các chiến lược khác nhau được cung cấp cho các trường hợp sử dụng hoặc điểm cuối khác nhau.
Các Thực hành Tốt nhất khi Triển khai Phân trang
Bất kể chiến lược đã chọn là gì, hãy tuân thủ các thực hành tốt nhất sau:
- Đặt tên Tham số Nhất quán: Sử dụng tên rõ ràng và nhất quán cho các tham số phân trang của bạn (ví dụ:
limit
,offset
,page
,pageSize
,after_cursor
,before_cursor
). Tuân thủ một quy ước (ví dụ:camelCase
hoặcsnake_case
) trong toàn bộ API của bạn. - Cung cấp Liên kết Điều hướng (HATEOAS): Như đã trình bày trong các ví dụ phản hồi, bao gồm các liên kết cho
self
,next
,prev
,first
, vàlast
(nếu áp dụng). Điều này làm cho API dễ khám phá hơn và tách rời máy khách khỏi logic xây dựng URL. - Giá trị Mặc định và Giới hạn Tối đa:
- Luôn đặt các giá trị mặc định hợp lý cho
limit
(ví dụ: 10 hoặc 25). - Áp đặt một giới hạn tối đa cho
limit
để ngăn máy khách yêu cầu quá nhiều dữ liệu và làm quá tải máy chủ (ví dụ: tối đa 100 bản ghi trên mỗi trang). Trả về lỗi hoặc giới hạn giá trị nếu một giá trị không hợp lệ được yêu cầu.
- Tài liệu API Rõ ràng: Tài liệu hóa chiến lược phân trang của bạn một cách kỹ lưỡng:
- Giải thích các tham số được sử dụng.
- Cung cấp các yêu cầu và phản hồi mẫu.
- Làm rõ các giới hạn mặc định và tối đa.
- Giải thích cách sử dụng con trỏ (nếu áp dụng), mà không tiết lộ cấu trúc nội bộ của chúng nếu chúng được coi là mờ.
- Sắp xếp Nhất quán: Đảm bảo rằng dữ liệu cơ bản được sắp xếp nhất quán cho mọi yêu cầu phân trang. Đối với offset/limit, điều này là rất quan trọng để tránh lệch dữ liệu. Đối với cursor-based, thứ tự sắp xếp quyết định cách con trỏ được xây dựng và giải thích. Sử dụng một cột phụ duy nhất (như ID chính) nếu cột sắp xếp chính có thể có giá trị trùng lặp.
- Xử lý các Trường hợp Ngoại lệ (Edge Cases):
- Kết quả Rỗng: Trả về một mảng dữ liệu rỗng và metadata phân trang phù hợp (ví dụ:
totalItems: 0
hoặchasNextPage: false
). - Tham số Không hợp lệ: Trả về lỗi
400 Bad Request
nếu máy khách cung cấp các tham số phân trang không hợp lệ (ví dụ: limit âm, số trang không phải số nguyên). - Không tìm thấy Con trỏ (đối với cursor-based): Nếu một con trỏ được cung cấp không hợp lệ hoặc trỏ đến một mục đã bị xóa, hãy quyết định hành vi: trả về
404 Not Found
hoặc400 Bad Request
, hoặc gracefully fall back về trang đầu tiên.
- Cân nhắc về Tổng số (Total Count):
- Đối với offset/limit, việc cung cấp
totalItems
vàtotalPages
là phổ biến. Hãy lưu ý rằngCOUNT(*)
có thể chậm trên các bảng rất lớn. Khám phá các tối ưu hóa hoặc ước tính cụ thể của cơ sở dữ liệu nếu điều này trở thành điểm nghẽn. - Đối với cursor-based,
totalItems
thường bị bỏ qua để cải thiện hiệu suất. Nếu cần, hãy cân nhắc cung cấp một số lượng ước tính hoặc một điểm cuối riêng biệt tính toán nó (có thể không đồng bộ).
- Xử lý Lỗi: Trả về các mã trạng thái HTTP phù hợp cho các lỗi (ví dụ:
400
cho đầu vào xấu,500
cho lỗi máy chủ trong quá trình tìm nạp dữ liệu). - Bảo mật: Mặc dù không trực tiếp là cơ chế phân trang, hãy đảm bảo rằng dữ liệu đang được phân trang tuân thủ các quy tắc ủy quyền. Người dùng chỉ nên có thể phân trang qua dữ liệu mà họ được phép xem.
- Bộ nhớ đệm (Caching): Các phản hồi phân trang thường có thể được lưu vào bộ nhớ đệm. Đối với phân trang dựa trên offset,
GET /items?page=2&pageSize=10
có khả năng lưu bộ nhớ đệm cao. Đối với phân trang dựa trên con trỏ,GET /items?limit=10&after_cursor=XYZ
cũng có thể lưu bộ nhớ đệm. Đảm bảo chiến lược bộ nhớ đệm của bạn hoạt động tốt với cách các liên kết phân trang được tạo và sử dụng. Cần xem xét các chiến lược vô hiệu hóa nếu dữ liệu cơ bản thay đổi thường xuyên.
Các Chủ đề Nâng cao (Đề cập Ngắn gọn)
- Cuộn Vô hạn (Infinite Scrolling): Phân trang theo con trỏ rất phù hợp với giao diện người dùng cuộn vô hạn. Máy khách tìm nạp trang đầu tiên và khi người dùng cuộn gần cuối, nó sử dụng
nextCursor
để tìm nạp tập hợp các mục tiếp theo. - Phân trang với Lọc và Sắp xếp Phức tạp: Khi kết hợp phân trang với các tham số lọc và sắp xếp động, hãy đảm bảo rằng:
- Đối với offset/limit: Số lượng
totalItems
phản ánh chính xác tập dữ liệu đã lọc. - Đối với cursor-based: Con trỏ mã hóa trạng thái của cả sắp xếp và lọc nếu chúng ảnh hưởng đến ý nghĩa của "tiếp theo". Điều này có thể làm phức tạp đáng kể thiết kế con trỏ. Thông thường, nếu bộ lọc hoặc thứ tự sắp xếp thay đổi, phân trang sẽ được đặt lại về "trang đầu tiên" của chế độ xem mới.
- Phân trang GraphQL: GraphQL có cách xử lý phân trang chuẩn hóa riêng, thường được gọi là "Connections". Nó thường sử dụng phân trang theo con trỏ và có cấu trúc được định nghĩa để trả về các cạnh (các mục có con trỏ) và thông tin trang. Nếu bạn đang sử dụng GraphQL, hãy tuân thủ các quy ước của nó (ví dụ: đặc tả Relay Cursor Connections).
Kết luận
Triển khai phân trang một cách chính xác là nền tảng để xây dựng các API REST có khả năng mở rộng và thân thiện với người dùng. Mặc dù phân trang offset/limit đơn giản hơn để bắt đầu, phân trang theo con trỏ mang lại hiệu suất và tính nhất quán vượt trội cho các tập dữ liệu lớn, động. Bằng cách hiểu chi tiết kỹ thuật của từng chiến lược, chọn chiến lược phù hợp nhất với nhu cầu ứng dụng của bạn và tuân thủ các thực hành tốt nhất cho việc triển khai và thiết kế API, bạn có thể đảm bảo rằng API của mình phân phối dữ liệu hiệu quả đến máy khách, bất kể quy mô nào. Hãy nhớ luôn ưu tiên tài liệu rõ ràng và xử lý lỗi mạnh mẽ để mang lại trải nghiệm mượt mà cho người dùng API.