Spring Boot Pagination with Thymeleaf and Spring Data JPA

Build a Spring Boot App with Pagination. Use Thymeleaf and a SQL Database.
Our DB contains Directors, Movies and Genres. Each Movies has one Director (@OneToMany). A Movie can have multiple Genres and vice versa (@ManyToMany).

Find the application in Heroku

Download the source code from GitHub

Generate Project with Spring Initializr:

This project will use Dependency ‘Lombok’ to omit getters, setters and constructors. However you don’t have to use this Dependency if you’re not comfortable with it. Same goes for ‘DevTools’. ‘PostgreSQL’ will be necessary for a Heroku Deployment.

Open the generated file in an IDE of your choice. Create a Database “pagination_db” in your MySQL Workbench.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>paginationexample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>paginationexample</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties

.Change user and password according to your MySQL Workbench nstallation. If you want to deploy your app to heroku later, delete everything below “Properties for Running in IDE”.

These properties initialize SQL Data after every restart. If you want to prevent this change “spring.jpa.hibernate.ddl-auto = create” to “update” and “spring.datasource.initialization-mode=always” to “never”.

###########################################
# Properties for Heroku
###########################################
#spring.datasource.url=${JDBC_DATABSE_URL}
#spring.datasource.username=${JDBC_DATABSE_USERNAME}
#spring.datasource.password=${JDBC_DATABSE_PASSWORD}

#spring.jpa.show-sql = false
#spring.jpa.generate.ddl = true
#spring.jpa.hibernate.ddl-auto = create
#spring.datasource.initialization-mode=always


###########################################
#Properties for Running in IDE
###########################################
spring.datasource.url=jdbc:mysql://localhost/pagination_db?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false

spring.datasource.username=root
spring.datasource.password=password

spring.jpa.show-sql=true
## Hibernate Logging
logging.level.org.hibernate.SQL= DEBUG
#init data after every restart
spring.jpa.hibernate.ddl-auto=create
spring.datasource.initialization-mode=always

## Server Properties
server.port= 5000

## Hibernate Properties
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

## Jackson Properties
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS= false
spring.jackson.time-zone= UTC

data.sql

INSERT INTO directors (id, name) VALUES
  ( 1000, 'Christopher Nolan'),
  ( 1001, 'David Fincher'),
  ( 1002, 'Quentin Tarantino'),
  ( 1003, 'Stanley Kubrick'),
  ( 1004, 'Ridley Scott'),
  ( 1005, 'Joss Whedon'),
  ( 1006, 'Scott Derrickson'),
  ( 1007, 'James Gunn'),
  ( 1008, 'Jon Favreau'),
  ( 1009, 'Jon Watts'),
  ( 2000, 'Taika Waititi'),
  ( 2001, 'Shane Black'),
  ( 2002, 'Chad Stahelski'),
  ( 2003, 'David F. Sandberg');

INSERT INTO movies (id, name, director_id) VALUES
  ( 1000, 'The Avengers', 1005),
  ( 1001, 'Interstellar', 1000),
  ( 1002, 'Django Unchained', 1002),
  ( 1003, 'A Clockwork Orange', 1003),
  ( 1004, 'The Dark Knight', 1000),
  ( 1005, 'Inglorious Basterds', 1002),
  ( 1006, 'Inception', 1000),
  ( 1007, 'Dunkirk', 1000),
  ( 1008, 'Batman Begins', 1000),
  ( 1009, 'Prestige', 1000),
  ( 1010, 'The Dark Knight Rises', 1000);

INSERT INTO genres (id, name) VALUES
  (1000, 'Fantasy'),
  (1001, 'Adventure'),
  (1002, 'Mystery'),
  (1003, 'Science Fiction'),
  (1004, 'Action'),
  (1005, 'Crime'),
  (1006, 'Drama');

