12 minutes read

Imagine your project requires a large dataset, and also, there is a need to present this data to the users. How do you do it? The best solution to this problem is to split the data into several chunks and show them piece by piece. This technique is called pagination. But if you do it, how do you work with the database? You will learn the answer in this topic.

Declaring a repository

Let's begin by building a project that uses the new features. For that, you need to have Spring Data dependency installed. If you use SQL databases, you can use the Spring Data JPA starter:

Maven
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Gradle(Kotlin)
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-data-jpa")

}
Gradle(Groovy)
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

}

If we use NoSQL databases, we should add the specific Spring Data version dependency instead of JPA.

Also, we need to have a dependency to interact with the database itself. For example, if we use an H2 database we should have the following dependency:

Maven
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <scope>runtime</scope>
</dependency>
Gradle(Kotlin)
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-data-jpa")
  runtimeOnly("com.h2database:h2")
}
Gradle(Groovy)
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  runtimeOnly 'com.h2database:h2'
}

Our examples will be based on an online movie info application. Let's implement our main Movie class by which the table will be formed. It will also have the toString() method to have a string representation of the object:

Java
@Entity
public class Movie {
    @Id
    @GeneratedValue
    private long id;
    private String name;
    private String genre;
    private int rating;
    private int yearOfRelease;

    //constructors, getters, setters...

    public String toString() {
        return "Movie{" +
                "name='" + name + '\'' +
                ", genre='" + genre + '\'' +
                ", rating=" + rating +
                ", yearOfRelease=" + yearOfRelease +
                '}';
    }
}
Kotlin
@Entity
data class Movie(
    @Id
    @GeneratedValue
    var id: Long = 0,
    var name: String = "",
    var genre: String = "",
    var rating: Int = 0,
    var yearOfRelease: Int = 0
)

Spring Data JPA is used in the example above, but the rest of the topic is also applicable to NoSQL databases that are compatible with Spring Data.

Our repository must extend from PagingAndSortingRepository to have the required functionality:

Java
public interface MovieRepository extends PagingAndSortingRepository<Movie, Long> {
}
Kotlin
interface MovieRepository : PagingAndSortingRepository<Movie, Long>

The PagingAndSortingRepository interface has two methods that we will consider later:

Page<T>     findAll(Pageable pageable);
Iterable<T> findAll(Sort sort);

Also, the methods of CrudRepository are available because PagingAndSortingRepository extends it.

PagingAndSortingRepository doesn't extend CrudRepository starting from Spring Boot 3.0 version. You may extend two interfaces to have the former functional:

Java
public interface MovieRepository extends PagingAndSortingRepository<Movie, Long>,
        CrudRepository<Movie, Long> {
}
Kotlin
interface MovieRepository : PagingAndSortingRepository<Movie, Long>, CrudRepository<Movie, Long>

You can test the new features in the ApplicationRunner:

Java
public class Runner implements CommandLineRunner {
    private final MovieRepository repository;
    
    public Runner(MovieRepository repository) {
        this.repository = repository;
    }

    @Override
    public void run(String... args) {
        // your code
    }
}
Kotlin
@Component
class Runner(private val repository: MovieRepository) : CommandLineRunner {
    override fun run(vararg args: String?) {
        // your code
    }
}

Sorting

Now, we will discover new features that let us get sorted results of a database query. We will sort with the help of the following function:

Iterable<T> findAll(Sort sort);

The Sort class is the new one for us. It has wide functionality you can explore in the documentation. We will only discuss its main features.

Sort is used to set the parameters of the sorting. You can get the instance of the class with the help of the static method by. The necessary argument for this function is the name of a table column. For example, if we need to sort movies by the year of release, the following instance will help us:

Java
Sort sortByYear = Sort.by("yearOfRelease");
Kotlin
val sortByYear: Sort = Sort.by("yearOfRelease")

Then, we can get the sorted data:

Java
Iterable<Movie> listMovie = repository.findAll(sortByYear);
Kotlin

val listMovie: Iterable<Movie> = repository.findAll(sortByYear)

However, sorting movies in descending order seems to be more convenient. There are two ways to do it:

