En este tutorial rápido, veremos cómo podemos Paginar y Ordenar los datos de una tabla mediante Spring Boot, Thymeleaf y Spring Data JPA.
1. Información General
En este tutorial rápido, veremos cómo podemos Paginar y Ordenar los datos de una tabla mediante Spring Boot, Thymeleaf y Spring Data JPA.
2. Dependencias de Gradle
Para crear este ejemplo, usaremos las bibliotecas Spring Framework, Data JPA junto con la biblioteca Thymeleaf.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
3. Estructura del Proyecto
Antes de saltar a la capa de vista, creemos la estructura MVC como se muestra a continuación:
4. Vista HTML
Ahora vamos a crear la vista HTML, para ello vamos a crear un nuevo “HTML file” en nuestro paquete, “templates” y la vamos a llamar “index.html”. Esta vista constará de una tabla.
Esta vista la vamos a realizar en Bootstrap 4. Para poder realizar la vista en Bootstrap 4 debemos incluir los siguientes archivos CSS y JS, estos se incluirán dentro de la etiqueta “head” y al final de la etiqueta “body”.
<!-- Fontawesome CSS -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css">
<!-- Bootstrap CSS -->
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<!-- Javascript -->
<script th:src="@{/js/jquery-3.4.1.min.js}"></script>
<script th:src="@{/js/popper.min.js}"></script>
<script th:src="@{/js/bootstrap.min.js}"></script>
Ahora vamos a crear nuestra tabla:
<div class="container">
<div class="row justify-content-md-center">
<div class="col-md-10 py-3">
<h2 class="text-center py-3">Spring Boot JPA Paging and Sorting</h2>
<h6>Total de registros: <span th:text="${totalElementos}"></span></h6>
<table class="table table-bordered table-striped table-sm">
<thead>
<tr>
<th class="text-center " scope="col">
<a th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}/(numeroPagina=${numeroPagina},campoOrden='idCiudad',sentidoOrden=${tipoSentidoOrden})}">Id <i th:classappend="${tipoSentidoOrden eq 'asc' ? 'fas fa-sort-down' : 'fas fa-sort-up'}" class="fas fa-sort-down"></i></a>
</th>
<th class="text-center" scope="col">
<a th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}/(numeroPagina=${numeroPagina},campoOrden='nombre',sentidoOrden=${tipoSentidoOrden})}">Nombre <i th:classappend="${tipoSentidoOrden eq 'asc' ? 'fas fa-sort-down' : 'fas fa-sort-up'}" class="fas fa-sort-down"></i></a>
</th>
<th class="text-center" scope="col">
<a th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}/(numeroPagina=${numeroPagina},campoOrden='codigoPais',sentidoOrden=${tipoSentidoOrden})}">Código País <i th:classappend="${tipoSentidoOrden eq 'asc' ? 'fas fa-sort-down' : 'fas fa-sort-up'}" class="fas fa-sort-down"></i></a>
</th>
<th class="text-center" scope="col">
<a th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}/(numeroPagina=${numeroPagina},campoOrden='distrito',sentidoOrden=${tipoSentidoOrden})}">Distrito <i th:classappend="${tipoSentidoOrden eq 'asc' ? 'fas fa-sort-down' : 'fas fa-sort-up'}" class="fas fa-sort-down"></i></a>
</th>
<th class="text-center" scope="col">
<a th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}/(numeroPagina=${numeroPagina},campoOrden='poblacion',sentidoOrden=${tipoSentidoOrden})}">Población <i th:classappend="${tipoSentidoOrden eq 'asc' ? 'fas fa-sort-down' : 'fas fa-sort-up'}" class="fas fa-sort-down"></i></a>
</th>
</tr>
</thead>
<tbody>
<tr th:each="ciudad : ${listaCiudad}">
<td class="text-center" th:text="${ciudad.idCiudad}"></td>
<td th:text="${ciudad.nombre}"></td>
<td class="text-center" th:text="${ciudad.codigoPais}"></td>
<td th:text="${ciudad.distrito}"></td>
<td class="text-center" th:text="${ciudad.poblacion}"></td>
</tr>
</tbody>
</table>
<div> </div>
<div class="row">
<div class="col-md-12" th:if="${totalPaginas > 1}">
<nav aria-label="Page navigation example" th:if="${totalPaginas gt 0}">
<ul class="pagination">
<li class="page-item" th:if="${numeroPagina > 1}">
<a class="page-link" th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}(numeroPagina=1,campoOrden=${campoOrden},sentidoOrden=${tipoSentidoOrden eq 'desc'} ? 'asc' : 'desc')}" aria-label="Primero" title="Primero">Primero</a>
</li>
<li class="page-item" th:if="${numeroPagina > 1}" th:classappend="${ numeroPagina > totalPaginas} ? 'disabled' ">
<a class="page-link" th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}(numeroPagina=${numeroPagina-1},campoOrden=${campoOrden},sentidoOrden=${tipoSentidoOrden eq 'desc'} ? 'asc' : 'desc')}" aria-label="Anterior" title="Anterior">Anterior</a>
</li>
<li class="page-item" th:classappend="${i eq numeroPagina} ? 'active'" th:each="i : ${#numbers.sequence( numeroPagina , totalPaginas > 10 + numeroPagina ? numeroPagina + 10 : totalPaginas, 1)}">
<a class="page-link" th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}(numeroPagina=${i},campoOrden=${campoOrden},sentidoOrden=${tipoSentidoOrden eq 'desc'} ? 'asc' : 'desc')}" th:text="${i}" th:title="${'Página '+ i}" rel="tooltip"></a>
</li>
<li class="page-item disabled" th:if="${numeroPagina + 10 < totalPaginas}">
<a class="page-link svg-icon" href="#">
<span data-feather="more-horizontal" width="20" height="20">...</span>
</a>
</li>
<li class="page-item" th:if="${numeroPagina < totalPaginas}" th:classappend="${numeroPagina eq totalPaginas} ? 'disabled'">
<a class="page-link svg-icon" th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}(numeroPagina=${numeroPagina+1},campoOrden=${campoOrden},sentidoOrden=${tipoSentidoOrden eq 'desc'} ? 'asc' : 'desc')}" aria-label="Siguiente" title="Siguiente" rel="tooltip">
<span aria-hidden="true" data-feather="chevrons-right" width="20" height="20">Siguiente</span>
</a>
</li>
<li class="page-item" th:if="${numeroPagina < totalPaginas}" th:classappend="${numeroPagina eq totalPaginas} ? 'disabled'">
<a class="page-link" th:href="@{/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}(numeroPagina=${totalPaginas},campoOrden=${campoOrden},sentidoOrden=${tipoSentidoOrden eq 'desc'} ? 'asc' : 'desc')}" aria-label="Último" title="Último">Último</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
5. Clase Entidad
Nuestra aplicación solo consta de una sola entidad llamada “Ciudad”, está se ubica en nuestro paquete “Entidades” con sus respectivos atributos, getters y setters.
/**
* The Class Ciudad.
*/
@Entity
@Table(name = "city")
public class Ciudad implements Serializable {
/** The Constant serialVersionUID. */
private static final long serialVersionUID = 8166702833824607499L;
/** The id ciudad. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private Integer idCiudad;
/** The nombre. */
@Column(name = "Name")
private String nombre;
/** The codigo pais. */
@Column(name = "CountryCode")
private String codigoPais;
/** The distrito. */
@Column(name = "District")
private String distrito;
/** The poblacion. */
@Column(name = "Population")
private Integer poblacion;
/**
* Gets the id ciudad.
*
* @return the id ciudad
*/
public Integer getIdCiudad() {
return idCiudad;
}
/**
* Sets the id ciudad.
*
* @param idCiudad the new id ciudad
*/
public void setIdCiudad(Integer idCiudad) {
this.idCiudad = idCiudad;
}
/**
* Gets the nombre.
*
* @return the nombre
*/
public String getNombre() {
return nombre;
}
/**
* Sets the nombre.
*
* @param nombre the new nombre
*/
public void setNombre(String nombre) {
this.nombre = nombre;
}
/**
* Gets the codigo pais.
*
* @return the codigo pais
*/
public String getCodigoPais() {
return codigoPais;
}
/**
* Sets the codigo pais.
*
* @param codigoPais the new codigo pais
*/
public void setCodigoPais(String codigoPais) {
this.codigoPais = codigoPais;
}
/**
* Gets the distrito.
*
* @return the distrito
*/
public String getDistrito() {
return distrito;
}
/**
* Sets the distrito.
*
* @param distrito the new distrito
*/
public void setDistrito(String distrito) {
this.distrito = distrito;
}
/**
* Gets the poblacion.
*
* @return the poblacion
*/
public Integer getPoblacion() {
return poblacion;
}
/**
* Sets the poblacion.
*
* @param poblacion the new poblacion
*/
public void setPoblacion(Integer poblacion) {
this.poblacion = poblacion;
}
/**
* Instantiates a new ciudad.
*/
public Ciudad() {
super();
}
/**
* To string.
*
* @return the string
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Ciudad [idCiudad=");
builder.append(idCiudad);
builder.append(", nombre=");
builder.append(nombre);
builder.append(", codigoPais=");
builder.append(codigoPais);
builder.append(", distrito=");
builder.append(distrito);
builder.append(", poblacion=");
builder.append(poblacion);
builder.append("]");
return builder.toString();
}
}
6. Repositorio de datos
Para nuestra entidad Ciudad crearemos un repositorio de nombre “CiudadRepository” en nuestro paquete “repositorios” para cargar los datos de Ciudad desde la base de datos.
/**
* The Interface CiudadRepository.
*/
@Repository("ciudadRepository")
public interface CiudadRepository extends JpaRepository<Ciudad, Integer> {
}
7. Servicio e Implementación
Ahora crearemos una interfaz de nombre “CiudadService” en nuestro paquete servicios, esta tendrá un método abstracto de nombre “obtenerCiudades” que recibirá 4 parámetros y el cual será nuestro servicio.
/**
* The Interface CiudadService.
*/
public interface CiudadService {
/**
* Obtener ciudades.
*
* @param numeroPagina the numero pagina
* @param tamanioPagina the tamanio pagina
* @param campoOrden the campo orden
* @param sentidoOrden the sentido orden
* @return the page
*/
Page<Ciudad> obtenerCiudades(int numeroPagina, int tamanioPagina, String campoOrden, String sentidoOrden);
}
Una vez creada nuestra interfaz “CiudadService” crearemos una clase de nombre “CiudadServiceImpl” en nuestro paquete “impl” que se encuentra dentro del paquete servicios, este implementara el método abstracto que creamos en la interfaz “CiudadService” y a su vez le inyectaremos nuestro repositorio “CiudadRepository”.
/**
* The Class CiudadServiceImpl.
*/
@Service("ciudadService")
public class CiudadServiceImpl implements CiudadService {
/** The ciudad repository. */
@Autowired
private CiudadRepository ciudadRepository;
/**
* Obtener ciudades.
*
* @param numeroPagina the numero pagina
* @param tamanioPagina the tamanio pagina
* @param campoOrden the campo orden
* @param sentidoOrden the sentido orden
* @return the page
*/
@Override
public Page<Ciudad> obtenerCiudades(int numeroPagina, int tamanioPagina, String campoOrden, String sentidoOrden) {
Pageable pageable = PageRequest.of(numeroPagina - 1, tamanioPagina,
sentidoOrden.equals("asc") ? Sort.by(campoOrden).ascending() : Sort.by(campoOrden).descending());
return ciudadRepository.findAll(pageable);
}
}
Donde los parametros:
- numeroPagina: es el número de pagina por el cual iniciara la búsqueda
- tamanioPagina: es el número total de elementos que se mostraran
- campoOrden: es el campo por el cual se podrán ordenar los elementos de la tabla
- sentidoOrden: es el campo con el cual indicaremos si los registros se ordenaran de manera descendente o ascendente
8. Clase Controller
Vamos a crear una clase “InicioController” en nuestro paquete controladores. Lo primero que vamos a hacer es anotar la clase con un @Controller para indicar que esta clase es un controlador.
Esta clase inyectaremos nuestro servicio “CiudadServicio” para poder hacer uso de nuestro método el cual se encargará de hacer la llamada a la base de datos.
/**
* The Class InicioController.
*/
@Controller
public class InicioController {
/** The Constant logger. */
private static final Logger logger = LoggerFactory.getLogger(InicioController.class);
/** The ciudad service. */
@Autowired
@Qualifier("ciudadService")
private CiudadService ciudadService;
/**
* Ver ciudades.
*
* @param numeroPagina the numero pagina
* @param campoOrden the campo orden
* @param sentidoOrden the sentido orden
* @return the model and view
*/
@GetMapping("/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}")
public ModelAndView verCiudades(@PathVariable("numeroPagina") Integer numeroPagina,
@PathVariable("campoOrden") String campoOrden, @PathVariable("sentidoOrden") String sentidoOrden) {
ModelAndView mav = new ModelAndView("index");
/*
* Total de elementos por pagina
* */
int tamanioPagina = 15;
Page<Ciudad> page = ciudadService.obtenerCiudades(numeroPagina, tamanioPagina, campoOrden, sentidoOrden);
List<Ciudad> listaCiudad = page.getContent();
mav.addObject("numeroPagina", numeroPagina);
mav.addObject("totalPaginas", page.getTotalPages());
mav.addObject("totalElementos", page.getTotalElements());
mav.addObject("listaCiudad", listaCiudad);
mav.addObject("campoOrden", campoOrden);
mav.addObject("sentidoOrden", sentidoOrden);
mav.addObject("tipoSentidoOrden", sentidoOrden.equals("asc") ? "desc" : "asc");
logger.info("Muestra vista index.html con resutados");
return mav;
}
}
Como vemos en el método “verCiudades” la dirección URL mediante la que accederemos a la vista creada anteriormente será “/pagina/{numeroPagina}/{campoOrden}/{sentidoOrden}”.
9. Resultado
Ahora ya podemos dirigirnos a nuestro navegador favorito y acceder a nuestra vista HTML a través de la URL “localhost:8080/springboot-jpa-paging-and-sorting/pagina/1/idCiudad/asc”, el puerto por defecto de Spring es el 8080.
Ahora podremos ver que, al hacer clic en los encabezados de la columna de la tabla, la búsqueda se genera por el nombre del campo y se ordena ya sea de manera ascendente o descendente, así como al hacer clic en los enlaces de navegación en la parte inferior generan la búsqueda respectiva por cada número de página.
Hasta aquí este tutorial, ahora ya sabemos Paginar y Ordenar mediante Spring Boot, Thymeleaf y Spring Data JPA.
Una versión funcional del código que se muestra en este tutorial está disponible en Gitlab.
Referencias
Srivastava, S. (10 de Octubre de 2020). Baeldung. Obtenido de Pagination and Sorting using Spring Data JPA: https://www.baeldung.com/spring-data-jpa-pagination-sorting