INSERT INTO movies_genres (movie_id, genre_id) VALUES
  (1000, 1003),
  (1000, 1001),
  (1000, 1004),
  (1001, 1003),
  (1001, 1002),
  (1001, 1001),
  (1002, 1004),
  (1002, 1001),
  (1003, 1003),
  (1003, 1005),
  (1003, 1006),
  (1004, 1006),
  (1004, 1005),
  (1004, 1004),
  (1005, 1001),
  (1000, 1006),
  (1006, 1001),
  (1006, 1003),
  (1006, 1006),
  (1007, 1006),
  (1008, 1005),
  (1008, 1006),
  (1008, 1004),
  (1009, 1002),
  (1009, 1003),
  (1009, 1006),
  (1010, 1006),
  (1010, 1004),
  (1010, 1005);

Director, Movie and Genre Entities with Lombock Annotations to shorten the Code:

package com.example.paginationexample.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "directors")
public class Director {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
package com.example.paginationexample.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "genres")
public class Genre {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(fetch = FetchType.LAZY,
            cascade = {
                    CascadeType.PERSIST,
                    CascadeType.MERGE
            },
            mappedBy = "genres")
    private Set<Movie> movies = new HashSet<>();

}
package com.example.paginationexample.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "movies")
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "director_id", nullable = false)
    private Director director;


    @ManyToMany(fetch = FetchType.LAZY,
            cascade = {
                    CascadeType.PERSIST,
                    CascadeType.MERGE
            })
    @JoinTable(name = "movies_genres",
            joinColumns = { @JoinColumn(name = "movie_id") },
            inverseJoinColumns = { @JoinColumn(name = "genre_id") })
    private Set<Genre> genres = new HashSet<>();
}

The Repositories:

@Repository
public interface DirectorRepository extends JpaRepository<Director, Long> {
    Optional<Director> findById( Long directorId);
}
@Repository
public interface GenreRepository extends JpaRepository<Genre, Long> {
    Optional<Genre> findById(Long genreId);

    /**
     * DB contains table 'movies_genres:(movie_id, genre_id)
     * Spring Data JPA automatically finds all Genres that are related to 'movie'
     *
     * @param movie         movie object, contains 'movie_id'
     * @param pageable      Abstract interface for pagination information from PageRequest
     * @return              Page<T> Object with Genre Objects
     */
    Page<Genre> findAllByMovies(Movie movie, Pageable pageable);
}
@Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
    Optional<Movie> findById(Long movieId);

    /**
     * DB has table 'movies:(id, name, director_id)'
     * @param directorId        director_id
     * @param pageable          Abstract interface for pagination information from PageRequest
     * @return                  Page<T> Object with Movie Objects
     */
    Page<Movie> findAllByDirectorId(Long directorId, Pageable pageable);

    /**
     * DB contains table 'movies_genres:(movie_id, genre_id)
     * Spring Data JPA automatically finds all Genres that are related to 'genre'
     *
     * @param genre         genre object, contains 'genre_id'
     * @param pageable      Abstract interface for pagination information from PageRequest
     * @return              Page<T> Object with Movie Objects
     */
    Page<Movie> findAllByGenres(Genre genre, Pageable pageable);
}

The PageModel for Pagination:

@Component is a generic Spring Annotation like @Controller, @Repository and @Service. A Java class decorated with this Annotation is found during classpath scanning and registered in the Spring Context. We will use it with @Autowire in our MainController to initialize pagination.

package com.example.paginationexample.helper;

import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;

@Component
public class PageModel {
    private static int PAGE = 0;
    private static int SIZE = 5;

    private HttpServletRequest request;

    public PageModel(HttpServletRequest request) {
        this.request = request;
    }

    public void initPageAndSize(){
        if (request.getParameter("page") != null && !request.getParameter("page").isEmpty()) {
            PAGE = Integer.parseInt(request.getParameter("page")) - 1;
        }

        if (request.getParameter("size") != null && !request.getParameter("size").isEmpty()) {
            PAGE = Integer.parseInt(request.getParameter("size"));
        }
    }

    public static void setSIZE(int SIZE) {
        PageModel.SIZE = SIZE;
    }

    public static int getPAGE() {
        return PAGE;
    }

    public static int getSIZE() {
        return SIZE;
    }
}

MainController:

package com.example.paginationexample.controller;

import com.example.paginationexample.helper.PageModel;
import com.example.paginationexample.model.Director;
import com.example.paginationexample.model.Genre;
import com.example.paginationexample.model.Movie;
import com.example.paginationexample.repository.DirectorRepository;
import com.example.paginationexample.repository.GenreRepository;
import com.example.paginationexample.repository.MovieRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.Optional;

