Spring JPA Specification with Sort - java

I am using Spring JPA.
To be more precise I am using a Repository which extends JpaRepository and JpaSpecificationExecutor because I require pagination, filtering and sorting.
Now I have the pagination and filtering all working just fine, but I cannot get sorting to work as well.
I notice with some disappointment that JpaSpecificationExecutor has findAll() methods:
findAll(Specification, Pageable);
findAll(Specification, Sort);
But the one I need:
findAll(Specification, Pageable, Sort); //or something like this
does not exist!
So, Plan B, include Sorting in the Specification.
With the help of the Accepted Answer to this question: JpaSpecificationExecutor JOIN + ORDER BY in Specification I put together the following:
private Specification<MainEntity> matches() {
return new Specification<MainEntity>() {
public Predicate toPredicate(Root<MainEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
//Attempt to sort by Surname, has no effect
query.orderBy(cb.asc(root.get("surname")));
//add string filters
for (String field : stringFilterMap.keySet()) {
String valuePattern = stringFilterMap.get(field);
predicates.add(cb.like(root.get(field), "%"+valuePattern+"%"));
}
//...snip...
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
}
Where springFilterMap is an instance field, Map<String,String> whose keys are field names and values are filters values.
Above you will see my attempt to order by Surname, but this seems to have no effect.
What am I doing wrong; & how can I achieve Sorting along with Pagination and Filtering?

Use PageRequest, which is an implementation of Pageable, and implements paging and sorting at once, like you want it. For example through this constructor:
public PageRequest(int page, int size, Sort sort)
UPDATE:
Since Spring Data JPA 2.0 the above constructor is deprecated and you should use the static factory method of:
public static PageRequest of(int page, int size, Sort sort)

Related

What is the best practice for solving the number of cases of N searches in Spring JPA?

What is the best practice for solving the number of cases of N searches?
There are 3 search conditions for me.
There are search conditions of A, B, and C.
In this case, the number of possible cases is
Search only A, search only B, search only C,
Search only AB, search only AC, search only BC
Search only ABC
In the above situation, there are a total of 6 cases like this.(3!)
To map the number of all cases without thinking
#GetMapping("/A/{A}/B/{B}/C/{C}")
public ReturnType MethodName(#PathVariable AClass A
#PathVariable BClass B,
#PathVariable CClass C) {
return service.findByAandBandC(A, B, C);
...
I have to create 6 controller methods like the one above.
With 4 search conditions, need 24 methods (4! = 4 * 3 * 2)
If there are 5 search conditions, need 120 methods (5! = 5 * 4 * 3 * 2)
As above, it grows exponentially.
Instead of making all cases a method, I wonder if there is a best practice.
If possible, any answer utilizing spring data jpa would be appreciated.
best regards!
Instead of creating different controllers for each search option, try to make use of one controller. Suppose you have an endpoint to get the list of all users.
#GetMapping
public List<User> listAll() {
return service.listAll();
}
Accepting query parameters in a map is a good solution. Change the method signature and pass the query parameter map to the service:
#GetMapping
public List<User> listAll(#RequestParam Map<String, String> queryParams) {
return service.listAll(queryParams);
}
This way you can accept dynamic number of search options. In your service class use Spring JPA Specification. To execute specification your repository interface need to extend JpaSpecificationExecutor<T> interface.
public List<User> listAll(Map<String, String> queryParams) {
return repository.findAll(createSpec(queryParams));
}
private Specification<User> createSpec(Map<String, String> queryParams) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
String value = queryParams.get("email");
if(StringUtils.isNotBlank(value)) {
Predicate email = criteriaBuilder.like(root.get("email"), "%" + value + "%");
predicates.add(name);
}
value = queryParams.get("name");
if(StringUtils.isNotBlank(value)) {
Predicate name = criteriaBuilder.like(root.get("name"), "%" + value + "%");
predicates.add(name);
}
return criteriaBuilder.and(predicates.toArray(Predicate[]::new));
};
}
When there is a requirement to add more search options, you can add them inside createSpec() method without breaking existing API contract or adding more methods.

In back end (java spring boot) sorting, how to sort with Alias name using pageable? Without parent table as prefix

I am using backend pagination and sorting with java spring boot pageable. While passing sort field as usercount (This gives count of user_role_mapping), Java triggering an error column f.usercount does not exist .
Actually usercount not a column it's an Alias name.
How to sort using usercount as Alies name without f. as prefix?
API URL:
http://localhost:8080/facility/list?pageNumber=0&pageSize=10&sortBy=usercount&sortType=asc
Default sortBy & sortType are id and desc respectively in controller layer.
Java Code give below:
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(sortBy).descending());
if (sortType.equalsIgnoreCase("asc")) {
pageable = PageRequest.of(pageNumber, pageSize, Sort.by(sortBy).ascending());
}
Page<FacilityProjection> facilityList = facilityRepository.facilityListing(12,pageable);
Postgres sql Hibernate query for listing facility details along with user count based on role id, given below:
#Query(nativeQuery = true, value = " Select f.name as facilityname,f.id as facilityid,count(urm.id) as usercount
from facility f
join user u on f.user_id=u.id
join user_role_mapping urm on u.id = urm.user_id
where urm.role_id=:roleId ")
Page<FacilityProjection> facilityListing(#Param("roleId") Long roleId,Pageable pageable);
The problem is that usercount is aggregation function result. To order by this field query have to contain order by count(urm.id) instead of order by usercount.
In this case I'd suggest you to resort page content using Collections::sort
boolean sortByUserCount = sortBy.equalsIgnoreCase("usercount");
boolean desc = sortType.equalsIgnoreCase("desc");
final Pageable pageable;
if (sortByUserCount) {
pageable = PageRequest.of(pageNumber, pageSize);
} else {
if (desc) {
pageable = PageRequest.of(pageNumber, pageSize, Sort.by(sortBy).descending());
} else {
pageable = PageRequest.of(pageNumber, pageSize, Sort.by(sortBy).ascending());
}
}
Page<FacilityProjection> facilityList = facilityRepository.facilityListing(12, pageable);
if (sortByUserCount) {
Comparator<FacilityProjection> comparator = Comparator.comparing(FacilityProjection::getUserCount);
if(desc) {
comparator = comparator.reversed();
}
Collections.sort(facilityList.getContent(), comparator);
}
As far as I know, this is not possible.
Having said that, I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Facility.class)
public interface FacilityProjection {
#IdMapping
Long getId();
String getName();
#Mapping("SIZE(users)")
Long getUsercount();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
FacilityProjection a = entityViewManager.find(entityManager, FacilityProjection.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features so you can see this as a replacement for Spring Data Projections that supports more use cases!

how to create generic (type and attribute) Spring Jpa Specifications

Currently i am using spring data JPA with criteria API. I created a custom repository for an entity. The predicates are created this way 2 examples:
public static <E> Predicate matchingCountryOrGroup(CountryCode country, List<String> euGroups,
SingularAttribute<E, String> countryAttribute,
SetAttribute<E, String> countryGroupAttribute, Root<E> deliveryMode,
CriteriaBuilder cb) {
String countryGroup = country.getCountryGroup();
Predicate countryGroupMatchPredicate = matchCountryGroup(countryGroup, euGroups, countryGroupAttribute, cb, deliveryMode);
Predicate countryMatchPredicate = caseInsensitiveStringMatch(country.getCountryId(), countryAttribute, cb,
deliveryMode);
Predicate countryPredicate = cb.or(countryGroupMatchPredicate, countryMatchPredicate);
Predicate bothNull = cb.and(cb.isNull(deliveryMode.get(countryAttribute)),
cb.isEmpty(deliveryMode.get(countryGroupAttribute)));
return cb.or(countryPredicate, bothNull);
}
public static <E> Predicate caseInsensitiveStringMatch(String match,
SingularAttribute<E, String> stringAttribute, CriteriaBuilder cb,
Root<E> deliveryMode) {
return cb.equal(cb.lower(deliveryMode.get(stringAttribute)), match.toLowerCase());
}
I use these predicates in multiple queries for different attributes, thus the parameter for the attribute and different entities.
What is really annoying, is the fact that i always have to put in CriteriaBuilder and Root.
On the other hand i saw the Spring Specification API that seems to solve exactly this problem, by injecting the correct root and a CriteriaBuilder.
The question is: how can i leverage/use this Specification API with my kind of predicates - generic for different Entities and generic for different attributes?
Updated question and code to add clarity

How to sort by a composed field with CrudRepository?

I have a simple CrudRepository that accepts a sortable:
class PersonReposiroty extends CrudRepository<Person, Long> {
findAll(Pageable page);
}
I want to sort it by the sum of two fields:
dao.findAll(PageRequest.of(page, size, Sort.by("price1, price2"));
Problem: this sorts first by price1, then by price2. How can I sum it before?
I found out that it is actually possible to order by sum, as follows with querydsl:
private PageRequest pricePageSort() {
OrderSpecifier<BigDecimal> sort = QPerson.person.price1.add(QPerson.person.price2).asc(); //or .desc()
return new QPageRequest(pageNumber, pageSize, sort);
}
Then use it by:
dao.findAll(predicate, pricePageSort());
Maybe the CrudRepository has to extends QuerydslPredicateExecutor<Person> therefore, but I'm using it anyways.

JPA Hibernate Order by alphanumeric field

I am using Jpa/Hibernate to access MariaDB in a Spring Boot application
I am strugling to sort data by an alphanumeric field containng numbers that might end with one letter (pattern \d+[a-z]?)
e.g.
10
104
20a
100b
and I need them ordered like this
10
20a
100b
104
I a bulding my own query with the Criteria Api because I also have to do some complex filtering.
#Transactional(readOnly = true)
public class EntryRepositoryImpl implements EntryRepositoryCustom {
#PersistenceContext
private EntityManager entityManager;
#Override
public Page<Entry> get(MultiValueMap<String, String> parameters, Pageable pageable) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// count total number of filterd entries
Long totalResultCount = getResultCount(parameters, criteriaBuilder);
// build query to get filterd entries
CriteriaQuery<Entry> selectQuery = criteriaBuilder.createQuery(Entry.class);
Root<Entry> getRoot = selectQuery.from(Entry.class);
CriteriaQuery<Entry> select = selectQuery.select(getRoot);
addFilters(parameters, criteriaBuilder, getRoot, select);
// add sorting
List<javax.persistence.criteria.Order> sortOrders = JpaUtils.translateSorting(pageable,
getRoot);
select.orderBy(sortOrders);
// get one page of filterd entries
List<Entry> results = getPageResults(pageable, select);
return new PageImpl<>(results, pageable, totalResultCount);
}
/**
* Translate spring to jpa sorting.
*/
public static List<javax.persistence.criteria.Order> translateSorting(Pageable pageable,
Root<Entry> root) {
List<Sort.Order> orders = new ArrayList<>();
if (pageable.getSort() != null) {
pageable.getSort().iterator().forEachRemaining(orders::add);
}
return orders.stream().
map(order -> {
String[] parts = order.getProperty().split("\\.");
String field = parts[0];
Path path = parts.length == 2 ? root.join(field).get(parts[1]) : root.get(field);
return new OrderImpl(path, order.isAscending());
})
.collect(Collectors.toList());
}
I already have a custom comparator but it seems, there is no way to translate it so the DB could use it.
So far I found the following solutions/ideas
using #SortComparator, but it is not feasible for my use case because the ordering has to happen in the database, because there are over 500k complex rows.
this sql base solution but don't know how to translate it into the Criteria Api.
after looking at the function of CriteriaBuilder (javadoc) I got the idea to split the value into the numeric and string parts and apply to orders but there is not function to split with a regular expression.
Edit:
For now I did split the field into 2 and use two sort expression.

Categories