spring jpa query with pageable, sort and filter and return projection - spring

I am using Spring Data Rest with org.springframework.boot 1.5.2 with hibernate 5.2.9. What i am trying to achieve is a way to use JPA to query with sort, filter, pageable that can return a subset of the entity or return a projection.
Below is the code that uses:
(1) Specification for filtering
(2) Projection and Excerpts to apply projection in collection
(3) The controller that tries to return Page,
but it only works if the return type is Page.
where Student is the entity, StudentLite is the projection
Question is:
(1) How to have a query+sort+filter that returns Page projection
(2) Possible to apply the Excerpts to just that query?
(3) Any way to use #JsonView in #RepositoryRestController to solve?
StudentRepository class
#RepositoryRestResource(excerptProjection = StudentLite.class)
public interface StudentRepository extends PagingAndSortingRepository<Student,Long>,
JpaSpecificationExecutor<Student> {}
and
StudentSpecification class
public class StudentSpecification {
public static Specification<Student> filteredStudentList(StudentSearch c) {
final StudentSearch criteria = c;
return new Specification<Student>() {
#Override
public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Student, Contact> joinContact = root.join(Student_.contact);
Path<Contact> contact = root.get(Student_.contact);
Path<String> officialId = root.get(Student_.officialId);
Path<String> name = root.get(Student_.name);
Path<String> email = contact.get(Contact_.email);
Path<String> phoneMobile = contact.get(Contact_.phoneMobile);
final List<Predicate> predicates = new ArrayList<Predicate>();
if(criteria.getOfficialId()!=null) {
predicates.add(cb.like(officialId, "%" + criteria.getOfficialId() + "%"));
System.out.println("==not null...criteria.getOfficialId()="+criteria.getOfficialId()+" :officialId="+officialId.toString());
}
if(criteria.getName()!=null) {
predicates.add(cb.like(name, "%"+criteria.getName()+"%"));
}
if(criteria.getEmail()!=null) {
predicates.add(cb.like(email, "%"+criteria.getEmail()+"%"));
}
if(criteria.getPhoneMobile()!=null) {
predicates.add(cb.like(phoneMobile, "%"+criteria.getPhoneMobile()+"%"));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
}
}
and the controller where the class is annotated with #ExposesResourceFor(Student.class) and #RepositoryRestController :
#RequestMapping(method=RequestMethod.GET)
public #ResponseBody Page<StudentLite> getStudentList(Pageable pageable, #RequestParam Map<String,String> criteria) {
StudentSearch ss = new StudentSearch(criteria);
// Below statement fail, as findAll(...) is suppose to return Page<Student>
Page<StudentLite> pagedStudentLite = studentRep.findAll( StudentSpecification.filteredStudentList(ss), pageable);
return pagedStudentLite;
}

Related

JpaSpecificationExecutor#findAll() doesn't anymore apply filter after updating Spring Boot 2.7.4 to 3.0.2

I have a controller that passes a filter with parameters for a search, I call my repository that extends the JpaSpecificationExecutor interface but when calling the findAll method passing the filter parameters it returns all records without filtering. Below are the codes for my controller, repository, specs class, and model class.
Controller
#GetMapping("/{codigoPedido}")
public PedidoModel buscar(#PathVariable String codigoPedido) {
Pedido pedido = emissaoPedido.buscarOuFalhar(codigoPedido);
return pedidoModelAssembler.toModel(pedido);
}
Repository
#Repository
public interface PedidoRepository extends CustomJpaRepository<Pedido, Long>,
JpaSpecificationExecutor<Pedido> {
Optional<Pedido> findByCodigo(String codigo);
#Query("from Pedido p join fetch p.cliente join fetch p.restaurante r join fetch r.cozinha")
List<Pedido> findAll(Specification<Pedido> spec);
}
Spec
public class PedidoSpecs {
public Specification<Pedido> usandoFiltro(PedidoFilter filtro) {
return (root, query, builder) -> {
root.fetch("restaurante").fetch("cozinha");
root.fetch("cliente");
var predicates = new ArrayList<Predicate>();
if (filtro.getClienteId() != null) {
predicates.add(builder.equal(root.get("cliente"), filtro.getClienteId()));
}
if (filtro.getRestauranteId() != null) {
predicates.add(builder.equal(root.get("restaurante"), filtro.getRestauranteId()));
}
if (filtro.getDataCriacaoInicio() != null) {
predicates.add(builder.greaterThanOrEqualTo(root.get("dataCriacao"),
filtro.getDataCriacaoInicio()));
}
if (filtro.getDataCriacaoFim() != null) {
predicates.add(builder.lessThanOrEqualTo(root.get("dataCriacao"),
filtro.getDataCriacaoFim()));
}
return builder.and(predicates.toArray(new Predicate[0]));
};
}
}
Filter
#Setter
#Getter
public class PedidoFilter {
private Long clienteId;
private Long restauranteId;
#DateTimeFormat(iso = ISO.DATE_TIME)
private OffsetDateTime dataCriacaoInicio;
#DateTimeFormat(iso = ISO.DATE_TIME)
private OffsetDateTime dataCriacaoFim;
}
Request
GET- /pedidos?clienteId=1&restauranteId=1
Answer
all records without filter.
with this same implementation, it worked on Spring Boot 2.7.4 and Javax.
where can i make it work?
Cody repository:
https://github.com/raderleao/fastfood-api.git

Add WHERE IN clause to JPA Specification

I'm trying to implement search functionality limited by IN clause:
I want to implement search implementation with filter limitation:
#GetMapping("find")
public Page<MerchantUserDTO> getAllBySpecification(
#And({
#Spec(path = "name", spec = LikeIgnoreCase.class),
#Spec(path = "login", spec = LikeIgnoreCase.class),
#Spec(path = "email", spec = LikeIgnoreCase.class),
}) Specification<Users> specification,
#SortDefault(sort = "login", direction = Sort.Direction.DESC) Pageable pageable
) {
return merchantUserService.getAllBySpecification(specification, pageable)
.map(g -> MerchantUserDTO.builder()
.id(g.getId())
.login(g.getLogin())
.build()
);
}
#Override
public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
return dao.findAllByTypeIn(specification, pageable, "MerchantUser");
}
Repository:
#Repository
public interface MerchantUserRepository extends JpaRepository<Users, Integer>, JpaSpecificationExecutor<Users> {
Page<Users> findAllByTypeIn(Pageable page, String... types);
Page<Users> findAllByTypeIn(Specification<Users> specification, Pageable pageable, String... types);
}
What is the proper way to extend the specification with IN clause?
specification.and(path.in(types)) path is a attribute but how to implement it properly?
Generally this can be achieved this way:
1) Create specification implementation
public class MerchantUserSpecification implements Specification<Users> {
private final List<String> types;
public MerchantUserSpecification(List<String> types) {
this.types = types;
}
#Override
public Predicate toPredicate(Root<Users> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
if (types != null && !types.isEmpty()) {
return root.get(Users_.type).in(types);
} else {
// always-true predicate, means that no filtering would be applied
return cb.and();
}
}
2) Use method Page findAll(#Nullable Specification spec, Pageable pageable); inherited from JpaSpecificationExecutor interface instead of using your custom findAllByTypeIn(Specification<Users> specification....)
#Override
public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
// combine original specification (passed from outside) and filter-by-types specification
Specification<Users> finalSpec = specification
.and(new MerchantUserSpecification(Arrays.asList("MerchantUser")))
return dao.findAll(finalSpec, pageable)
}
P.S.
With Java 8+ and for simple cases (like yours) the code may be reduced even more. Instead of implementing Specification<T> in separate class you can just create a method
private Specification<Users> typeIn(List<String> types) {
return (root, query, cb) -> {
if (types != null && !types.isEmpty()) {
return root.get(Users_.type).in(types);
} else {
// always-true predicate, means that no filtering would be applied
return cb.and();
}
}
}
#Override
public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
// combine original specification (passed from outside) and filter-by-types specification
Specification<Users> finalSpec = specification
.and(typeIn(Arrays.asList("MerchantUser")))
return dao.findAll(finalSpec, pageable)
}
UPDATE: Shortest way
#Override
public Page<Users> getAllBySpecification(Specification<Users> specification, Pageable pageable) {
// combine original specification (passed from outside) and filter-by-types specification
Specification<Users> finalSpec = specification
.and((root, query, cb) -> root.get(Users_.type).in(Arrays.asList("MerchantUser"))
return dao.findAll(finalSpec, pageable)
}

How to write a generic java function in Spring-boot for querying results,based on multiple query filters

I was working on java using jdo where I used to write query functions like below, which queries from an Entity based on what parameters are passed to the function.
Now Im moving to spring-boot, and want to know if I can achieve the same using spring-boot.Any help or suggestions would be heartfully appreciated.Thank you!!
public List<Result> getQueryResult(int filter1, String filter2,Float filter3,Long id){
Query query = new Query("select from Entity1");
String filter = "id == "+id;
if(filter1 != null){
filter = filter+" && filter1 == "+filter1+";
}
if(filter2 != null){
filter = filter+" && filter2 == '"+filter2+"'";
}
if(filter3 != null){
filter = filter+"filter3 == "+filter3;
}
query.setFIlter(filter);
List<Result> results = query.excute();
return results;
}
You have two options - you can use JPA Criteria Builder or JPA Specifications
class Person {
String firstName;
String lastName;
int age;
}
JPA Criteria Builder
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
Predicate sellAlcohol = builder.ge(root.get(Person_.age), 21);
Predicate toMindy = builder.equal(root.get(Person_.firstName), "Mindy");
Usage
query.where(builder.and(sellAlcohol, toMindy));
em.createQuery(query.select(root)).getResultList();
Specificatons
public PersonSpecifications {
public static Specification<Person> sellAlcohol() {
return new Specification<Person> {
public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
return cb.ge(root.get(Person_.age), 21);
}
};
}
public static Specification<Person> toMindy() {
return new Specification<Person> {
public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
return cb.equal(root.get(Person_.firstName), "Mindy");
}
};
}
}
Usage
personRepository.findAll(where(sellAlcohol()).and(toMindy()));