@Controller
public class MainController {

    @Autowired
    private DirectorRepository directorRepository;
    @Autowired
    private MovieRepository movieRepository;
    @Autowired
    private GenreRepository genreRepository;
    @Autowired
    private PageModel pageModel;

    /**
     * Pagination for one Entity example
     * @param model     represents data in the MVC, ( director objects per Page
     * @return          template 'directors.html'
     */
    @GetMapping("/directors")
    public String getAllDirectors(Model model) {
        pageModel.setSIZE(8);
        pageModel.initPageAndSize();
        model.addAttribute("directors", directorRepository.findAll(PageRequest.of(pageModel.getPAGE(), pageModel.getSIZE())));
        return "directors";
    }

    /**
     * 1:M example (Director:Movies)
     * @param directorId    id of director
     * @param model         represents data in the MVC: director object, Page with movie objects
     * @return              template 'moviesByDirector.html'
     */
    @GetMapping("/getMoviesByDirector/{directorId}")
    public String getMoviesByDirector(@PathVariable Long directorId, Model model) {
        pageModel.initPageAndSize();
        Optional<Director> director = directorRepository.findById(directorId);
        model.addAttribute("director", director.get());
        model.addAttribute("movies", movieRepository.findAllByDirectorId(directorId, PageRequest.of(pageModel.getPAGE(), pageModel.getSIZE())));
        return "moviesByDirector";
    }

    /**
     * M:M example (Movies:Genres)
     * @param genreId       id of the genre
     * @param model         represents data in the MVC: genre object, Page with movie objects
     * @return              template 'moviesByGenre.html'
     */
    @GetMapping("/getMoviesByGenre/{genreId}")
    public String getMoviesByGenre(@PathVariable Long genreId, Model model) {
        pageModel.initPageAndSize();
        Optional<Genre> genre = genreRepository.findById(genreId);
        model.addAttribute("movies", movieRepository.findAllByGenres(genre.get(), PageRequest.of(pageModel.getPAGE(), pageModel.getSIZE())));
        model.addAttribute("genre", genre.get());
        return "moviesByGenre";
    }

    /**
     * M:M example (Movies:Genres)
     * @param movieId       id of the movie
     * @param model         represents data in the MVC: movie object, Page with genre objects
     * @return              template 'genresByMovie.html'
     */
    @GetMapping("/getGenresByMovie/{movieId}")
    public String getGenresByMovie(@PathVariable Long movieId, Model model) {
        pageModel.initPageAndSize();
        Optional<Movie> movie = movieRepository.findById(movieId);
        model.addAttribute("genres", genreRepository.findAllByMovies(movie.get(), PageRequest.of(pageModel.getPAGE(), pageModel.getSIZE())));
        model.addAttribute("movie", movie.get());
        return "genresByMovie";
    }
}

 

This concludes the Backend.

The Frontend:

fragments/footer.html

<footer></footer>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>

fragments/head.html

<head>
    <meta charset="UTF-8">
    <title>Entities with pagination</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <link rel="stylesheet" type="text/css" th:href="@{https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Roboto+Slab:400,700|Material+Icons}" />
    <link th:href="@{/css/style.css}" rel="stylesheet" />
</head>

The Tables and Pagination:

