Hế lô anh em ✌️✌️✌️
Phân trang (Paging) và Sắp xếp (Sorting) có lẽ là những khái niệm không còn quá xa lạ, đặc biệt là đối với các anh em lập trình viên web.
Và chúng ta đều biết vai trò của hai thao tác này khi làm việc với tập dữ liệu lớn là như thế nào! Vì vậy trong bài viết hôm này mình sẽ cùng anh em tìm hiểu cách thức triển khai hai thao tác này thông qua Spring Data JPA.
1. Paging và Sorting là gì?
Mình nghĩ nhiều anh em nắm được hai khái niệm này rồi nhưng mình vẫn muốn giải thích lại để anh em nào chưa nắm được có thể dễ hình dung hơn.
1.1 - Paging
Chúng ta vẫn hay gọi đây là thao tác phân trang nhưng đó là đứng ở phía client còn đứng ở phía server có thể hiểu đây là thao tác tìm kiếm dữ liệu theo điều kiện.
Giả sử mình cần trả ra dữ liệu cho client hiển thị lên một table trên giao diện và mình sẽ có hai cách.
Cách 1: Trả ra toàn bộ dữ liệu tìm kiếm được và phía client sẽ tự động phân trang trên tập dữ liệu đó. Với cách này nếu dữ liệu ít thì không sao nhưng nếu lên tới hàng ngàn bản ghi thì phía server sẽ chịu tải rất nặng vì mỗi lần request phải lấy ra cả ngàn bản ghi.
Cách 2: Trả một tập vừa đủ các bản ghi để cho client hiển thị (ví dụ 15 bản ghi cho mỗi trang). Nếu client muốn xem tiếp thì chọn trang tiếp theo. Với cách này mỗi lần chọn trang chúng ta lại phải request lên server để trả về tập dữ liệu tương ứng nhưng đổi lại đầu server sẽ chịu tải ít hơn do không phải lấy lên toàn bộ dữ liệu.
Tóm lại, bản chất của việc phân trang chính là chúng ta tìm cách triển khai cách làm số 2
1.2 - Sorting
Cùng với phân trang thì sắp xếp cũng là một việc rất hay được sử dụng khi chúng ta thực hiện các thao tác tìm kiếm dữ liệu.
Vậy vai trò của Sorting là gì?
Đơn giản là chúng ta muốn sắp xếp dữ liệu theo một điều kiện nào đó để dễ quan sát hay đôi khi để tiện cho việc thống kê và sàng lọc dữ liệu...
Lưu ý rằng giống với phân trang thì sắp xếp cũng sẽ được thực hiện hết ở tầng cơ sở dữ liệu cho nên nếu kết hợp cả hai thao tác này thì anh em nên lưu ý về mặt hiệu năng nhé.
2. Paging & Sorting sử dụng Spring Data JPA?
Nếu anh em nào thắc mắc Spring Data JPA là gì thì mình sẽ giành riêng một bài viết khác để giới thiệu còn trong phần tiếp theo này mình sẽ cùng anh em tập trung vào việc làm sao thực hiện Paging và Sorting sử dụng Spring Data JPA.
2.1 - Công cụ, công nghệ
+ Spring Boot (ver 2.6.7): Để triển khai code
+ Maven (ver 4.0.0): Quản lý thư viện
+ MariaDB (ver 10.6.7): Cơ sở dữ liệu
+ Postman: Test APIs
+ IntelliJ Idea: Viết và biên dịch code
Note: Anh em có thể sử dụng các công cụ và phiên bản khác tương tự sao cho phù hợp với thói quen cũng như sở thích của anh em nhé.
Về cách khởi tạo ứng dụng Spring Boot thì anh em có thể tham khảo ở bài viết này mình đã hướng dẫn khá chi tiết rồi.
Và để sử dụng được Spring Data JPA anh em thêm cho mình thư viện sau vào file pom.xml
của ứng dụng Spring Boot vừa tạo.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
Tiếp theo để có dữ liệu test mình tạo một bảng trong cơ sở dữ liệu là bảng POSTS với các trường thông tin như sau:
CREATE TABLE `POSTS` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`POST_TITLE` varchar(255) DEFAULT NULL COMMENT 'Tiêu đề bài viết',
`POST_CONTENT` text DEFAULT NULL COMMENT 'Nội dung bài viết',
`POST_LIKE` bigint(20) DEFAULT NULL COMMENT 'Số lượt like',
PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=457 DEFAULT CHARSET=utf8mb4;
Sau khi tạo xong bảng mình thêm dữ liệu bằng cách chạy procedure bên dưới. Sở dĩ mình viết proceduce là vì bài viết này mình đang ví dụ về phân trang nên cần nhiều dữ liệu một chút mà thêm tay thì hơi cực nên mình viết cho anh em cùng dùng luôn😉.
CREATE DEFINER=`root`@`localhost` PROCEDURE `b2a`.`insert_data`()
BEGIN
DECLARE i int DEFAULT 1;
WHILE i <= 100 DO
INSERT INTO posts (post_title, post_content, post_like) VALUES (CONCAT("title ", i), CONCAT("content ", i), i);
SET i = i + 1;
END WHILE;
END
Anh em có thể execute hai đoạn script bên trên để tạo bảng vào thêm dữ liệu sau đó chúng ta sẽ bắt đâu triển khai code ở phần tiếp theo.
2.2 - Coding
■ Đầu tiên anh em kết nối đến cơ sở dữ liệu thông qua file application.properties
theo cấu hình bên dưới.
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://<server's address>:<port>/<database's name>
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name =com.mysql.jdbc.Driver
Note: Lưu ý là ở đây mình sử dụng MariaDB nên cấu hình sẽ như vậy, nếu anh em sử dụng các hệ quản trị cơ sở dữ liệu khác thì thông tin cấu hình có thể sẽ khác.
■ Sau đó mình tạo Entity Class để mapping với bảng POSTS trong cở dữ liệu:
@Data
@Entity
@Table(name = "POSTS")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Long id;
@Column(name = "POST_TITLE")
private String postTitle;
@Type(type = "text")
@Column(name = "POST_CONTENT")
private String postContent;
@Column(name = "POST_LIKE")
private Long postLike;
}
■ Okay, vậy là về cơ bản chúng ta làm việc xong với cơ sở dữ liệu. Tiếp theo mình định nghĩa một repository class đó là PostRepository để thao tác với cơ sở dữ liệu.
@Repository
public interface PostRepository extends PagingAndSortingRepository<Post, Long> {
}
Ở đây anh em thấy mình để PostRepository extends interface PagingAndSortingRepository của thư viện Spring Data JPA. Interface này cung cấp hai phương thức findAll()
mà lát mình sẽ cùng anh em tìm hiểu kỹ hơn trong phần 3.
■ Tầng repository chỉ đơn giản vậy thôi và sau khi định nghĩa xong repository mình định nghĩa tiếp các thành phần của tầng service.
Ở đây mình sử dụng một interface và một class triển khai (implements) interface đó như sau:
public interface PostService {
Page<Post> getPosts();
}
@Service
public class PostServiceImpl implements PostService {
@Autowired
PostRepository postRepository;
@Override
public Page<Post> getPosts() {
Pageable pageable = PageRequest.of(0, 5);
return postRepository.findAll(pageable);
}
}
■ Cuối cùng là tầng controller mình sử dụng HTTP method GET để tương tác với client và trả ra dữ liệu thông qua API như sau:
@RestController
@RequestMapping("/posts")
public class PostController {
@Autowired
PostService postService;
@GetMapping("/v1")
public ResponseEntity<Page<Post>> getPosts() {
return ResponseEntity.ok().body(postService.getPosts());
}
}
Có thể nhiều anh em chưa hiểu nhưng hãy cứ triển khai cho mình đoạn code bên trên sau đó chạy ứng dụng và test thử xem kết quả như thế nào.
Nếu anh em nào gặp vấn đề về việc triển khai code thì có thể tham khảo code mẫu của mình tại đây.
■ Testing
Nếu anh em nào sử dụng Postman thì có thể request như sau:
Hoặc nếu không có thể mở Command Prompt rồi chạy lệnh:
curl -v http://localhost:8088/posts/v1
Output:
{
"content": [
{
"id": 504,
"postTitle": "title 1",
"postContent": "content 1",
"postLike": 1
},
{
"id": 505,
"postTitle": "title 2",
"postContent": "content 2",
"postLike": 2
},
{
"id": 506,
"postTitle": "title 3",
"postContent": "content 3",
"postLike": 3
},
{
"id": 507,
"postTitle": "title 4",
"postContent": "content 4",
"postLike": 4
},
{
"id": 508,
"postTitle": "title 5",
"postContent": "content 5",
"postLike": 5
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"pageNumber": 0,
"pageSize": 5,
"offset": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 20,
"totalElements": 100,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"first": true,
"numberOfElements": 5,
"size": 5,
"number": 0,
"empty": false
}
Vậy là chúng ta ấy ra được 5 bản ghi của trang đầu tiên.
Tương tự nếu muốn lấy ra 5 bản ghi của trang thứ 2 (trang 1):
Pageable pageable = PageRequest.of(1, 5);
Hay lấy ra 10 bản ghi thay vì 5 bản ghi ở trang thứ nhất:
Pageable pageable = PageRequest.of(0, 10);
■ Rõ ràng việc fixed code như vậy là không nên vì chúng ta không thể biết được client muốn truyền hai thông tin trang hiện tại (pageNumber) và số bản ghi trên một trang (pageSize) như thế nào.
Để khắc phục điều này chúng ta có thể truyền thêm thông tin pageNumber và pageSize thay vì fixed cứng như đoạn code bên trên:
@RestController
@RequestMapping("/posts")
public class PostController {
@Autowired
PostService postService;
@GetMapping("/v2")
public ResponseEntity<Page<Post>> getPostsWithPaginationAndSorting(
@RequestParam Integer pageNumber,
@RequestParam Integer pageSize
) {
return ResponseEntity.ok().body(postService.getPostsWithPaginationAndSorting(pageNumber, pageSize));
}
}
public interface PostService {
Page<Post> getPostsWithPaginationAndSorting(Integer pageNumber, Integer pageSize);
}
@Service
public class PostServiceImpl implements PostService {
@Autowired
PostRepository postRepository;
@Override
public Page<Post> getPostsWithPaginationAndSorting(Integer pageNumber, Integer pageSize) {
Pageable pageable = PageRequest.of(pageNumber, pageSize);
return postRepository.findAll(pageable);
}
}
■ Okay, phân trang sử dụng Spring Data JPA chỉ đơn giản vậy thôi anh em ạ! Lát mình sẽ cùng anh em tìm hiểu kỹ hơn trong phần 3. Tiếp theo bây giờ làm sao chúng ta sắp xếp (sorting)?
Cũng hết sức đơn giản luôn, mình chỉ cần thêm một tham số như sau:
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("postLike").descending());
Sort.by("postLike").descending()
có nghĩa là sắp xếp theo chiều giảm dần của trường POST_LIKE
3. Chi tiết
Trong phần 2, mình đã cùng anh em triển khai hai thao tác phân trang và sắp xếp sử dụng Spring Data JPA. Trong phần tiếp theo này mình sẽ đi sâu hơn để giải thích các thành phần liên quan!
3.1 - PagingAndSortingRepository
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
Được sinh tra với mục đích chính là hỗ trợ việc phân trang và sắp xếp dữ liệu đồng thời kế thừa interface CrudReposity
nên PagingAndSortingRepository
có thể thực hiện nhiều thao tác cơ bản khác liên quan đến việc thêm, sửa, xóa dữ liệu
Ở đây anh em thấy có hai hàm findAll(Sort sort)
và findAll(Pageable pageable)
hỗ trợ chúng ta trong trường hợp tìm kiếm không điều kiện.
Trong trường hợp anh em muốn tìm kiếm theo điều kiện thì Spring Data cũng hỗ trợ nhiều cách khác nhau. Đơn giản nhất là thông qua method's name hoặc phức tạp hơn thì anh em có thể viết các native queries.
+ Sử dụng method's name:
@Repository
public interface PostRepository extends PagingAndSortingRepository<Post, Long> {
Page<Post> findByTitle(String title, Pageable pageable);
}
+ Sử dụng native queries:
@Repository
public interface PostRepository extends PagingAndSortingRepository<Post, Long> {
@Query(
value = "SELECT * FROM posts p WHERE p.title = :title",
nativeQuery = true
)
Page<Post> findByTitle(String title, Pageable pageable);
}
Note: Việc tìm kiếm theo điều kiện sử dụng method's name hoặc native queries cũng có một vài lưu ý và mình sẽ trình bày kỹ hơn trong bài viết tiếp theo.
3.2 - Spring Data Page
public interface Page<T> extends Slice<T> {
static <T> Page<T> empty() {
return empty(Pageable.unpaged());
}
static <T> Page<T> empty(Pageable pageable) {
return new PageImpl(Collections.emptyList(), pageable, 0L);
}
int getTotalPages();
long getTotalElements();
<U> Page<U> map(Function<? super T, ? extends U> converter);
}
Khi thực hiện phân trang client sẽ cần một số thông tin như:
+ Tổng số phần tử (total elements)
+ Số phần tử trên một trang (number of elements per page)
+ Số trang (number of page)
Số phần tử trên một trang thì do client truyền xuống nên đầu server có thể không cần trả về nhưng còn tổng số phần tử thì chỉ có đầu server mới có thông tin.
Khi đó nếu không sử dụng interface Page
chúng ta sẽ lại phải tính toán để trả về tổng số lượng phần tử. Ngược lại nếu sử dụng interface Page
chúng ta có sẵn hàm getTotalPages()
để lấy tổng số trang và getTotalElements()
để lấy tổng số lượng phần tử.
Ngoài ra do kế thừa từ interface Slide
nên chúng ta cũng có thể sử dụng thêm các hàm khác của interface này.
public interface Slice<T> extends Streamable<T> {
int getNumber();
int getSize();
int getNumberOfElements();
List<T> getContent();
boolean hasContent();
Sort getSort();
...
}
3.3 - Spring Data Pageable
public interface Pageable {
int getPageNumber();
int getPageSize();
long getOffset();
Sort getSort();
Pageable next();
Pageable previousOrFirst();
Pageable first();
boolean hasPrevious();
...
}
Pageable là một interface chứa các phương thức để lấy thông tin của page như:
getPageNumber()
: Trang hiện tại
getPageSize()
: Số bản ghi trên một trang
...
Spring Data tự động nhận biết tham số pageable để thực hiện các thao tác paging hoặc sorting. Vì vậy khi muốn phân trang tập kết quả (có hoặc không có điều kiện) chúng ta đều chỉ cần truyền tham số này vào các phương thức tương ứng là được.
Ví dụ:
Page<Post> findAll(Pageable pageable);
Page<Post> findByTitle(String title, Pageable pageable);
Page<Post> findByTitleAndContent(String title, String content, Pageable pageable);
4. Lời kết
Vậy là trong bài viết hôm nay mình đã cùng anh em tìm hiểu cơ bản về cách để chúng ta triển khai hai thao tác pagination và sorting sử dụng Spring Data JPA.
Trong bài viết tiếp theo mình sẽ cùng anh em tìm cách tùy chỉnh, biến tấu cách dùng Spring Data JPA để tạo ra các class hỗ trợ paging và sorting phù hợp hơn với yêu cầu thực tế.
Source code:
Anh em có thể download source code tại đây.
Tham khảo:
https://www.baeldung.com/spring-data-jpa-pagination-sorting
https://www.bezkoder.com/spring-boot-pagination-filter-jpa-pageable/
Hẹn gặp lại anh em trong các bài viết tiếp theo, bye. Thanks all ❤️❤️❤️
Không có nhận xét nào: