java - jparepository - repository spring data jpa
Filtrar las filas de la base de datos con spring-data-jpa y spring-mvc (1)
Tengo un proyecto spring-mvc que está utilizando spring-data-jpa para acceso a datos. Tengo un objeto de dominio llamado Travel
y quiero permitir que el usuario final le aplique varios filtros.
Para eso, he implementado el siguiente controlador:
@Autowired
private TravelRepository travelRep;
@RequestMapping("/search")
public ModelAndView search(
@RequestParam(required= false, defaultValue="") String lastName,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page<Travel> travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject("page", page);
mav.addObject("lastName", lastName);
return mav;
}
Esto funciona bien: el usuario tiene un formulario con un cuadro de entrada lastName
que se puede usar para filtrar Travels.
Más allá de lastName, mi objeto de dominio Travel
tiene muchos más atributos por los cuales me gustaría filtrar. Creo que si estos atributos fueran todas las cadenas, podría agregarlos como @RequestParam
y agregar un método spring-data-jpa para consultar por estos. Por ejemplo, agregaría un método findByLastNameLikeAndFirstNameLikeAndShipNameLike
.
Sin embargo, no sé cómo debo hacerlo cuando necesito filtrar las claves externas. Por lo tanto, mi Travel
tiene un atributo de period
que es una clave externa al objeto de dominio Period
, que necesito tener como un menú desplegable para que el usuario seleccione el Period
.
Lo que quiero hacer es cuando el período sea nulo. Quiero recuperar todos los viajes filtrados por el apellido y cuando el período no sea nulo, quiero recuperar todos los viajes para este período filtrados por el apellido.
Sé que esto se puede hacer si implemento dos métodos en mi repositorio y uso un if
para mi controlador:
public ModelAndView search(
@RequestParam(required= false, defaultValue="") String lastName,
@RequestParam(required= false, defaultValue=null) Period period,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
Page travels = null;
if(period==null) {
travels = travelRep.findByLastNameLike("%"+lastName+"%", pageable);
} else {
travels = travelRep.findByPeriodAndLastNameLike(period,"%"+lastName+"%", pageable);
}
mav.addObject("page", page);
mav.addObject("period", period);
mav.addObject("lastName", lastName);
return mav;
}
¿Hay alguna manera de hacer esto sin usar el if
? My Travel no solo tiene el período sino también otros atributos que deben filtrarse utilizando los menús desplegables. Como puede comprender, la complejidad aumentaría exponencialmente cuando necesite utilizar más menús desplegables, ya que todas las combinaciones tendrían que considerarse :(
Actualización 03/12/13 : Continuando con la excelente respuesta de M. Deinum, y después de implementarla, me gustaría brindar algunos comentarios sobre la compleción de la pregunta / asnwer:
En lugar de implementar
JpaSpecificationExecutor
, debe implementarJpaSpecificationExecutor<Travel>
para evitar advertencias de verificación de tipo.Por favor, eche un vistazo a la excelente respuesta de kostja a esta pregunta. JPA CriteriaBuilder realmente dinámico, ya que tendrá que implementar esto si quiere tener los filtros correctos.
La mejor documentación que pude encontrar para la API de Criteria fue http://www.ibm.com/developerworks/library/j-typesafejpa/ . Esta es una lectura bastante larga, pero la recomiendo totalmente: después de leerla, se respondieron la mayoría de mis preguntas para Root y CriteriaBuilder :)
No fue posible reutilizar el objeto
Travel
porque contenía varios otros objetos (que también contenían otros objetos) que necesitaba buscar usandoLike
; en su lugar, utilicé un objetoTravelSearch
que contenía los campos que necesitaba buscar.
Actualización 10/05/15 : según la solicitud de @priyank, así es cómo implementé el objeto TravelSearch:
public class TravelSearch {
private String lastName;
private School school;
private Period period;
private String companyName;
private TravelTypeEnum travelType;
private TravelStatusEnum travelStatus;
// Setters + Getters
}
Este objeto fue utilizado por TravelSpecification (la mayoría del código es específico del dominio pero lo dejo allí como un ejemplo):
public class TravelSpecification implements Specification<Travel> {
private TravelSearch criteria;
public TravelSpecification(TravelSearch ts) {
criteria= ts;
}
@Override
public Predicate toPredicate(Root<Travel> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
Join<Travel, Candidacy> o = root.join(Travel_.candidacy);
Path<Candidacy> candidacy = root.get(Travel_.candidacy);
Path<Student> student = candidacy.get(Candidacy_.student);
Path<String> lastName = student.get(Student_.lastName);
Path<School> school = student.get(Student_.school);
Path<Period> period = candidacy.get(Candidacy_.period);
Path<TravelStatusEnum> travelStatus = root.get(Travel_.travelStatus);
Path<TravelTypeEnum> travelType = root.get(Travel_.travelType);
Path<Company> company = root.get(Travel_.company);
Path<String> companyName = company.get(Company_.name);
final List<Predicate> predicates = new ArrayList<Predicate>();
if(criteria.getSchool()!=null) {
predicates.add(cb.equal(school, criteria.getSchool()));
}
if(criteria.getCompanyName()!=null) {
predicates.add(cb.like(companyName, "%"+criteria.getCompanyName()+"%"));
}
if(criteria.getPeriod()!=null) {
predicates.add(cb.equal(period, criteria.getPeriod()));
}
if(criteria.getTravelStatus()!=null) {
predicates.add(cb.equal(travelStatus, criteria.getTravelStatus()));
}
if(criteria.getTravelType()!=null) {
predicates.add(cb.equal(travelType, criteria.getTravelType()));
}
if(criteria.getLastName()!=null ) {
predicates.add(cb.like(lastName, "%"+criteria.getLastName()+"%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
}
Finalmente, aquí está mi método de búsqueda:
@RequestMapping("/search")
public ModelAndView search(
@ModelAttribute TravelSearch travelSearch,
Pageable pageable) {
ModelAndView mav = new ModelAndView("travels/list");
TravelSpecification tspec = new TravelSpecification(travelSearch);
Page<Travel> travels = travelRep.findAll(tspec, pageable);
PageWrapper<Travel> page = new PageWrapper<Travel>(travels, "/search");
mav.addObject(travelSearch);
mav.addObject("page", page);
mav.addObject("schools", schoolRep.findAll() );
mav.addObject("periods", periodRep.findAll() );
mav.addObject("travelTypes", TravelTypeEnum.values());
mav.addObject("travelStatuses", TravelStatusEnum.values());
return mav;
}
Espero que haya ayudado!
Para empezar, debe dejar de usar @RequestParam
y poner todos sus campos de búsqueda en un objeto (tal vez reutilizar el objeto Travel para eso). Luego tiene 2 opciones que puede usar para construir dinámicamente una consulta
- Usa el
JpaSpecificationExecutor
y escribe unaSpecification
- Utilice
QueryDslPredicateExecutor
y use QueryDSL para escribir un predicado.
Usando JpaSpecificationExecutor
Primero agregue el JpaSpecificationExecutor
a su JpaSpecificationExecutor
esto le dará un findAll(Specification)
y usted puede eliminar sus métodos de búsqueda personalizados.
public interface TravelRepository extends JpaRepository<Travel, Long>, JpaSpecificationExecutor<Travel> {}
Luego puede crear un método en su repositorio que use una Specification
que básicamente construye la consulta. Consulte la documentation Spring Data JPA para esto.
Lo único que debe hacer es crear una clase que implemente la Specification
y que genere la consulta en función de los campos que están disponibles. La consulta se genera mediante el enlace API de criterios de JPA.
public class TravelSpecification implements Specification<Travel> {
private final Travel criteria;
public TravelSpecification(Travel criteria) {
this.criteria=criteria;
}
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
// create query/predicate here.
}
}
Y finalmente necesita modificar su controlador para usar el nuevo método findAll
(me tomé la libertad de limpiarlo un poco).
@RequestMapping("/search")
public String search(@ModelAttribute Travel search, Pageable pageable, Model model) {
Specification<Travel> spec = new TravelSpecification(search);
Page<Travel> travels = travelRep.findAll(spec, pageable);
model.addObject("page", new PageWrapper(travels, "/search"));
return "travels/list";
}
Usando QueryDslPredicateExecutor
Primero agregue QueryDslPredicateExecutor
a su QueryDslPredicateExecutor
esto le dará un findAll(Predicate)
y podrá eliminar sus métodos de búsqueda personalizados.
public interface TravelRepository extends JpaRepository<Travel, Long>, QueryDslPredicateExecutor<Travel> {}
Luego implementaría un método de servicio que usaría el objeto Travel
para construir un predicado usando QueryDSL.
@Service
@Transactional
public class TravelService {
private final TravelRepository travels;
public TravelService(TravelRepository travels) {
this.travels=travels;
}
public Iterable<Travel> search(Travel criteria) {
BooleanExpression predicate = QTravel.travel...
return travels.findAll(predicate);
}
}
Ver también esta publicación de pantano .