Queries Dinámicos en Spring Data JPA

Tuve la necesidad de crear un Query de forma dinámica con JPA, pues tenía varios métodos definidos y podrían crecer mas, algo así:

QuestionRepository.groovy

interface QuestionRepository extends PagingAndSortingRepository<Question,Long> {
  List<Question> findAllByAuthorAndEnabledAndStatusInAndParentIsNull(UserGE author,Boolean enabled,List<String> statuses, Pageable pageable)
  List<Question> findAllByCampus_CodeLikeAndSubject_subjectNumberLikeAndTypeLikeAndCareer_codeLikeAndUnityLikeAndComplexityLikeAndAuthorAndEnabledAndStatusInAndParentIsNull(String campusCode, String subjectNumber, String type, String careerCode, String unity, String complexity, UserGE author, Boolean enabled,List<String> statuses, Pageable pageable)
  long countByAuthorAndEnabledAndStatusInAndParentIsNull(UserGE author,Boolean enabled,List<String> statuses)
  long countByCampus_CodeLikeAndSubject_subjectNumberLikeAndTypeLikeAndCareer_codeLikeAndUnityLikeAndComplexityLikeAndAuthorAndEnabledAndStatusInAndParentIsNull(String campusCode, String subjectNumber, String type, String careerCode, String unity, String complexity,UserGE author, Boolean enabled,List<String> statuses)
  List<Question> findAllByEnabledAndStatusInAndParentIsNull(Boolean enabled,List<String> statuses, Pageable pageable)
  List<Question> findAllByCampus_CodeLikeAndSubject_subjectNumberLikeAndTypeLikeAndCareer_codeLikeAndUnityLikeAndComplexityLikeAndEnabledAndStatusInAndParentIsNull(String campusCode, String subjectNumber, String type,  String careerCode, String unity, String complexity,Boolean enabled,List<String> statuses, Pageable pageable)
  long countAllByEnabledAndStatusInAndParentIsNull(Boolean enabled,List<String> statuses)
  long countAllByCampus_CodeLikeAndEnabledAndStatusInAndParentIsNull(String campusCode, Boolean enabled,List<String> statuses)
  long countAllByCampus_CodeLikeAndSubject_subjectNumberLikeAndTypeLikeAndCareer_codeLikeAndUnityLikeAndComplexityLikeAndEnabledAndStatusInAndParentIsNull(String campusCode, String subjectNumber, String type,  String careerCode, String unity, String complexity, Boolean enabled,List<String> statuses)
  List<Question> findAllByAuthorAndStatusNotInAndParentIsNull(UserGE author,List<String> statuses)
  List<Question> findAllByParentAndEnabled(Question question,Boolean enabled)
  Question findByUuid(String uuid)
}

Inclusive se tienen que hacer consultas sobre relaciones de la misma clase. Es aquí en dónde la documentación de Spring ayuda diciéndonos al respecto de Specifications(https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#spec...)

Describo lo que hice de manera genérica:

Uso de la interface JpaSpecificationExecutor

Sólo implementa la interface sobre el Repository que ya tengas definido:

QuestionRepository.groovy

interface QuestionRepository extends PagingAndSortingRepository<Question,Long>, JpaSpecificationExecutor {
  // ....
}

Implementa el metamodelo

Te recomiendo que explores el paquete javax.persistence.metamodel, y la documentación de Hibernate para comprender mucho mejor de que trata la generación del metamodelo; en breve, te muestro un ejemplo que denota cuándo tienes atributos simples, una relación con un objeto o con una lista de objetos, lo cuçal funciona para poder buscar a través de ellos, justo como HQL o JPQL.

Question_.groovy

import javax.persistence.metamodel.ListAttribute
import javax.persistence.metamodel.SingularAttribute
import javax.persistence.metamodel.StaticMetamodel
import javax.persistence.metamodel.MapAttribute

@StaticMetamodel(Question.class)
public abstract class Question_ {
    public static volatile MapAttribute<Question, Campus, String> campusCode;
    public static volatile SingularAttribute<Question, String> status;
    public static volatile SingularAttribute<Question, Date> dateCreated;
    public static volatile SingularAttribute<Question, Long> id;
    public static volatile ListAttribute<Question, List<Question>> questions;
    public static volatile SingularAttribute<Question, UserGE> author;
    // ....
}

Observa que el metamodelo hace referencia a la clase original mapeada con @Entity, y que por convención se le suma un guión bajo en el nombre.

Crea el Criterion Spec

Aquí es dónde aplicaremos el _dinamismo_ de la consulta pues en base a un mapa descartaremos la estructura del query:

QuestionCriteriaSpecs.groovy

// ... More imports ...

import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.CriteriaQuery
import javax.persistence.criteria.Predicate
import javax.persistence.criteria.Root

class QuestionCriteriaSpecs {

  static Specification<Question> byParams(Map params){
    { Root<Question> root, CriteriaQuery<?> query, CriteriaBuilder builder ->
      List<Predicate> predicates = [
          builder.like(root.join("campus").getAt("code"), params.campusCode),
          // More builder statements ...
          builder.like(root.getAt("complexity"), params.complexity),
          builder.isNull(root.get("parent")),
          builder.equal(root.getAt("enabled"), true)
          // More builder statements ...
      ]
      if(params.containsKey("author")){
        predicates << builder.equal(root.get("author"), params.author)
      }
      if(params.containsKey("status")){
        predicates << builder.in(root.get(Question_.status), params.status)
      }
      if(params.dateCreated){
        predicates << builder.between(root.get(Question_.dateCreated), params.dateCreated - 1, params.dateCreated + 1)
      }
      builder.and(*predicates)
    } as Specification<Question>
  }
}

Aquí hay varias observaciones:

  • Se hace la implementación de la interface Specification, pero cómo estamos usando Groovy podemos hacerlo con un Closure, sin embargo, podrías hacerlo igual con Java y alguna Lambda.
  • Root usa el Metamodelo definido, query y builder son los que te ayudarán a crear tu búsqueda.
  • Revisá la documentación de cada clase, es muy recomendable.

Sólo úsalo...

Podemos sustituir todas las consultas anteriores por algo cómo lo siguiente:

Pageable thePage = new PageRequest(0, 10, new Sort("id"))
Map params = [code: "CODE", complexity: 2, enabled: true]
if(author){
  params.author = author
}
Page page = questionRepository.findAll(QuestionCriteriaSpecs.byParams(params), thePage)

Sólo es cuestión de manejar un Map y agregar los atributos por los cuáles quiero buscar

En resumen podría decirse que traslade complejidad pero dando un poco más de flexibilidad y re-uso.