Динамический поиск JPA

Всем доброго времени суток, хочу поделиться опытом поиска через 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

Начать дискуссию