Passing array of reg expressions to spring based mongo #Query

I'm using Spring boot with mongodb. I've extended PagingAndSortingRepository repository and added the following function
#Query("{'title':{ $nin: [?0]}}")
List<Item> findItem(String[] exclude);
I want to be able to pass it an array of regular expressions such as /dog/,/cat/,/horse/ to exclude any item that may have one of these in it's title.
The above function does not work because the exclude is converted to a string. How can I pass an array of regular expressions to be able to do the above?
You can work it out by using a Querydsl predicate in one of your controller method.
Add something like this to your controller:
#RequestMapping(value="/search/findByNameRegexNotIn", method = RequestMethod.GET)
#ResponseBody
public List<Item> findByNameRegexNotIn(#RequestParam(name = "name") List<String> names) {
// build a query predicate
BooleanBuilder predicate = new BooleanBuilder(); // comes from the Querydsl library
for (String name : names) {
predicate.and(QItem.item.name.contains(name).not()); // the QItem class is generated by Querydsl
}
List<Item> items = (List<Item>)repository.findAll(predicate);
return items;
}
You can of course add a Pageable parameter and return a Page<Item> instead of a List.
Edit: another solution if you use Querydsl for this sole purpose is to override the default bindings of your query parameter.
public interface ItemRepository extends CrudRepository<Item, String>,
QueryDslPredicateExecutor<Item>, QuerydslBinderCustomizer<QItem> {
#Override
default public void customize(QuerydslBindings bindings, QItem item) {
bindings.bind(item.name).all(
(path, values) -> path.matches(StringUtils.collectionToDelimitedString(values, "|")).not());
// disable query on all parameters but the item name
bindings.including(item.name);
bindings.excludeUnlistedProperties(true);
}
}
The controller method:
#RequestMapping(value="/search/query", method = RequestMethod.GET)
#ResponseBody
public List<Item> queryItems(
#QuerydslPredicate(root = Item.class) Predicate predicate) {
List<Item> items = (List<Item>)repository.findAll(predicate);
return items;
}
Edit: if you don't wan't to override the default QuerydslBinderCustomizer#customize, you can also implement your own binder and specify it in the controller method.
public interface ItemRepository extends CrudRepository<Item, String>,
QueryDslPredicateExecutor<Item> {
...
}
The controller method:
#RequestMapping(value="/search/query", method = RequestMethod.GET)
#ResponseBody
public List<Item> queryItems(
#QuerydslPredicate(root = Item.class, bindings = ItemBinder.class) Predicate predicate) {
List<Item> items = (List<Item>)repository.findAll(predicate);
return items;
}
The binder class:
class ItemBinder implements QuerydslBinderCustomizer<QItem> {
#Override
public void customize(QuerydslBindings bindings, QItem item) {
bindings.bind(item.name).all(
(path, values) -> path.matches(StringUtils.collectionToDelimitedString(values, "|")).not()
);
bindings.including(item.name);
bindings.excludeUnlistedProperties(true);
}
}
Edit: for the sake of exhaustivity and those who don't want to hear about Querysl. Using the solution proposed in Spring Data Mongodb Reference.
Define a custom repository interface:
interface ItemRepositoryCustom {
public Page<Item> findByNameRegexIn(Collection<String> names, Pageable page);
}
Define an custom repository implementation (Impl postfix required!):
public class ItemRepositoryImpl implements ItemRepositoryCustom {
#Autowired
private MongoOperations operations;
#Override
public Page<Item> findByNameRegexNotIn(Collection<String> names, Pageable pageable) {
String pattern = StringUtils.collectionToDelimitedString(names, "|");
// this time we use org.springframework.data.mongodb.core.query.Query instead of Querydsl predicates
Query query = Query.query(where("name").regex(pattern).not()).with(pageable);
List<Item> items = operations.find(query, Item.class);
Page<Item> page = new PageImpl<>(items, pageable, items.size());
return page;
}
}
Now simply extend ItemRepositoryCustom:
public interface ItemRepository extends MongoRepository<Item, String>, ItemRepositoryCustom {
...
}
And you're done!
You can pass a java.util.regex.Pattern[] to the method. This will be converted to regex array under the hood:
#Query("{'title':{ $nin: ?0}}")
List<Item> findItem(Pattern[] exclude);

