Filtros y ordenación

Con el método find() podemos obtener listas de entidades donde aplicamos criterios de filtro. En este vídeo presento cómo aplicar filtros usando HQL o bien de la forma más simple, pasándole directamente un campo y su valor esperado. También introduzco al uso de Sort para aplicar criterios de ordenación.

A Quarkus le podemos pedir que filtre los datos que se trae de la base de datos. Por ejemplo, es posible que mi tabla tenga miles de libros y yo solo quiera traerme aquellos que sean de un género específico, como Policíaca o Informática; o bien solo los libros que se han publicado en los últimos 12 meses. De este modo, seleccionamos la información que interesa traer.

También puede ser interesante ordenar los resultados. Por ejemplo, podríamos ordenar la lista de libros por ventas, para que aquellos que más unidades hayan vendido en una librería salgan de los primeros en la lista, o bien ordenarlos por el número de páginas para tener al principio de la lista aquellos que sean más cortos.

Criterios de filtro

Para aplicar criterios de filtro, podemos usar el método .find() o .list(), que forman parte del PanacheRepository o del PanacheEntity. Existen varias formas de usar este método.

Para devolver solo los libros que formen parte del género Tragicomedia, podríamos directamente ponerle la columna a filtrar como primer parámetro, y el valor del filtro como segundo parámetro:

List<Book> librosPorTema = bookRepository.list("genre", "Tragicomedia");
return librosPorTema;

Esto es equivalente a la sentencia SQL SELECT * FROM Book where genre = 'Tragicomedia';.

Cuando por la semántica del criterio de búsqueda no sea posible simplemente hacer una asignación, podemos especificar directamente la query en sintaxis HQL, que es el lenguaje de consulta de Hibernate, muy parecido a SQL. Por ejemplo, una forma simple de buscar libros de más de 500 páginas podría ser la siguiente:

return bookRepository.list("numPages > 500");

En este caso, directamente como parámetro le indicamos el criterio de búsqueda, correspondiente a limitar los registros devueltos a aquellos que tengan en la columna numPages un valor superior a 500. Esto no lo podemos expresar usando la primera forma que vimos, porque esa solo vale para equivalencias.

En los casos en los que queramos aplicar criterios con parámetros que vengan del exterior, como puede ser un query param de un controlador, tenemos que reprimir las ganas de concatenar el string que le entra como parámetro. Es decir, lo siguiente no es seguro:

public List<Book> listarLibrosPorGenero(@QueryParam("genero") String genero) {
  // Esto no es seguro y no debería hacerse, para prevenir problemas de seguridad.
  return bookRepository.list("genero = " + genero);
}

Se trata de una vulnerabilidad que puede que suene denominada inyección SQL. Si el género fuese '; DELETE FROM Book; --, si el driver no estuviese bien preparado para limpiar los criterios de búsqueda podrían borrar toda la tabla, debido a que estamos mezclando sentencias SQL. En su lugar, lo suyo es usar sentencias preparadas. Esencialmente, como primer parámetro usaremos placeholders especiales para señalizarle a Hibernate que ahí vendrán parámetros. Esos placeholders se especifican usando el símbolo interrogación y un número, por ejemplo ?1, ?2, ?3 o ?4. Y luego, al método le pasaremos más parámetros (tantos como nos haga falta), para indicar los distintos valores que queremos que tenga:

public List<Book> listarLibrosPorGenero(@QueryParam("genero") String genero) {
  // Esto es más seguro
  return bookRepository.list("genero = ?1", genero);
}

public List<Book> buscarLibros(@QueryParam("q") String q) {
  String title = "%" + q + "%";
  String description = "%" + q + "%";
  return bookRepository.list("title ILIKE ?1 OR description ILIKE ?2", title, description);
}

En este último caso, le estaremos pasando más de un parámetro. Como los parámetros se asignan por orden de indicación, ?1 será sustituido por el valor de la variable title y ?2 por el de la variable description. Aun así, estos códigos serán parcialmente incorrecto debido a que si alguno de los parámetros no está proporcionado, valdrá null, y eso provocará errores de tipo NullPointerException. Podemos prevenir esto usando un simple condicional. En este ejemplo completo, filtramos tanto por género como por texto libre, y si no se proporciona ningún parámetro, caemos en un listAll():

@GET
public List<Book> listBooks(
  @QueryParam("q") String query,
  @QueryParam("genre") String genre
) {
  if (query != null && genre != null) {
    String criteria = "(title ILIKE ?1 OR description ILIKE ?2) AND genre = ?3";
    String freeQuery = "%" + query + "%";
    return booksRepository.list(criteria, freeQuery, freeQuery, genre);
  } else if (query != null) {
    String criteria = "title ILIKE ?1 OR description ILIKE ?2";
    String freeQuery = "%" + query + "%";
    return booksRepository.list(criteria, freeQuery, freeQuery);
  } else if (genre != null) {
    return booksRepository.list("genre", genre);
  } else {
    return booksRepository.listAll();
  }
}

Ordenación

Todos los métodos que estamos viendo aquí, find, findAll, list y listAll, tienen un constructor sobrecargado donde se puede especificar también un criterio de ordenación, que es una instancia de tipo Sort. Tenemos varias formas de fabricarlos, mediante alguno de los métodos estáticos de esta clase:

  • Podemos llamar a Sort.by() y especificamos la columna a filtrar. Además, le podemos sobrecargar con la dirección de búsqueda, que puede ser Sort.Direction.Ascending para ordenar de menor a mayor o Sort.Direction.Descending para ordenar de mayor a menor.
  • O bien llamando a los métodos rápidos Sort.ascending() y Sort.descending() para indicar el orden en el propio método.

También podemos juntar varios criterios de ordenación llamando al método and() perteneciente a las instancias de la clase Sort.

Aquí algunos ejemplos:

// Los libros más recientemente publicados aparecerán en los primeros resultados.
Sort orderByRecent = Sort.by("pubDate", Sort.Direction.Descending);

// Los libros aparecerán ordenados alfabéticamente
Sort orderByTitle = Sort.by("title");

// También podemos usar Sort.ascending y Sort.descending.
Sort orderByLongest = Sort.descending("numPages");

// Podemos ordenar por varios campos.
Sort orderByBestSellerAndRecent = Sort.descending("booksSold").and("pubDate", Sort.Direction.Descending);

La forma de pasar este criterio de ordenación en algunos casos a los métodos de filtro y de listado puede ser un poco incómoda. Java exige que los parámetros variádicos de una función vayan los últimos. Es por eso que si a una función de varios filtros le vamos a proporcionar además un criterio de ordenación, el parámetro de ordenación se pase entre medias, porque no queda otra que hacerlo así para que el código siga compilando:

// Esto no compilaría, porque pensaría que `sort` es otro argumento variádico.
List<Book> libros = repository.list("title = ?1 AND genre = ?2", title, genre, sort);

// En su lugar, hay que pasarlo entre medias, por raro que parezca.
List<Book> libros = repository.list("title = ?1 AND genre = ?2", sort, title, genre);
Lista de reproducción
  1. 1
    Configurar una base de datos
    7 minutos
  2. 2
    Crear una entidad y un repositorio
    12 minutos
  3. 3
    Active Record con PanacheEntity
    4 minutos
  4. 4
    Modificar y borrar registros
    12 minutos
  5. 5
    Filtros y ordenación
    12 minutos
  6. 6
    Paginación de resultados
    13 minutos
  7. 7
    Aplicar filtros dinámicos
    12 minutos
  8. 8
    JsonIgnore, JsonProperty y JsonAlias
    6 minutos