Post

Spring Data JPA - Slice, Page (Infinite Scroll, Pagination)

Spring Data JPA - Slice, Page (Infinite Scroll, Pagination)

This post was migrated from Tistory. You can find the original here.

Spring Data JPA Paging

When implementing paging, there are generally two approaches to consider: infinite scroll, where more data is fetched as the user scrolls to the bottom, and pagination, where the user moves between pages to see the next set of data.

(1) Infinite scroll

(2) Pagination

Both can technically be implemented by making good use of SQL’s limit and offset. However, Spring Data JPA provides a convenient interface for this. If you pass a Pageable as an argument to a query method, you can get back a Slice, Page, List, etc.

1
2
3
4
5
6
7
8
public interface S3ImageRepository extends JpaRepository<Image, Long> {
    // Simple paging (no need to write this in the repository)
    Slice<Image> findAll(Pageable pageable);
    Page<Image> findAll(Pageable pageable);
    
    // Condition-based paging
    Page<Image> findByStatus(String status, Pageable pageable);
}

Note:

Spring Data JPA automatically applies query methods.

For example, if you use a method name like findAllByPage, it doesn’t conform to the query method naming convention, and you’ll get a No property 'page' found for type blabla error.

There isn’t a special naming convention for paging methods.

Instead, you just specify the return type as Page<T> or Slice<T> and pass a Pageable as a method argument.

For simple paging, you don’t even need to declare the method in the repository.

For condition-based paging, you declare the method in the repository and pass Pageable as the last argument.

You can conveniently create a Pageable via PageRequest, since PageRequest implements Pageable.

1
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id"));

Slice

Slice is a good fit for case (1), infinite scroll.

1
2
3
4
5
6
7
8
9
10
11
12
13
select
        i1_0.id,
        i1_0.content,
        i1_0.created_at,
        i1_0.modified_at,
        i1_0.pin_image_url,
        i1_0.title,
        i1_0.user_id 
    from
        image i1_0 
    order by
        i1_0.id desc limit ?,
        ?

Running findSliceBy produces the limit/offset query shown above.

1
boolean hasNext();

Slice doesn’t query the total count; instead it fetches limit + 1 rows and uses the hasNext method to determine whether there’s more data. Since infinite scroll only needs to know whether more data exists, Slice is well suited for it.

Page

Page is a good fit for case (2), pagination.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
select
        i1_0.id,
        i1_0.content,
        i1_0.created_at,
        i1_0.modified_at,
        i1_0.pin_image_url,
        i1_0.title,
        i1_0.user_id 
    from
        image i1_0 
    order by
        i1_0.id desc limit ?,
        ?;

select
        count(i1_0.id) 
    from
        image i1_0;

Running findPageBy produces the same limit/offset query as Slice, plus an additional count aggregate query.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Page<T> extends Slice<T> {
    ...
	/**
	 * Returns the number of total pages.
	 *
	 * @return the number of total pages
	 */
	int getTotalPages();

	/**
	 * Returns the total amount of elements.
	 *
	 * @return the total amount of elements
	 */
	long getTotalElements();
    ....
}

Page extends Slice. In addition to Slice’s methods, it offers getTotalPages and getTotalElements, letting you know the total number of pages and the total number of elements. That’s why it’s suited to pagination, where you need to know the total page count.

Response

1
2
3
4
public Page<Image> getImages(){
    ....
    return Page<Image>
}

Also, when you return a response as Page<T> or Slice<T>, you can see that extra information is automatically included, even without any additional configuration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	"pageable": {
		"sort": {
			"empty": false,
			"sorted": true,
			"unsorted": false
		},
		"offset": 20,
		"pageNumber": 1,
		"pageSize": 20,
		"paged": true,
		"unpaged": false
	},
	"last": false,
	"totalElements": 1097,
	"totalPages": 55,
	"size": 20,
	"number": 1,
	"sort": {
		"empty": false,
		"sorted": true,
		"unsorted": false
	},
	"first": false,
	"numberOfElements": 20,
	"empty": false

The above is a Page<T> response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	"pageable": {
		"sort": {
			"empty": false,
			"sorted": true,
			"unsorted": false
		},
		"offset": 20,
		"pageSize": 20,
		"pageNumber": 1,
		"paged": true,
		"unpaged": false
	},
	"size": 20,
	"number": 1,
	"sort": {
		"empty": false,
		"sorted": true,
		"unsorted": false
	},
	"first": false,
	"last": false,
	"numberOfElements": 20,
	"empty": false

The above is a Slice<T> response.

Notice that the totalElements and totalPages properties present in the Page response are missing here.

Usage Example

1
Page<Batch> findAll(Pageable pageable);
1
2
3
4
5
6
7
8
9
10
    //controller
    @GetMapping("/all/batch/page")
    public ResponseEntity<Page<BatchResponseDto>> getAllBatchDto(
            @RequestParam("pageNumber") String pageNumber,
            @RequestParam("pageSize") String pageSize,
            @RequestParam("descByColumn") String descByColumn
    ) {
        Page<BatchResponseDto> batchResponse = scheduleService.getBatchResponseByPage(pageNumber, pageSize, descByColumn);
        return ResponseEntity.ok().body(batchResponse);
    }
1
2
3
4
5
6
    //service
    public Page<BatchResponseDto> getBatchResponseByPage(String pageNumber, String pageSize, String descByColumn) {
        Pageable pageable = PageRequest.of(Integer.parseInt(pageNumber), Integer.parseInt(pageSize), Sort.by(Sort.Direction.DESC, descByColumn));
        Page<Batch> page = batchRepository.findAll(pageable);
        return page.map(b -> batchResponseMapper.toDto(b));
    }
This post is licensed under CC BY-NC 4.0 by the author.