[Java] Делаем динамический отчет средствами JPA Criteria.Api
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Очень часто в корпоративной разработке происходит диалог:
Сталкивались?
В данной статье мы рассмотрим, каким образом можно сделать запросы по таблице с изменяющимся списком критериев в среде Spring+JPA/Hibernate без прикручивания дополнительных библиотек.
Основных вопросов всего два:
- Как динамически собрать SQL-запрос
- Как передать условия для формирования этого запроса
Для сборки запросов JPA, начиная с 2.0 (а это было очень и очень давно), предлагает решение – Criteria Api, продукты которого – объекты Specification, мы можем далее передавать в параметры методов JPA-репозиториев.
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
@Entity
public class ExampleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
public int value;
public ExampleEntity(int value){
this.value = value;
}
}
...
// репозиторий
@Repository
public 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 можно почитать здесь.
===========
Источник:
habr.com
===========
Похожие новости:
- [C, JavaScript, Интернет вещей, Программирование микроконтроллеров, Разработка для интернета вещей] Термостат на ThingJS (beta)
- [JavaScript, Алгоритмы, Программирование] Нестабильная сортировка в JavaScript
- [Big Data, Java, Scala] ZTools для Apache Zeppelin
- [JavaScript] «Никогда не писали автотесты? Попробуйте Cypress»
- [Программирование, Конференции, Учебный процесс в IT, Карьера в IT-индустрии] Бесплатные онлайн-мероприятия по разработке (28 сентября – 7 октября)
- [JavaScript, VueJS] Как я умный аквариум делал (frontend)
- [JavaScript, Node.JS, Финансы в IT] Принимаем криптовалютные платежи с Coinbase Commerce
- [JavaScript, Развитие стартапа, Разработка веб-сайтов] Как быстро создать Bootstrap-сайт для бизнеса: 6 полезных инструментов
- [Google Chrome, JavaScript, Софт] Используем Chrome DevTools профессионально (перевод)
- [JavaScript, Программирование] Compose повсюду: композиция функций в JavaScript (перевод)
Теги для поиска: #_java, #_java, #_jpa, #_criteria.api, #_blog_kompanii_rosselhozbank (
Блог компании Россельхозбанк
), #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:39
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Очень часто в корпоративной разработке происходит диалог: Сталкивались? В данной статье мы рассмотрим, каким образом можно сделать запросы по таблице с изменяющимся списком критериев в среде Spring+JPA/Hibernate без прикручивания дополнительных библиотек. Основных вопросов всего два:
Для сборки запросов JPA, начиная с 2.0 (а это было очень и очень давно), предлагает решение – Criteria Api, продукты которого – объекты Specification, мы можем далее передавать в параметры методов JPA-репозиториев. 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
@Entity public class ExampleEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; public int value; public ExampleEntity(int value){ this.value = value; } } ... // репозиторий @Repository public 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 можно почитать здесь. =========== Источник: habr.com =========== Похожие новости:
Блог компании Россельхозбанк ), #_java |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:39
Часовой пояс: UTC + 5