Сталкивались?
В данной статье мы рассмотрим, каким образом можно сделать запросы по таблице с изменяющимся списком критериев в среде Spring+JPA/Hibernate без прикручивания дополнительных библиотек.
Основных вопросов всего два:
- Как динамически собрать SQL-запрос
- Как передать условия для формирования этого запроса
Для сборки запросов JPA, начиная с 2.0 (
Specification итоговые ограничения запроса, содержит объекты Predicate как условия WHERE, HAVING. Предикаты конечные выражения, которые могут принимать значения true или false.
Одиночное условие состоит из поля, оператора сравнения и значения для сравнения. Также условия могут быть вложенными. Опишем полностью условие классом SearchCriteria:
public class SearchCriteria{ //Сравниваемое поле String key; //Оператор сранения(больше, меньше и пр.) SearchOperator operator; //Значение для сравнения String value; //Тип примыкания дочерних выражений private JoinType joinType; //Список дочерних выражений private List<SearchCriteria> criteria;}
Теперь опишем сам построитель. Он будет уметь строить спецификацию на основании поданного списка условий, а также объединять несколько спецификаций определенным образом:
/*** Построитель спецификаций*/public class JpaSpecificationsBuilder<T> { // список возможных операций private Map<SearchOperation, PredicateBuilder> predicateBuilders = Stream.of( new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.EQ,new EqPredicateBuilder()), new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.MORE,new MorePredicateBuilder()), new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.MOREQ,new MoreqPredicateBuilder()), new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.LESS,new LessPredicateBuilder()), new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.LESSEQ,new LesseqPredicateBuilder()) ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); /** * Строит спецификацию по поданным условиям */ public Specification<T> buildSpecification(SearchCriteria criterion){ return (root, query, cb) -> buildPredicate(root,cb,criterion); } /** * Объединяет спецификации */ public Specification<T> mergeSpecifications(List<Specification> specifications, JoinType joinType) { return (root, query, cb) -> { List<Predicate> predicates = new ArrayList<>(); specifications.forEach(specification -> predicates.add(specification.toPredicate(root, query, cb))); if(joinType.equals(JoinType.AND)){ return cb.and(predicates.toArray(new Predicate[0])); } else{ return cb.or(predicates.toArray(new Predicate[0])); } }; }}
Чтобы не городить огромный if для операций сравнения, реализуем Map операторов вида <Операция, Оператор>. Оператор должен уметь построить одиночный предикат. Приведу пример операции ">", остальные пишем по аналогии:
public class EqPredicateBuilder implements PredicateBuilder { @Override public SearchOperation getManagedOperation() { return SearchOperation.EQ; } @Override public Predicate getPredicate(CriteriaBuilder cb, Path path, SearchCriteria criteria) { if(criteria.getValue() == null){ return cb.isNull(path); } if(LocalDateTime.class.equals(path.getJavaType())){ return cb.equal(path,LocalDateTime.parse(criteria.getValue())); } else { return cb.equal(path, criteria.getValue()); } }}
Теперь осталось реализовать рекурсивный разбор нашей структуры SearchCriteria. Отмечу, метод buildPath, который по Root области определения объекта T будет находить путь к полю, на которое ссылается SearchCriteria.key:
private Predicate buildPredicate(Root<T> root, CriteriaBuilder cb, SearchCriteria criterion) { if(criterion.isComplex()){ List<Predicate> predicates = new ArrayList<>(); for (SearchCriteria subCriterion : criterion.getCriteria()) { // стоит реализовать ограничитель глубины рекурсии, но мы ленивые и не будем этого делать predicates.add(buildPredicate(root,cb,subCriterion)); } if(JoinType.AND.equals(criterion.getJoinType())){ return cb.and(predicates.toArray(new Predicate[0])); } else{ return cb.or(predicates.toArray(new Predicate[0])); } } return predicateBuilders.get(criterion.getOperation()).getPredicate(cb,buildPath(root, criterion.getKey()),criterion);} private static Path buildPath(Root<?> root, String key) { if (!key.contains(".")) { return root.get(key); } else { String[] path = key.split("\\."); // Если в нашем выражении присутствует символ ".", постепенно проходим иерархию Root-а до конечного элемента. Join<Object, Object> join = root.join(path[0]); for (int i = 1; i < path.length - 1; i++) { join = join.join(path[i]); } return join.get(path[path.length - 1]); } }
Напишем тестовый кейс для нашего построителя:
//Класс Entity@Entitypublic class ExampleEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; public int value; public ExampleEntity(int value){ this.value = value; } } ... // репозиторий@Repositorypublic interface ExampleEntityRepository extends JpaRepository<ExampleEntity,Long>, JpaSpecificationExecutor<ExampleEntity> {} ... // тест/*ваши настройки запуска*/public class JpaSpecificationsTest { @Autowired private ExampleEntityRepository exampleEntityRepository; @Test public void getWhereMoreAndLess(){ exampleEntityRepository.save(new ExampleEntity(3)); exampleEntityRepository.save(new ExampleEntity(5)); exampleEntityRepository.save(new ExampleEntity(0)); SearchCriteria criterion = new SearchCriteria( null,null,null, Arrays.asList( new SearchCriteria("value",SearchOperation.MORE,"0",null,null), new SearchCriteria("value",SearchOperation.LESS,"5",null,null) ), JoinType.AND ); assertEquals(1,exampleEntityRepository.findAll(specificationsBuilder.buildSpecification(criterion)).size()); } }
Итого, мы научили наше приложение разбирать логическое выражение, используя Criteria.API. Набор операций в текущей реализации ограничен, но читатель может самостоятельно реализовать нужные ему. На практике решение применено, но пользователям не интересно(
Реализованную версию вы можете найти в моем репозитории на Github
Более подробно о Criteria.Api можно почитать здесь.