20 - Pagination, Sorting, Filtering
เป้าหมายของบท
Section titled “เป้าหมายของบท”บทนี้จะปรับ endpoint รายชื่อผู้ใช้ให้รองรับข้อมูลจำนวนมาก ด้วย pagination, sorting และ filtering
หลังจบบทนี้ endpoint รายชื่อผู้ใช้ควรรองรับ query parameter แบบนี้:
GET /api/v1/users?page=0&size=20&sortBy=createdAt&direction=desc&keyword=johnผู้อ่านควรเข้าใจ:
- ทำไมไม่ควรใช้
findAll()กับข้อมูลจำนวนมาก - ใช้
Pageable,PageRequest,Sortอย่างไร - ส่ง pagination metadata ให้ client อย่างไร
- filtering เบื้องต้นด้วย keyword ทำอย่างไร
- จำกัด
sizeเพื่อป้องกัน request ใหญ่เกินไป
ทำไม findAll ไม่พอ
Section titled “ทำไม findAll ไม่พอ”ตอนมี user 10 คน findAll() ยังไม่เป็นปัญหา
แต่ถ้ามี user 100,000 คน การส่งข้อมูลทั้งหมดกลับไปใน request เดียวจะทำให้:
- database ทำงานหนัก
- backend ใช้ memory มาก
- network ช้า
- frontend render ช้า
- user experience แย่
ดังนั้น endpoint list ควรดึงเป็นหน้า
รูปแบบ query parameter
Section titled “รูปแบบ query parameter”เราจะใช้:
| Parameter | ตัวอย่าง | ความหมาย |
|---|---|---|
page | 0 | เลขหน้า เริ่มจาก 0 |
size | 20 | จำนวนรายการต่อหน้า |
sortBy | createdAt | field ที่ใช้จัดเรียง |
direction | desc | เรียงจากมากไปน้อยหรือจากน้อยไปมาก |
keyword | john | ค้นหา username/email |
ตัวอย่าง:
GET /api/v1/users?page=0&size=10&sortBy=username&direction=asc&keyword=joสร้าง PageResponse
Section titled “สร้าง PageResponse”สร้างไฟล์:
src/main/java/com/example/backendapi/common/PageResponse.javapackage com.example.backendapi.common;
import java.util.List;import org.springframework.data.domain.Page;
public record PageResponse<T>( List<T> content, int page, int size, long totalElements, int totalPages, boolean first, boolean last) { public static <T> PageResponse<T> from(Page<T> page) { return new PageResponse<>( page.getContent(), page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages(), page.isFirst(), page.isLast() ); }}PageResponse ทำให้ frontend รู้ว่า:
- ตอนนี้อยู่หน้าไหน
- มีทั้งหมดกี่หน้า
- มีทั้งหมดกี่ record
- เป็นหน้าแรกหรือหน้าสุดท้ายไหม
เพิ่ม method ใน UserRepository
Section titled “เพิ่ม method ใน UserRepository”เปิด UserRepository แล้วเพิ่ม method ค้นหาด้วย keyword:
Page<User> findByUsernameContainingIgnoreCaseOrEmailContainingIgnoreCase( String username, String email, Pageable pageable);พร้อม import:
import org.springframework.data.domain.Page;import org.springframework.data.domain.Pageable;method นี้ค้นหา username หรือ email ที่มี keyword โดยไม่สนตัวพิมพ์เล็กใหญ่
ปรับ UserService
Section titled “ปรับ UserService”เพิ่ม method:
public PageResponse<UserResponse> findAll( int page, int size, String sortBy, String direction, String keyword) { int safeSize = Math.min(size, 100); Sort sort = direction.equalsIgnoreCase("desc") ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, safeSize, sort); Page<User> users;
if (keyword == null || keyword.isBlank()) { users = userRepository.findAll(pageable); } else { users = userRepository .findByUsernameContainingIgnoreCaseOrEmailContainingIgnoreCase( keyword, keyword, pageable ); }
return PageResponse.from(users.map(this::toResponse));}จำกัด size ไม่เกิน 100 เพื่อป้องกัน client ส่ง size=100000
ปรับ UserController
Section titled “ปรับ UserController”@GetMappingpublic ApiResponse<PageResponse<UserResponse>> findAll( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "createdAt") String sortBy, @RequestParam(defaultValue = "desc") String direction, @RequestParam(required = false) String keyword) { PageResponse<UserResponse> users = userService.findAll( page, size, sortBy, direction, keyword );
return ApiResponse.ok("Users found", users);}ตัวอย่าง response
Section titled “ตัวอย่าง response”{ "success": true, "message": "Users found", "data": { "content": [ { "id": 1, "username": "john", "email": "john@example.com", "role": "USER", "status": "ACTIVE" } ], "page": 0, "size": 20, "totalElements": 1, "totalPages": 1, "first": true, "last": true }, "timestamp": "2026-06-05T15:30:00"}ควรระวัง sortBy
Section titled “ควรระวัง sortBy”ตัวอย่างนี้รับ sortBy จาก client ตรง ๆ เพื่อให้เห็น flow ง่าย
ในงานจริงควร whitelist field ที่อนุญาต เช่น:
idusernameemailcreatedAtเพราะถ้า client ส่ง field ที่ไม่มีอยู่ เช่น sortBy=abc อาจเกิด error จาก JPA
Checkpoint ปิดภาค Better API
Section titled “Checkpoint ปิดภาค Better API”หลังจบบทนี้ API ควรมีคุณภาพดีขึ้น:
- validate request body
- มี error response กลาง
- ใช้ DTO แยกจาก entity
- success response สม่ำเสมอ
- list endpoint รองรับ pagination, sorting, filtering
นี่คือฐานที่ดีสำหรับเข้าสู่ภาค Login/Register เพราะ auth API ต้องการ validation, error handling และ DTO ที่ชัดเจน
แบบฝึกหัดท้ายบท
Section titled “แบบฝึกหัดท้ายบท”- เพิ่ม
PageResponse - ปรับ
GET /api/v1/usersให้รับpageและsize - เพิ่ม sorting ด้วย
sortByและdirection - เพิ่ม keyword search ด้วย username/email
- จำกัด
sizeสูงสุดไม่เกิน 100 - ทดสอบ keyword ที่ไม่เจอข้อมูล