¿Cómo hacer consultas dinámicas con Spring Data JPA?

Spring Data JPA es una interfaz común que permite a los desarrolladores conectarse a distintas bases de datos relacionales a través de repositorios basados en la API de persistencia de Java (JPA). Permite crear consultas a la base de datos con un alto nivel de abstracción utilizando la anotación @Query a partir del lenguaje de consultas de persistencia de Java (JPQL) o utilizando el lenguaje SQL nativo o simplemente utilizando los nombres de los métodos. Para utilizar Spring Data JPA simplemente tenemos que extender de cualquiera de los repositorios de Spring Data y aprovechar al máximo esta función, un ejemplo sencillo es el siguiente:

public interface EventRepository extends JpaRepository<Event, String> {
    List<Event> findBySport(String sport);
    Page<Event> findBySportAndCountry(String sport, String country, Pageable p);
    @Query(value = "SELECT * FROM event WHERE sport=:sport;", nativeQuery=true)
    List<Event> eventsBySport(String sport);
}

La línea 3 refleja un método que busca y devuelve la lista de eventos cuyo deporte coincidan con un determinado nombre. En la línea 5 un método que a partir de un determinado deporte y país devuelve una página con los eventos. Por último en la línea 7 se hace uso de la anotación @Query para devolver la misma información que el primer método a partir de una consulta nativa SQL.

Aunque parece bastante sencillo utilizar esta forma para ejecutar las consultas, ya que con la mínima cantidad de código puedes obtener resultados satisfactorios, pero esto tiene un gran inconveniente:

Supongamos que queremos obtener la lista de eventos cuyo país y deportes coincidan con los de una lista de países y deportes respectivamente, pero si alguna de esas listas están vaciás, se debe descartar ese criterio. En cada método se define un número fijo de criterios, por lo que la cantidad de criterios y parámetros no se pueden cambiar dinámicamente en tiempo de ejecución.

Para aplicaciones más grandes y complejas, necesitamos una estrategia de generación de consultas robusta y flexible para manejar diferentes tipos de casos de uso. Ahí es donde entran las especificaciones de Spring Data JPA.

Spring Data JPA Specifications

Spring Data Specifications permiten crear consultas dinámicas utilizando la clase CriteriaBuilder definiendo una especificación como un predicado sobre una entidad. Veamos la interfaz de Specification:

public interface Specification<T> extends Serializable {
    @Nullable
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
                CriteriaBuilder criteriaBuilder);
}
Ejemplo de aplicación

Veamos como utilizar JPA Specifications en la creación de una aplicación ejemplo donde se gestionan eventos, esto que les voy a mostrar es una alternativa para utilizar multiples criterios de consulta y lo mas importante, de manera opcional y dinámica. Empecemos con lo necesario para utilizar la interfaz de Specification.

Clase SearchCriteria
package cu.sacavix.springboot.jpa.specifications.search;
public class SearchCriteria {
    private String attr;
    private Object value;
    private SearchOperation searchOperation;
    public SearchCriteria() {}
    public SearchCriteria(String attr, Object value, SearchOperation searchOperation) {
        this.attr = attr;
        this.value = value;
        this.searchOperation = searchOperation;
    }
    public String getAttr() {
        return attr;
    }
    public void setAttr(String attr) {
        this.attr = attr;
    }
    public Object getValue() {
        return value;
    }
    public void setValue(Object value) {
        this.value = value;
    }
    public SearchOperation getSearchOperation() {
        return searchOperation;
    }
    public void setSearchOperation(SearchOperation searchOperation) {
        this.searchOperation = searchOperation;
    }
}

Con esta clase podemos getionar los criterios de búsqueda, pasando el campo por donde queremos buscar, el valor y la operación que queremos hacer.

Clase SearchOperation
package cu.sacavix.springboot.jpa.specifications.search;
public enum SearchOperation {
    GREATER_THAN,
    LESS_THAN,
    GREATER_THAN_EQUAL,
    LESS_THAN_EQUAL,
    NOT_EQUAL,
    EQUAL,
    MATCH,
    MATCH_START,
    MATCH_END,
    IN,
    NOT_IN
}

En esta clase se definen todas las operaciones, aunque realmente pueden ser más o menos en dependencia de lo que necesitemos en nuestra aplicación.