Java
Iterable<Movie> listMovie = repository.findAll(Sort.by("yearOfRelease").descending());
Iterable<Movie> listMovie = repository.findAll(Sort.by(Sort.Direction.DESC, "yearOfRelease"));
Kotlin
val listMovie: Iterable<Movie> = repository.findAll(Sort.by("yearOfRelease").descending())
val listMovie: Iterable<Movie> = repository.findAll(Sort.by(Sort.Direction.DESC, "yearOfRelease"))

Also, we can sort by several parameters. For example, we want films with higher ratings to appear first, but we also want to have the newer ones higher among them. We can do this kind of sorting using the following code:

Java
Iterable<Movie> listMovie = repository.findAll(Sort.by(Sort.Direction.DESC, "rating").
                    and(Sort.by(Sort.Direction.DESC, "yearOfRelease")));
Kotlin
val listMovie: Iterable<Movie> = repository.findAll(
    Sort.by(Sort.Direction.DESC, "rating").and(Sort.by(Sort.Direction.DESC, "yearOfRelease"))
)

Pagination

As we mentioned earlier, pagination is about fetching data from the database in chunks. We can do it with the help of this method of PagingAndSortingRepository:

Page<T> findAll(Pageable pageable);

This function returns Page, the interface whose implementation contains the objects from the database, the number of rows, and the number of pages. You can find out the last two parameters by the following methods of the Page interface:

long    getTotalElements();
int     getTotalPages();

As you can see, the argument of the findAll function is the object that should extend the Pageable interface. The basic implementation of this interface is the PageRequest class. It is used for the setting of the pagination parameters. As well as in the case of the Sort class, it has wide functionality which is described in the documentation. We will discuss the main features.

You can create a new instance of PageRequest with the static method of. Two numbers are needed for that: the number of pages and their size. The numeration of pages begins with zero. Imagine we want to have 10 movies on a single web page. Then we set the following parameters to the first page:

Java
Pageable pageable = PageRequest.of(0, 10);
Kotlin
val pageable: Pageable = PageRequest.of(0, 10)

Now we can get this chunk of data:

Java
Page<Movie> pageMovie = repository.findAll(pageable);
Kotlin
val pageMovie: Page<Movie> = repository.findAll(pageable)

Let's look at an example of pagination usage. First, we create a little dataset of movies:

Java
repository.save(new Movie("The Shawshank Redemption", "drama", 91, 1994));
repository.save(new Movie("Forrest Gump", "drama", 89, 1994));
repository.save(new Movie("Intouchables", "drama", 88, 2011));
repository.save(new Movie("Inception", "science fiction", 87, 2010));
repository.save(new Movie("Fight Club", "thriller", 87, 1999));
repository.save(new Movie("The Godfather", "drama", 87, 1972));
repository.save(new Movie("The Prestige", "thriller", 85, 2006));
repository.save(new Movie("Gladiator", "history", 86, 2000));
Kotlin
repository.save(Movie(name = "The Shawshank Redemption", genre = "drama", rating = 91, yearOfRelease = 1994))
repository.save(Movie(name = "Forrest Gump", genre = "drama", rating =  89, yearOfRelease = 1994))
repository.save(Movie(name = "Intouchables", genre =  "drama", rating =  88, yearOfRelease = 2011))
repository.save(Movie(name = "Inception", genre = "science fiction", rating = 87, yearOfRelease =  2010))
repository.save(Movie(name = "Fight Club", genre = "thriller", rating =  87, yearOfRelease =  1999))
repository.save(Movie(name = "The Godfather", genre =  "drama", rating =  87, yearOfRelease = 1972))
repository.save(Movie(name = "The Prestige", genre =  "thriller", rating =  85, yearOfRelease = 2006))
repository.save(Movie(name = "Gladiator", genre = "history", rating = 86, yearOfRelease = 2000))

Our page will have 5 movies. So, we can create 2 pages and see what's inside them:

Java
for (int i = 0; i < 2; i++) {
    System.out.println("Page " + (i + 1));
    Page<Movie> moviePage = repository.findAll(PageRequest.of(i, 5));
    for (Movie movie : moviePage) {
        System.out.println(movie.toString());
    }
}
Kotlin
for (i in 0..1) {
    println("Page " + (i + 1))
    val moviePage = repository.findAll(PageRequest.of(i, 5))
    for (movie in moviePage) {
        println(movie.toString())
    }
}

