java - jparepository - spring data specifications examples
Spring Data JPA: Creando consultas de obtención de especificaciones (3)
Clase de especificación:
public class MatchAllWithSymbol extends Specification<Gene> {
private String symbol;
public CustomSpec (String symbol) {
this.symbol = symbol;
}
@Override
public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//This part allow to use this specification in pageable queries
//but you must be aware that the results will be paged in
//application memory!
Class clazz = query.getResultType();
if (clazz.equals(Long.class) || clazz.equals(long.class))
return null;
//building the desired query
root.fetch("aliases", JoinType.LEFT);
root.fetch("attributes", JoinType.LEFT);
query.distinct(true);
query.orderBy(cb.asc(root.get("entrezGeneId")));
return cb.equal(root.get("symbol"), symbol);
}
}
Uso:
List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol"));
TL; DR : ¿Cómo se replican las operaciones de unión a captura de JPQL usando especificaciones en Spring Data JPA?
Estoy tratando de construir una clase que maneje la creación dinámica de consultas para las entidades JPA utilizando Spring Data JPA. Para hacer esto, estoy definiendo una serie de métodos que crean objetos de Predicate
(como se sugiere en los documentos de Spring Data JPA y en otros lugares), y luego los encadeno cuando se envía el parámetro de consulta apropiado. Algunas de mis entidades tienen relaciones de uno a muchos con otras entidades que ayudan a describirlas, que se obtienen con entusiasmo cuando se las consulta y se unen en colecciones o mapas para la creación de DTO. Un ejemplo simplificado:
@Entity
public class Gene {
@Id
@Column(name="entrez_gene_id")
privateLong id;
@Column(name="gene_symbol")
private String symbol;
@Column(name="species")
private String species;
@OneToMany(mappedBy="gene", fetch=FetchType.EAGER)
private Set<GeneSymbolAlias> aliases;
@OneToMany(mappedBy="gene", fetch=FetchType.EAGER)
private Set<GeneAttributes> attributes;
// etc...
}
@Entity
public class GeneSymbolAlias {
@Id
@Column(name = "alias_id")
private Long id;
@Column(name="gene_symbol")
private String symbol;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="entrez_gene_id")
private Gene gene;
// etc...
}
Los parámetros de la cadena de consulta se pasan de la clase Controller
a la clase Service
como pares clave-valor, donde se procesan y ensamblan en Predicates
:
@Service
public class GeneService {
@Autowired private GeneRepository repository;
@Autowired private GeneSpecificationBuilder builder;
public List<Gene> findGenes(Map<String,Object> params){
return repository.findAll(builder.getSpecifications(params));
}
//etc...
}
@Component
public class GeneSpecificationBuilder {
public Specifications<Gene> getSpecifications(Map<String,Object> params){
Specifications<Gene> = null;
for (Map.Entry param: params.entrySet()){
Specification<Gene> specification = null;
if (param.getKey().equals("symbol")){
specification = symbolEquals((String) param.getValue());
} else if (param.getKey().equals("species")){
specification = speciesEquals((String) param.getValue());
} //etc
if (specification != null){
if (specifications == null){
specifications = Specifications.where(specification);
} else {
specifications.and(specification);
}
}
}
return specifications;
}
private Specification<Gene> symbolEquals(String symbol){
return new Specification<Gene>(){
@Override public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder builder){
return builder.equal(root.get("symbol"), symbol);
}
};
}
// etc...
}
En este ejemplo, cada vez que quiero recuperar un registro Gene
, también quiero sus registros GeneAttribute
y GeneSymbolAlias
asociados. Todo esto funciona como se esperaba, y una solicitud de un solo Gene
activará 3 consultas: una para cada una de las tablas Gene
, GeneAttribute
y GeneSymbolAlias
.
El problema es que no hay ninguna razón por la que se deban ejecutar 3 consultas para obtener una única entidad de Gene
con atributos y alias incorporados. Esto se puede hacer en SQL plano, y se puede hacer con una consulta de JPQL en mi repositorio de Spring Data JPA:
@Query(value = "select g from Gene g left join fetch g.attributes join fetch g.aliases where g.symbol = ?1 order by g.entrezGeneId")
List<Gene> findBySymbol(String symbol);
¿Cómo puedo replicar esta estrategia de recuperación utilizando las especificaciones? Encontré esta pregunta aquí , pero solo parece hacer que las tomas perezosas se vuelvan ávidas.
Puede especificar la búsqueda de unión mientras crea la Especificación, pero dado que la misma especificación será utilizada por métodos localizables también como findAll (Especificación var1, Vareable pagable) y la consulta de recuento se quejará debido a la combinación de unión. Por lo tanto, para manejar eso podemos verificar el tipo de resultado de CriteriaQuery y aplicar la unión solo si no es Largo (tipo de resultado para consulta de conteo). ver código abajo:
public static Specification<Item> findByCustomer(Customer customer) {
return (root, criteriaQuery, criteriaBuilder) -> {
/*
Join fetch should be applied only for query to fetch the "data", not for "count" query to do pagination.
Handled this by checking the criteriaQuery.getResultType(), if it''s long that means query is
for count so not appending join fetch else append it.
*/
if (Long.class != criteriaQuery.getResultType()) {
root.fetch(Person_.itemInfo.getName(), JoinType.LEFT);
}
return criteriaBuilder.equal(root.get(Person_.customer), customer);
};
}
Sugiero esta biblioteca para la especificación. https://github.com/tkaczmarzyk/specification-arg-resolver
De esta biblioteca: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch
Puede utilizar la anotación @JoinFetch para especificar rutas para realizar la unión de recuperación en. Por ejemplo:
@RequestMapping("/customers")
public Object findByOrderedOrFavouriteItem(
@Joins({
@Join(path = "orders", alias = "o")
@Join(path = "favourites", alias = "f")
})
@Or({
@Spec(path="o.itemName", params="item", spec=Like.class),
@Spec(path="f.itemName", params="item", spec=Like.class)}) Specification<Customer> customersByItem) {
return customerRepo.findAll(customersByItem);
}