Всем доброго времени суток, хочу поделиться опытом поиска через JpaSpecificationExecutor. Авторы — Oliver Gierke и Christoph Strobl.
А так же много хорошего контента a.k.a Best Practices на нашем телеграм канале Java Best Practices присоединяйся и делись своим личном опытом мы рады каждому из Java community
JpaSpecificationExecutor является интерфейсом позволяющий выполнять Specification на основе API критериев JPA
И так допустим что нам надо вернуть массив данных по параметрам (page, perPage) и что бы была возможность динамической сортировки возвращаемого объекта
Для этого нам потребуется создать класс Criteria который будет содержать базовые параметры поиска.
public class GenericCriteria implements Criterias, Serializable {
protected Integer page;
protected Integer perPage;
protected String sortBy;
protected String sortDirection;
@ApiModelProperty(hidden = true)
public String getSortDirection() {
return sortDirection == null || sortDirection.equals("") ? "asc" : sortDirection;
}
@ApiModelProperty(hidden = true)
public Sort.Direction getDirection() {
return getSortDirection().toLowerCase().equals("asc") ? Sort.Direction.ASC : Sort.Direction.DESC;
}
}
Как видно из класса она имеет 4 поля которые можно указать в GET запросе ? page=1&perPage=10&sortBy=id&sortDirection=desc
Для дополнения класса полями можно создать собственный класс и наследоваться от GenericCriteria
Далее создадим абстрактный сервис в котором будет определен наш метод для поиска
public abstract class AbstractJpaService<AU extends Entity, C extends GenericCriteria, R extends Repository> {
protected final R repository;
@Autowired
public AbstractJpaService(R repository) {
this.repository = repository;
}
public Page<AU> findAll(C criteria) {
if (repository instanceof JpaSpecificationExecutor) {
return ((JpaSpecificationExecutor<AU>) repository).findAll((Specification<AU>) (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
processCriteriaSpecifications(root, criteriaBuilder, predicates, criteria);
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}, paging(criteria));
}
return null;
}
private Pageable paging(C criteria) {
Pageable paging = Pageable.unpaged();
if (!((criteria.getPage() == null || criteria.getPerPage() == null) || (criteria.getPage() < 0 || criteria.getPerPage() <= 0))) {
if (!utils.isEmpty(criteria.getSortBy())) {
paging = PageRequest.of(criteria.getPage(), criteria.getPerPage(), Sort.by(criteria.getDirection(), criteria.getSortBy()));
} else {
paging = PageRequest.of(criteria.getPage(), criteria.getPerPage());
}
}
return paging;
}
protected void processCriteriaSpecifications(Root<AU> root, CriteriaBuilder cb, List<Predicate> predicates, C criteria) {
}
}
Метод findAll принимает в качестве параметра Generic класс который наследуется от GenericCriteria и возвращает класс Page. Далее метод вызывает метод findAll итрейфейса JpaSpecificationExecutor и в лямбда выражении определяет интерфейс Specification с методом toPredicate который принимает три параметра: Root, CriteriaQuery и CriteriaBuilder
Далле вызывается метод processCriteriaSpecifications
Данный абстрактный класс по умолчанию работает без дополнительных predicate-ов только имея возможность для сортировки и пагинации
Для того что бы мы имели возможность динамического поиска, мы должны создать класс наследник от AbstractJpaService и переопределить метод processCriteriaSpecifications
@Override
protected void processCriteriaSpecifications(Root<Auction> root, CriteriaBuilder cb, List<Predicate> predicates, MyCriteria criteria) {
if (!utils.isEmpty(criteria.getGoodsId())) {
predicates.add(cb.equal(root.get("goods").get("id"), criteria.getGoodsId()));
}
if (!utils.isEmpty(criteria.getCategoryId())) {
predicates.add(cb.equal(root.get("category").get("id"), criteria.getCategoryId()));
}
if (!utils.isEmpty(criteria.getName())) {
predicates.add(cb.like(cb.lower(root.get("name")), utils.toSqlLike(criteria.getName())));
}
if (!utils.isEmpty(criteria.getType())) {
predicates.add(cb.equal(root.get("type"), criteria.getType()));
}
if (!utils.isEmpty(criteria.getStartFrom()) || !utils.isEmpty(criteria.getStartTo())) {
if (!utils.isEmpty(criteria.getStartFrom()) && !utils.isEmpty(criteria.getStartTo())) {
predicates.add(cb.between(root.get("beginAt"), utils.parseToLocalDateTime(criteria.getStartFrom()), utils.parseToLocalDateTime(criteria.getStartTo())));
} else if (!utils.isEmpty(criteria.getStartFrom())) {
predicates.add(cb.greaterThanOrEqualTo(root.get("beginAt"), utils.parseToLocalDateTime(criteria.getStartFrom())));
} else {
predicates.add(cb.lessThanOrEqualTo(root.get("beginAt"), utils.parseToLocalDateTime(criteria.getStartFrom())));
}
} else {
predicates.add(cb.greaterThanOrEqualTo(root.get("beginAt"), LocalDateTime.now()));
}
}
Таким не хитрым образом мы использовали принципы solid и добились динамического поиска довольно просто и удобно.
Спасибо за внимание!
А так же много хорошего контента a.k.a Best Practices на нашем телеграм канале Java Best Practices присоединяйся и делись своим личном опытом мы рады каждому из Java community