directors.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
    <body>
        <div class="container" style="margin-top: 50px;">
            <div class="row">
                <div class="col">
                    <h3>Pagination Example: Directors</h3>
                    <!-- Table directors -->
                    <table class="table table-bordered">
                        <thead>
                        <tr>
                            <th>ID</th>
                            <th>Name</th>
                            <th>nested Table</th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr th:each="director : ${directors}">
                            <td th:text="${director.id}"></td>
                            <td th:text="${director.name}"></td>
                            <td><a th:href="@{/getMoviesByDirector/{directorId}(directorId=${director.id})}">show movies</a></td>
                        </tr>
                        </tbody>
                    </table>
                    <!-- Table End-->
                </div>
            </div>
            <div class="row">
                <div class="col">
                    <!-- directors pagination -->
                    <nav aria-label="Pagination" th:if="${directors.totalPages gt 0}">
                        <ul class="pagination justify-content-center font-weight-bold">
                            <li class="page-item" th:classappend="${directors.number eq 0} ? 'disabled'">
                                <a class="page-link"
                                   th:href="@{/directors?page={id}(id=${directors.number lt 2 ? 1 : directors.number})}"
                                   aria-label="Previous" title="Previous Page" data-toggle="tooltip">
                                    <span aria-hidden="true">&laquo;</span>
                                </a>
                            </li>
                            <li class="page-item" th:classappend="${i eq directors.number + 1} ? 'active'"
                                th:each="i : ${#numbers.sequence( 1, directors.totalPages, 1)}">
                                <a class="page-link" th:href="@{/directors?page={id}(id=${i})}" th:text="${i}"
                                   th:title="${'Page '+ i}" data-toggle="tooltip"></a>
                            </li>
                            <li class="page-item" th:classappend="${directors.number + 1 eq directors.totalPages} ? 'disabled'">
                                <a class="page-link"
                                   th:href="@{/directors?page={id}(id=${directors.number + 2})}"
                                   aria-label="Next" title="Next Page" data-toggle="tooltip">
                                    <span aria-hidden="true">&raquo;</span>
                                </a>
                            </li>
                        </ul>
                    </nav>
                </div>
            </div>
        </div>
        <th:block th:include="fragments/footer"></th:block>
    </body>
</html>

genresByMovie.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>

<div class="container" style="margin-top: 50px;">
    <div class="row">
        <div class="col">
            <h5>Genres:Movies (M:M)</h5>
            <h5>Movie: <span th:text="${movie.name}"></span> has the following genres:</h5>
            <!-- Table -->
            <table class="table table-bordered">
                <thead>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Nested Table</th>
                </tr>
                </thead>
                <tbody>
                <tr th:each="genre : ${genres}">
                    <td th:text="${genre.id}"></td>
                    <td th:text="${genre.name}"></td>
                    <td><a th:href="@{/getMoviesByGenre/{movieId}(movieId=${movie.id})}">show movies</a></td>
                </tr>
                </tbody>
            </table>
            <!-- Table End-->
        </div>
    </div>
    <div class="row">
        <div class="col">
            <!-- products pagination -->
            <nav aria-label="Pagination" th:if="${genres.totalPages gt 0}">
                <ul class="pagination justify-content-center font-weight-bold">
                    <li class="page-item" th:classappend="${genres.number eq 0} ? 'disabled'">
                        <a class="page-link"
                           th:href="@{/getGenresByMovie/{movieId}?page={id}(movieId=${movie.id},id=${genres.number lt 2 ? 1 : genres.number})}"
                           aria-label="Previous" title="Previous Page" data-toggle="tooltip">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>
                    <li class="page-item" th:classappend="${i eq genres.number + 1} ? 'active'"
                        th:each="i : ${#numbers.sequence( 1, genres.totalPages, 1)}">
                        <a class="page-link" th:href="@{/getGenresByMovie/{movieId}?page={id}(movieId=${movie.id},id=${i})}" th:text="${i}"
                           th:title="${'Page '+ i}" data-toggle="tooltip"></a>
                    </li>
                    <li class="page-item" th:classappend="${genres.number + 1 eq genres.totalPages} ? 'disabled'">
                        <a class="page-link"
                           th:href="@{/getGenresByMovie/{movieId}?page={id}(movieId=${movie.id},id=${genres.number + 2})}"
                           aria-label="Next" title="Next Page" data-toggle="tooltip">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                </ul>
            </nav>
        </div>
    </div>
</div>

<th:block th:include="fragments/footer"></th:block>
</body>
</html>

moviesByDirector.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>