Clase SearchSpecifications
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
public class SearchSpecifications<T> implements Specification<T> {
    private List<SearchCriteria> searchCriteriaList;
    public SearchSpecifications() {
        this.searchCriteriaList = new ArrayList<>();
    }
    public void add(SearchCriteria criteria) {
        searchCriteriaList.add(criteria);
    }
    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        List<Predicate> predicates = new ArrayList<>();
        for (SearchCriteria criteria : searchCriteriaList) {
            switch (criteria.getSearchOperation() ) {
                case GREATER_THAN:
                    predicates.add(builder.greaterThan(root.get(criteria.getAttr()), criteria.getValue().toString()));
                    break;
                case LESS_THAN:
                    predicates.add(builder.lessThan(root.get(criteria.getAttr()), criteria.getValue().toString()));
                    break;
                case GREATER_THAN_EQUAL:
                    predicates.add(builder.greaterThanOrEqualTo(root.get((criteria.getAttr())), criteria.getValue().toString()));
                    break;
                case LESS_THAN_EQUAL:
                    predicates.add(builder.lessThanOrEqualTo(root.get(criteria.getAttr()), criteria.getValue().toString()));
                    break;
                case NOT_EQUAL:
                    predicates.add(builder.notEqual(root.get(criteria.getAttr()), criteria.getValue()));
                    break;
                case EQUAL:
                    predicates.add(builder.equal(root.get(criteria.getAttr()), criteria.getValue()));
                    break;
                case MATCH:
                    predicates.add(builder.like(builder.lower(root.get(criteria.getAttr())),"%" + criteria.getValue().toString().toLowerCase() + "%"));
                    break;
                case MATCH_END:
                    predicates.add(builder.like(builder.lower(root.get(criteria.getAttr())),criteria.getValue().toString().toLowerCase() + "%"));
                    break;
                case MATCH_START:
                    predicates.add(builder.like(builder.lower(root.get(criteria.getAttr())), "%" + criteria.getValue().toString().toLowerCase()));
                    break;
                case IN:
                    predicates.add(builder.in(root.get(criteria.getAttr())).value(criteria.getValue()));
                    break;
                case NOT_IN:
                    predicates.add(builder.not(root.get(criteria.getAttr())).in(criteria.getValue()));
            }
        }
        return builder.and(predicates.toArray(new Predicate[0]));
    }
}

Esta clase extiende de Specifications, y te permite crear múltiples consultas a partir de las distintas operaciones definidas en SearchOperation. Esta clase puede ser utilizada por cualquier entidad para generar multiples consultas de manera dinámica. Veamos como se utiliza para buscar Eventos.

Clase EventService
package cu.sacavix.springboot.jpa.specifications.service;
import cu.sacavix.springboot.jpa.specifications.entity.Event;
import cu.sacavix.springboot.jpa.specifications.repository.EventRepository;
import cu.sacavix.springboot.jpa.specifications.search.SearchCriteria;
import cu.sacavix.springboot.jpa.specifications.search.SearchOperation;
import cu.sacavix.springboot.jpa.specifications.search.SearchSpecifications;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class EventService {
    @Autowired
    private EventRepository eventRepository ;
    /**
     * Buscar todos los eventos donde los deportes, paises y ligas coincidan con sports, countries, leagues y que sea despues de la fecha actual
     * @param sports
     * @param countries
     * @param leagues
     * @return
     */
    public List<Event> search(List<String> sports, List<String> countries, List<String> leagues) {
        SearchSpecifications<Event> searchSpecifications = new SearchSpecifications<>();
        if(sports.size()>0){
            searchSpecifications.add(new SearchCriteria("sport",sports, SearchOperation.IN));
        }
        if(countries.size()>0){
            searchSpecifications.add(new SearchCriteria("country",countries, SearchOperation.IN));
        }
        if(leagues.size()>0){
            searchSpecifications.add(new SearchCriteria("league",leagues, SearchOperation.IN));
        }
        searchSpecifications.add(new SearchCriteria("date", new Date().getTime(), SearchOperation.GREATER_THAN));
        return eventRepository.findAll(searchSpecifications) ;
    }
}

Se adicionan criterios de búsquedas de forma dinámica.

Con este sencillo ejemplo podemos evidenciar como hacer consultas flexibles y dinámicas, elemento indispensable para aplicaciones grandes. Como siempre pueden ver el resto del código en este enlace: github.