I can recommend several approaches to solve such a task:
1) Simple but not flexible: just use request parameters in your controller according to your filter properties (name
, from
, to
) and prepare the corresponding query in your repo, for example:
Controller:
@GetMapping("/q")
public List<ResponseDto> getAllByQuery(
@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "from", required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate from,
@RequestParam(value = "to", required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate to,
Pageable pageable
) {
return service.getByQuery(name, from, to, pageable);
}
Service:
public Page<ResponseDto> getByQuery(String name, LocalDate from, LocalDate to, Pageable pageable) {
return repo.getByQuery(name, from, to, pageable).map(mapper::toResponseDto);
}
Repository:
@Query("select m from MyEntity m where " +
"(?1 is null or upper(m.name) like concat('%', upper(?1), '%')) " +
"and (?2 is null or m.createdAt >= ?2) " +
"and (?3 is null or m.createdAt <= ?3)")
Page<MyEntity> getByQuery(String name, final LocalDate from, final LocalDate to, final Pageable pageable);
Then perform a request:
GET http://localhost:8080/q?name=john&from=2019-04-19&to=2019-04-19
2) Using QueryDsl. You should add it to your project (you can find details here), extend your repo from QuerydslPredicateExecutor
and QuerydslBinderCustomizer
, add some 'tuning' to it:
public interface MyEntityRepo extends JpaRepository<MyEntity, Integer>, QuerydslPredicateExecutor<MyEntity>, QuerydslBinderCustomizer<QMyEntity> {
@Override
default void customize(@NonNull QuerydslBindings bindings, @NonNull QMyEntity entity) {
// Make case-insensitive 'like' filter for all string properties
bindings.bind(String.class).first((SingleValueBinding<StringPath, String>) StringExpression::containsIgnoreCase);
// Add 'between' and 'greater or equal' filter date property
bindings.bind(entity.createdAt).all((path, value) -> {
Iterator<? extends LocalDate> it = value.iterator();
LocalDate from = it.next();
if (value.size() >= 2) {
LocalDate to = it.next();
return Optional.of(path.between(from, to)); // between
} else {
return Optional.of(path.goe(from)); // greater than or equal
}
});
}
Add service method:
public Page<ResponseDto> getAllByQueryDsl(Predicate predicate, Pageable pageable) {
return repo.findAll(predicate, pageable).map(mapper::toResponseDto);
}
Add controller method:
@GetMapping("/query-dsl")
public Page<ResponseDto> getAllByQueryDsl(
@QuerydslPredicate(root = MyEntity.class, bindings = MyEntityRepo.class) Predicate predicate,
Pageable pageable
) {
return service.getAllByQueryDsl(predicate, pageable);
}
And add @DateTimeFormat
annotation to 'Date' properties of your entity:
@Entity
public class MyEntity {
// ...
@DateTimeFormat(iso = ISO.DATE) private LocalDate createdAt;
}
Then you can perform such a request:
GET http://localhost:8080/query-dsl?name=john&createdAt=2019-04-15&createdAt=2019-04-19
Where the first date is 'from' parameter and the second date is 'to' parameter. If you use only one date - it will be 'from' parameter (greater than or equal).
3) Using specification-arg-resolver library. Add it to your project (see instruction: 1 and 2), then extend your repo from JpaSpecificationExecutor
:
public interface MyEntityRepo extends JpaRepository<MyEntity, Integer>, JpaSpecificationExecutor<MyEntity> {}
Add such a method to your controller:
@GetMapping("/specification")
public Page<ResponseDto> getAllBySpecification(
@And({
@Spec(path = "name", spec = LikeIgnoreCase.class),
@Spec(path = "createdAt", params = "from", spec = GreaterThanOrEqual.class),
@Spec(path = "createdAt", params = "to", spec = LessThanOrEqual.class)
}) Specification<MyEntity> specification,
Pageable pageable
) {
return service.getAllBySpecification(specification, pageable);
}
Update your service:
public Page<ResponseDto> getAllBySpecification(final Specification<MyEntity> specification, final Pageable pageable) {
return repo.findAll(specification, pageable).map(mapper::toResponseDto);
}
And then request your data:
GET http://localhost:8080/specification?name=john&from=2019-04-10&to=2019-04-19
4) Building a Specification manualy:
Create a filter class:
@Data
public class MyFilter implements Specification<MyEntity> {
private String name;
@DateTimeFormat(iso = ISO.DATE) private LocalDate from;
@DateTimeFormat(iso = ISO.DATE) private LocalDate to;
@Override
public Predicate toPredicate(Root<MyEntity> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
if (name != null) predicates.add(builder.like(builder.upper(root.get("name")), "%" + name.toUpperCase() + "%"));
if (from != null) predicates.add(builder.greaterThanOrEqualTo(root.get("createdAt"), from));
if (to != null) predicates.add(builder.lessThanOrEqualTo(root.get("createdAt"), to));
return builder.and(predicates.toArray(new Predicate[0]));
}
}
Create a controller method:
@GetMapping("/filter")
public Page<ResponseDto> getAllByMyFilter(MyFilter filter, Pageable pageable) {
return service.getAllBySpecification(filter, pageable);
}
Then run the request:
GET http://localhost:8080/filter?name=john&from=2019-04-10&to=2019-04-19