Spring Specification Criteria Multiple Joins ? How?

I got stuck using a Spring Project with Spring Data + specification + criteria api.
I will try to simulate the situation with general entities we used write to get easy example.
The Entities:
Consider all attributes of the each entity is passed on the constructor showed below
Country(Long id, String name, String iso)
State(Long id, String name, String iso)
City(Long id, String name, String iso)
This is my repository:
public interface CityRepository extends PagingAndSortingRepository<City, Integer>, JpaSpecificationExecutor<City> {
}
As you can see, I don't need to implement anything on the repository
This is my service
#Service
#Transactional
public class CityService {
#Autowired
private CityRepository cityRepository;
#Transactional(readOnly = true)
public CityListVO findByNameLike(String name, PageRequest pageRequest) {
name = "%" + name + "%";
if (pageRequest == null) {
List<City> result = cityRepository.findAll(fillGridCriteria(name));
return new CityListVO(1, result.size(), result);
} else {
Page<City> result = cityRepository. findAll(fillGridCriteria(name), pageRequest);
return new CityListVO(result.getTotalPages(), result.getTotalElements(), result.getContent());
}
}
private static Specification<City> fillGridCriteria(String name) {
return new Specification<City>() {
#Override
public Predicate toPredicate(
Root<City> root,
CriteriaQuery<?> query,
CriteriaBuilder builder) {
/*
The current return I can do a like by name, and it works fine.
My problem is if for any reason I need to do multiple joins like the folow jpql:
select ci FROM City ci, State st, Country co where ci.st = st AND st.co = co AND co.name = 'Canada';
How to do this from here ? Inside this method.
How is gonna be the return for this method ?
*/
return builder.like(root.get("name"), name.trim());
}
};
}
}
Let's assume you want all the cities that their country's name like name and you have a relational Model in which :
Country(Long id, String name, String iso)
State(Long id,Long country, String name, String iso)
City(Long id, Long state, String name, String iso)
Predicate:
private static Specification<City> fillGridCriteria(String name) {
return new Specification<City>() {
#Override
public Predicate toPredicate(
Root<City> root,
CriteriaQuery<?> query,
CriteriaBuilder builder) {
return
builder.like(root.get("state").get("country").get("name"), name.trim());
}
};
}

Resources