[Java Backend Zero to Hello] [Phase 6] BÀI 6.7: PAGINATION & SORTING
📚 Series: Java Backend Zero to Hello 📂 Phân đoạn: Phase 6: REST API & Best Practices 📖 Nội dung: BÀI 6.7: PAGINATION & SORTING 💡 Khóa học lập trình Backend Java & Spring Boot chuẩn doanh nghiệp từ con số 0.
BÀI 6.7: PAGINATION & SORTING
Mục tiêu
- Implement phân trang hiệu quả
- Sắp xếp linh hoạt
- Tối ưu performance
1. CÁC LOẠI PAGINATION
1.1 Offset-based (Page/Size)
Phổ biến nhất, dễ hiểu.
GET /api/users?page=1&size=20
Ưu điểm: Dễ implement, jump to page Nhược điểm: Chậm với dữ liệu lớn, không ổn định khi data thay đổi
1.2 Cursor-based
Dùng cursor (thường là ID/timestamp) để lấy trang tiếp.
GET /api/users?cursor=abc123&limit=20
Ưu điểm: Nhanh, ổn định Nhược điểm: Không jump to page
1.3 Keyset-based
Tương tự cursor, dùng giá trị cuối cùng.
GET /api/users?lastId=100&limit=20
2. OFFSET-BASED VỚI SPRING DATA
2.1 Pageable
@GetMapping
public Page<UserResponse> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "asc") String direction) {
Sort.Direction dir = Sort.Direction.fromString(direction);
Pageable pageable = PageRequest.of(page, size, Sort.by(dir, sortBy));
return userService.findAll(pageable);
}
2.2 Service
public Page<UserResponse> findAll(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toResponse);
}
2.3 Response
{
"content": [
{ "id": 1, "name": "An" },
{ "id": 2, "name": "Bình" }
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": { "sorted": true }
},
"totalElements": 100,
"totalPages": 10,
"size": 10,
"number": 0,
"first": true,
"last": false,
"numberOfElements": 10
}
3. CUSTOM PAGINATION RESPONSE
public record PageResponse<T>(
List<T> data,
Pagination pagination
) {
public static <T> PageResponse<T> from(Page<T> page) {
return new PageResponse<>(
page.getContent(),
new Pagination(
page.getNumber(),
page.getSize(),
page.getTotalElements(),
page.getTotalPages(),
page.isFirst(),
page.isLast()
)
);
}
}
public record Pagination(
int page,
int size,
long total,
int totalPages,
boolean first,
boolean last
) {}
4. SORTING
4.1 Single field
Sort.by("name").ascending()
Sort.by("createdAt").descending()
4.2 Multiple fields
Sort.by(
Sort.Order.asc("lastName"),
Sort.Order.desc("firstName")
);
4.3 Từ query string
@GetMapping
public Page<UserResponse> getAll(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String sort) {
Pageable pageable = PageRequest.of(page, size, parseSort(sort));
return userService.findAll(pageable);
}
private Sort parseSort(String sort) {
if (sort == null || sort.isEmpty()) {
return Sort.unsorted();
}
String[] parts = sort.split(",");
String field = parts[0];
Sort.Direction direction = parts.length > 1 && parts[1].equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
return Sort.by(direction, field);
}
URL: /api/users?sort=name,desc
5. CURSOR-BASED PAGINATION
5.1 Entity
public record CursorPage<T>(
List<T> data,
String nextCursor,
boolean hasNext
) {}
5.2 Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.id > :cursor ORDER BY u.id ASC LIMIT :limit")
List<User> findByCursor(@Param("cursor") Long cursor,
@Param("limit") int limit);
}
5.3 Service
public CursorPage<UserResponse> findByCursor(Long cursor, int limit) {
Long effectiveCursor = cursor != null ? cursor : 0L;
List<User> users = userRepository.findByCursor(effectiveCursor, limit + 1);
boolean hasNext = users.size() > limit;
if (hasNext) {
users = users.subList(0, limit);
}
String nextCursor = hasNext
? String.valueOf(users.get(users.size() - 1).getId())
: null;
return new CursorPage<>(
users.stream().map(userMapper::toResponse).toList(),
nextCursor,
hasNext
);
}
5.4 Controller
@GetMapping
public CursorPage<UserResponse> getAll(
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "20") int limit) {
return userService.findByCursor(cursor, limit);
}
6. FILTERING
6.1 Specification
public class UserSpecifications {
public static Specification<User> hasName(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(root.get("name"), "%" + name + "%");
}
public static Specification<User> hasMinAge(Integer age) {
return (root, query, cb) ->
age == null ? null : cb.greaterThanOrEqualTo(root.get("age"), age);
}
public static Specification<User> hasStatus(UserStatus status) {
return (root, query, cb) ->
status == null ? null : cb.equal(root.get("status"), status);
}
}
6.2 Service
public Page<UserResponse> search(UserSearchCriteria criteria, Pageable pageable) {
Specification<User> spec = Specification.where(null);
if (criteria.name() != null) {
spec = spec.and(UserSpecifications.hasName(criteria.name()));
}
if (criteria.minAge() != null) {
spec = spec.and(UserSpecifications.hasMinAge(criteria.minAge()));
}
if (criteria.status() != null) {
spec = spec.and(UserSpecifications.hasStatus(criteria.status()));
}
return userRepository.findAll(spec, pageable)
.map(userMapper::toResponse);
}
7. PERFORMANCE TIPS
7.1 Index cho cột sort/filter
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_name ON users(name);
7.2 Tránh COUNT(*) khi không cần
// Slice thay vì Page - không count
Slice<User> findByStatus(UserStatus status, Pageable pageable);
7.3 Giới hạn max page size
@Validated
@GetMapping
public Page<UserResponse> getAll(
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size) {
// ...
}
7.4 Projection cho list
// Chỉ select field cần thiết
@Query("SELECT new com.example.dto.UserSummary(u.id, u.name) FROM User u")
Page<UserSummary> findAllSummary(Pageable pageable);
8. BÀI TẬP THỰC HÀNH
Bài 1: User Pagination
Implement API user với pagination, sort, filter.
Bài 2: Cursor Pagination
Implement cursor-based pagination cho feed.
Bài 3: Search
Tạo API search với nhiều tiêu chí.
9. TÓM TẮT
| Loại | Ưu điểm | Nhược điểm |
|---|---|---|
| Offset | Dễ dùng, jump page | Chậm với data lớn |
| Cursor | Nhanh, ổn định | Không jump page |
| Keyset | Nhanh | Cần field unique |
Bài tiếp theo: 6.8 File Upload/Download
🧭 Điều hướng bài học
- ⬅️ Bài trước: BÀI 6.6: CLEAN CODE
- ➡️ Bài tiếp theo: BÀI 6.8: FILE UPLOAD/DOWNLOAD
- 📑 Toàn bộ lộ trình: Java Backend Zero to Hello
⭐ Hãy bookmark (clip) lại series để tiện theo dõi các bài học tiếp theo nhé!
All rights reserved