Here you can see the results of the code's execution:

Page 1
Movie{name='The Shawshank Redemption', genre='drama', rating=91, yearOfRelease=1994}
Movie{name='Forrest Gump', genre='drama', rating=89, yearOfRelease=1994}
Movie{name='Intouchables', genre='drama', rating=88, yearOfRelease=2011}
Movie{name='Inception', genre='fantastic', rating=87, yearOfRelease=2010}
Movie{name='Fight Club', genre='thriller', rating=87, yearOfRelease=1999}
Page 2
Movie{name='The Godfather', genre='drama', rating=87, yearOfRelease=1972}
Movie{name='The Prestige', genre='thriller', rating=85, yearOfRelease=2006}
Movie{name='Gladiator', genre='history', rating=86, yearOfRelease=2000}

A very useful feature is the possibility of sorting before the pagination. We can place the movies with higher ratings on the first pages. There are two ways to do it: the first is by passing the Sort object to the PageRequest method of:

Java
Pageable pageable = PageRequest.of(0, 2, Sort.by("rating").descending());
Koltin
val pageable: Pageable = PageRequest.of(0, 2, Sort.by("rating").descending())

Also, we can define the sorting parameters without the instance of the Sort class:

Java
Pageable pageable = PageRequest.of(0, 2, Sort.Direction.DESC, "rating");
Kotlin
val pageable: Pageable = PageRequest.of(0, 2, Sort.Direction.DESC, "rating")

Custom methods of pagination

We can retrieve the filtered data and then get it paginated by a single method. We just need to define its signature in the repository.

Let's look at the example. Imagine that we want to create a filter by movie genres. Of course, we will need pagination there. We can define the following method in the repository:

Java
public interface MovieRepository extends PagingAndSortingRepository<Movie, Long> {
  Page<Movie> findByGenre(String genre, Pageable pageable);
}
Kotlin
interface MovieRepository : PagingAndSortingRepository<Movie, Long> {
    fun findByGenre(genre: String, pageable: Pageable): Page<Movie>
}

Now, it's ready to use! Let's test it on the little dataset we created previously. We will fetch only drama movies from the database and sort them by the year of release:

Java
for (int i = 0; i < 2; i++) {
    System.out.println("Page " + (i + 1));
    Page<Movie> moviePage = repository.findByGenre("drama", PageRequest.of(i, 5,
        Sort.by("yearOfRelease").descending()));
    for (Movie movie : moviePage) {
        System.out.println(movie.toString());
    }
}
Kotlin
for (i in 0..1) {
    println("Page " + (i + 1))
    val moviePage = repository.findByGenre(
        "drama", PageRequest.of(
            i, 5,
            Sort.by("yearOfRelease").descending()
        )
    )
    for (movie in moviePage) {
        println(movie.toString())
    }
}

As you can see, the results of the code execution correspond to our goals. The second page is empty because there are only 4 dramas in our database:

Page 1
Movie{name='Intouchables', genre='drama', rating=88, yearOfRelease=2011}
Movie{name='The Shawshank Redemption', genre='drama', rating=91, yearOfRelease=1994}
Movie{name='Forrest Gump', genre='drama', rating=89, yearOfRelease=1994}
Movie{name='The Godfather', genre='drama', rating=87, yearOfRelease=1972}
Page 2

Also, we can change the type of the return value. For example, we can return the List:

Java
public interface MovieRepository extends  PagingAndSortingRepository<Movie, Long> {
  List<Movie> findByGenre(String genre, Pageable pageable);
}
Kotlin
interface MovieRepository : PagingAndSortingRepository<Movie, Long> {
    fun findByGenre(genre: String, pageable: Pageable): List<Movie>
}

Feel free to experiment!

Conclusion

In this topic, you learned to use the tools of Spring Data for pagination – displaying data in chunks, on different pages. It's widely used in web applications that display a lot of content to users: from e-commerce to search engines. The Spring Data feature that allows sorting before the pagination makes this tool even more useful, making our queries more specific.

39 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo