Я бы порекомендовал Спецификация или Querydsl .
Он основан на доменном дизайне / модели, и реализация основывается на критериях JPA, которые обеспечивают гибкость в построении запросов.
Вот пример (не проверено, но вы должны получить общее представление).
Характеристики книги
Класс фабрики для динамического создания спецификаций.
public class BookSpecifications {
private BookSpecifications(){
}
public static Specification<Book> withAuthor(String author) {
return new Specification<Book>() {
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.get("author"), author);
}
};
}
public static Specification<Book> withTitle(String title) {
return new Specification<Book>() {
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.get("title"), title);
}
};
}
}
Обратите внимание, что использование постоянных значений, которые были сгенерированы во время компиляции из классов Entity, является более подходящим подходом.
Yo может ссылаться на атрибуты сущности с помощью Book.author
или Book.title
вместо использования некоторых String
, не проверенных во время компиляции, с реальной моделью сущности, таких как «автор» или «заголовок».
BookController
На стороне контроллера вы должны избегать сокращения до минимума логики и отдавать предпочтение делегированию обработки классу обслуживания, который создаст требуемую спецификацию и передаст ее в хранилище.
public class BookController {
@GetMapping
public ResponseEntity<List<Book>> searchBooksByTitleAndAuthor(
@RequestParam(value = "title", required = false) String title,
@RequestParam(value = "author", required = false) String author) {
List<Book> books = bookService.findAll(title, author);
if (books.isEmpty()) {
log.info("No books with this specification ");
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(books, HttpStatus.OK);
}
}
BookService
Вся логика здесь (модульные тесты должны быть сосредоточены здесь).
public class BookService {
private BookRepository bookRepository;
public BookService(BookRepository bookRepository){
this.bookRepository = bookRepository;
}
public List<Book> findAll(String title, String author) {
if (Stream.of(title, author)
.filter(s-> s == null || s.equals(""))
.count() == 0) {
return new ArrayList<>();
}
Specification<Book> specification = createSpecification(specification, title, () -> BookSpecifications.withTitle(title));
specification = createSpecification(specification, author, () -> BookSpecifications.withAuthor(author));
List<Book> books = bookRepository.findAll(specification);
return books;
}
Specification<Book> createSpecification(Specification<Book> currentSpecification, String arg, Callable<Specification<Book>> callable) {
// no valued parameter so we return
if (arg == null) {
return currentSpecification;
}
try {
// Specification instance already created : reuse it
if (currentSpecification != null) {
return currentSpecification.and(callable.call());
}
// Specification instance not created yes : create a new one
return callable.call();
} catch (Exception e) {
// handle the exception... if any
}
}
}
Добавить новый атрибут сущности в поиск очень просто.
Добавьте параметр в метод и в поток, который проверяет, заполнен ли хотя бы критерий поиска, а затем добавьте новый вызов в цепочку createSpecification()
:
// existing
Specification<Book> specification = createSpecification(specification, title, () -> BookSpecifications.withTitle(title));
specification = createSpecification(specification, author, () -> BookSpecifications.withAuthor(author));
// change below !
specification = createSpecification(specification, anotherColumn, () -> BookSpecifications.withAnotherColumn(anotherColumn));
BookRepository
Последний шаг: сделайте ваш BookRepository
интерфейс расширяемым JpaSpecificationExecutor
, чтобы иметь возможность вызывать Repository
методы, принимающие Specification<T>
параметры, такие как:
List<T> findAll(@Nullable Specification<T> spec);
Это должно быть хорошо:
@Repository
public interface BookRepository extends JpaRepository<Book, Long> , JpaSpecificationExecutor<Book> {
}
Обратите внимание, что если может быть запрошено много атрибутов объекта, использование более динамичного подхода может быть интересным:
public class BookSpecifications {
private BookSpecifications(){
}
public static Specification<Book> withAttribute(String name, T value) {
return new Specification<Book>() {
public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.get(name), value);
}
};
}
}
Но у него есть и недостаток для отсрочки обнаружения ошибок кодирования.
Действительно, ошибки в спецификации здания (например, тип значения не совместимы) могут быть обнаружены только во время выполнения.