<div class="container" style="margin-top: 50px;">
    <div class="row">
        <div class="col">
            <h5>Director:Movies (1:M)</h5>
            <h5>Director: <span th:text="${director.name}"></span> directed these movies:</h5>
            <!-- Table -->
            <table class="table table-bordered">
                <thead>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Nested Table</th>
                </tr>
                </thead>
                <tbody>
                <tr th:each="movie : ${movies}">
                    <td th:text="${movie.id}"></td>
                    <td th:text="${movie.name}"></td>
                    <td><a th:href="@{/getGenresByMovie/{movieId}(movieId=${movie.id})}">show genres</a></td>
                </tr>
                </tbody>
            </table>
            <!-- Table End-->
        </div>
    </div>
    <div class="row">
        <div class="col">
            <!-- movies pagination -->
            <nav aria-label="Pagination" th:if="${movies.totalPages gt 0}">
                <ul class="pagination justify-content-center font-weight-bold">
                    <li class="page-item" th:classappend="${movies.number eq 0} ? 'disabled'">
                        <a class="page-link"
                           th:href="@{/getMoviesByDirector/{directorId}?page={id}(directorId=${director.id},id=${movies.number lt 2 ? 1 : movies.number})}"
                           aria-label="Previous" title="Previous Page" data-toggle="tooltip">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>
                    <li class="page-item" th:classappend="${i eq movies.number + 1} ? 'active'"
                        th:each="i : ${#numbers.sequence( 1, movies.totalPages, 1)}">
                        <a class="page-link" th:href="@{/getMoviesByDirector/{directorId}?page={id}(directorId=${director.id},id=${i})}"
                           th:text="${i}" th:title="${'Page '+ i}" data-toggle="tooltip"></a>
                    </li>
                    <li class="page-item" th:classappend="${movies.number + 1 eq movies.totalPages} ? 'disabled'">
                        <a class="page-link"
                           th:href="@{/getMoviesByDirector/{directorId}?page={id}(directorId=${director.id},id=${movies.number + 2})}"
                           aria-label="Next" title="Next Page" data-toggle="tooltip">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                </ul>
            </nav>
        </div>
    </div>
</div>

<th:block th:include="fragments/footer"></th:block>
</body>
</html>

moviesByGenre.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:include="fragments/head"></th:block>
<body>

<div class="container" style="margin-top: 50px;">
    <div class="row">
        <div class="col">
            <h5>Genres:Movies (M:M)</h5>
            <h5>Genre: = <span th:text="${genre.name}"></span> has the following products:</h5>
            <!-- Table -->
            <table class="table table-bordered">
                <thead>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Nested Table</th>
                </tr>
                </thead>
                <tbody>
                <tr th:each="movie : ${movies}">
                    <td th:text="${movie.id}"></td>
                    <td th:text="${movie.name}"></td>
                    <td><a th:href="@{/getGenresByMovie/{genreId}(genreId=${genre.id})}">show all genres</a></td>
                </tr>
                </tbody>
            </table>
            <!-- Table End-->
        </div>
    </div>
    <div class="row">
        <div class="col">
            <!-- movie pagination -->
            <nav aria-label="Pagination" th:if="${movies.totalPages gt 0}">
                <ul class="pagination justify-content-center font-weight-bold">
                    <li class="page-item" th:classappend="${movies.number eq 0} ? 'disabled'">
                        <a class="page-link"
                           th:href="@{/getMoviesByGenre/{genreId}?page={id}(genreId=${genre.id},id=${movies.number lt 2 ? 1 : movies.number})}"

                           aria-label="Previous" title="Previous Page" data-toggle="tooltip">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>
                    <li class="page-item" th:classappend="${i eq movies.number + 1} ? 'active'"
                        th:each="i : ${#numbers.sequence( 1, movies.totalPages, 1)}">
                        <a class="page-link" th:href="@{/getMoviesByGenre/{genreId}?page={id}(genreId=${genre.id},id=${i})}"
                           th:text="${i}" th:title="${'Page '+ i}" data-toggle="tooltip"></a>
                    </li>
                    <li class="page-item" th:classappend="${movies.number + 1 eq movies.totalPages} ? 'disabled'">
                        <a class="page-link"
                           th:href="@{/getMoviesByGenre/{genreId}?page={id}(genreId=${genre.id},id=${movies.number + 2})}"
                           aria-label="Next" title="Next Page" data-toggle="tooltip">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                </ul>
            </nav>
        </div>
    </div>
</div>
<th:block th:include="fragments/footer"></th:block>
</body>
</html>

Run the app on http://localhost:5000/directors

Thanks for reading.

Leave a Reply