0

[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

⭐ 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí