CriteriaBuilder - Sum using SelectCase - criteria-api

I am trying to perform a summation SQL query like the following:
select group_ID, sum(case when user_type = 'Exec' then 1000
when user_type = 'Office' then 10 else 0 end)
from subscription
group by group_ID;
using the following snippet from a hiberate CriteriaBuilder query:
criteriaBuilder.sum(
criteriaBuilder.selectCase()
.when(criteriaBuilder.equal(subscriptionJoin.get(Subscription_.userType), "Exec"),1000)
.when(criteriaBuilder.equal(subscriptionJoin.get(Subscription_.userType), "Office"),1)
.otherwise(101))
However the following compile error appears:
Inferred type 'java.lang.object' for type parameter 'N' is not within its bound; should extend 'java.lang.number'
Any idea how to support performing a summation using the selectCase?

Sum is defined as follows:
<N extends Number> Expression<N> sum(Expression<N> x);
So reason to the compilation error is that sum method expect such arguments which is Expression with type that extends Number. It determines type from the selectCase and ends up with java.lang.Object, which is not acceptable.
Problem can be solved by giving type parameter (<Number>):
criteriaBuilder.sum(
criteriaBuilder.<Number>selectCase()

We are using Spring Data JPA in our project and i have the same case where i need to do sum. Instead of criteria query i'm just following the "named parameters" approach because this approach seems easy.
My method which gives me sum is as follows.
public interface ITransactionEntryRepo extends PagingAndSortingRepository<TransactionEntryEntity, String> {
#Query("select SUM(CASE WHEN te.debit = 'Y' THEN (te.amount * - 1) WHEN te.debit = 'N' THEN te.amount ELSE 0 END) AS availablebalance FROM TransactionEntity t, TransactionEntryEntity te WHERE t.id = te.transactionEntity.id and te.accountEntity.id = :id and te.valid = 'T' and t.retcode = 'XX' GROUP BY te.accountEntity.id")
public double findAvailableBalance(#Param("id") String id);
}
And I call this method in the class where i need
double balance = iTransactionEntryRepo.findAvailableBalance(accountEntity.getId());
and pass it(balance) wherever I need to. Hope this helps someone.

For aggregate operation you should pass the CriteriaQuery with numeric type to be proper expression for criteria builder, however this may not affect your criteria base restriction of you entity type. Finally you can append the desired predicates to your criteria query for having criteria base aggregation.
public class Aggregate<T, S extends Number> {
public Aggregate(Class<T> tableType, Class<S> type) {
this.criteriaBuilder = entityManager.getCriteriaBuilder();
this.criteria = criteriaBuilder.createQuery(type);
this.root = criteria.from(tableType);
}
public Aggregate<T, S> aggregate(String field) {
criteria.select(criteriaBuilder.sum(root.get(field)));
return this;
}
public <I> Aggregate<T, S> restrict(String field, I i) {
criteria.where(criteriaBuilder.equal(root.get(field), i));
return this;
}
public S perform() {
return entityManager.createQuery(criteria).getSingleResult();
}
private Root<T> root;
private final CriteriaQuery<S> criteria;
private final CriteriaBuilder criteriaBuilder;
}

Related

Spring Data JPA - #Query with null values based on user input

The user has a form:
checkbox with fields status and priority
submit btn
Objective: Query DB based on these values.
If one of them is null or false, the criteria should be ignored.
Ex: status is checked and priority is not, I want to query only based on status.
The code below will never execute with one of them being false. I also read about Query by Example but could not find a solution.
Null values can indeed be ignored, but need to be previously defined.
I thought about replacing null with something similar to *.
Ex: WHERE c.status = *. It didn't work.
#Query(value = "SELECT * FROM tickets c WHERE c.status = :status AND c.priority= :priority",
nativeQuery = true)
List<Ticket> findByFilter(#Param("status") String status,
#Param("priority") String priority);
Do you have any idea how can I do this?
Thank you
Thank you Simon for pointing me in the right direction.
This was the web page that helped me:
https://dimitr.im/writing-dynamic-queries-with-spring-data-jpa
My situation:
Created a specification class:
public final class DbFilterSpecification {
public static Specification<Ticket> statusContains(String expression) {
return (root, query, builder) -> builder.like(root.get("status"), contains(expression));
}
public static Specification<Ticket> priorityContains(String expression) {
return (root, query, builder) -> builder.like(root.get("priority"), contains(expression));
}
private static String contains(String expression) {
return MessageFormat.format("%{0}%", expression);
}
}
Created a method inside the service layer:
public List<Ticket> findAllWithSpecification(String status, String priority) {
Specification<Ticket> specification = Specification
.where(status == null ? null : DbFilterSpecification.statusContains(status))
.and(priority == null ? null : DbFilterSpecification.priorityContains(priority));
return ticketRepository.findAll(specification);
}

if/else doese not work in my POST Method of SpringBoot rest api

I use Spring Boot rest api with MongoDB.
In the POST Method, if there is not scoreID and there is not a player with specific date in my collection, because at the same time a player cannot play different games and bring score, then if the specific player and gamecode exist, create a score.
In fact, in the POST Method I used Nested IF-ELSE conditions.
But, in the Postman when I execute POST Request with this data:
{
"scoreid":"s11",
"score":1000,
"player":"sahari",
"gamecode":"g12",
"date":"2020-01-01"
}
always, I recieve an error, in the Postman, 400 Bad Request!, which i defined in the last line of my IF-ELSE statements.
I do not know, what is my mistake and why my program doese not execute IF conditions correct.
The POST Method:
//Create Score
#PostMapping
public ResponseEntity<?> createScore(#RequestBody #JsonView(Views.class) #Valid Score score) {
String p = srepo.findByPlayerName(score.getPlayer());
String g = srepo.findByGameCode(score.getGamecode());
String scoreid = srepo.findByScoreid(score.getScoreid());
Query query = new Query();
query.addCriteria(new Criteria().andOperator(Criteria.where("player").is(score.getPlayer()),
Criteria.where("date").is(score.getDate())));
if((scoreid != null)) {
return ResponseEntity.status(409).body("Conflict!"); }
else
if(mongoTemplate.exists(query, Score.class))
return ResponseEntity.status(409).body("There is not Possible at same time one player brings different Scores!");
else
if((p!= null)&&(g!= null))
{
history = new ArrayList<History>();
h = new History();
h.setScore(score.getScore());
h.setDate(score.getDate());
history.add(h);
hrepo.save(h);
score.setHistory(history);
srepo.insert(score);
return ResponseEntity.ok(score);
}
else
{
return ResponseEntity.status(400).body("Bad Request!");
}
}
The Score Repository:
#Repository
public interface ScoreRepository extends MongoRepository<Score, String>{
#Query("{'scoreid':?0}")
public String findByScoreid(String scoreid);
#Query("{'Player.nickname':?0}")
public String findByPlayerName(String player);
#Query("{'Games.code':?0}")
public String findByGameCode(String game);
}
The problem is not for my IF-ELSE statements.The problem is in the Score Repository
I must return a List instead of String for findByPlayerName and findByGameCode and for findByScoreid which is for checking duplicate in the POST Method I must return type of Score

JPA Criteria api - Total records for concrete query within pagination

I am programming function for pagination in my repository layer. Function receive as parameters spring's pageable object and some value like this:
public Page<Foo> filterFoo(Pageable pageable, String value) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Foo> fooQuery = cb.createQuery(Foo.class);
Root<Foo> foo = fooQuery .from(Foo.class);
fooQuery .where(adding predicate for match value);
List<Foo> result = entityManager.createQuery(fooQuery )
.setFirstResult((pageable.getPageNumber() - 1) * pageable.getPageSize())
.setMaxResults(pageable.getPageSize())
.getResultList();
return new PageImpl<>(result, pageable, xxxx);
}
Function return spring's PageImpl object filled with my result. To PageImpl I also need set total count of objects which suit predicates. This count number have to be of course without maxResult and firstResult. Is possible create another database call with my fooQuery to get total database records for that query without limit? What is the best practise to use pageable and criteria api in JPA? Thank you in advice.
Because generated SQL uses aliases - you may need make separate query for get total count of rows.
For example:
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
countQuery.select(cb.count(countQuery.from(Foo.class)));
if (Objects.nonNull(filters)) {
countQuery.where(filters);
}
return new PageImpl<>(result, pageable, em.createQuery(countQuery).getSingleResult());
where filters is equal to your adding predicate for match value expression.
Also, you may use a TupleQuery with custom SQL function for calculate count of rows in one select query.
Like this:
public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor {
#Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"count_over",
new SQLFunctionTemplate(
StandardBasicTypes.LONG,
"(count(?1) over())"
)
);
}
}
and Criteria:
public Page<Foo> findAll(Specification<Foo> specification, Pageable pageable) {
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Foo.class> fooRoot = cq.from(Foo.class);
cq.select(cb.tuple(fooRoot, cb.function("count_over", Long.class, fooRoot.get("id"))));
Predicate filters = specification.toPredicate(fooRoot, cq, cb);
if (Objects.nonNull(filters)) {
cq.where(filters);
}
TypedQuery<Tuple> query = em.createQuery(cq);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
List<Tuple> result = query.getResultList();
if (result.isEmpty()) {
return new PageImpl<>(List.of());
}
return new PageImpl<>(
result.stream().map(tuple -> (Foo) tuple.get(0)).collect(toUnmodifiableList()),
pageable,
(long) result.get(0).get(1)
);
}
See more about SQLFunction: https://vladmihalcea.com/hibernate-sql-function-jpql-criteria-api-query/ and Custom SQL for Order in JPA Criteria API

Cannot use MatchMode with variable of type double

I am implementing server-side filtering of dataTable. In the service which implements the filtering I want to test if the search criteria is a number ; if so then I implement the filtering based on the bean attribute of type double ( salary ) , otherwise I make the filtering based on the bean attribute of type String ( username ) :
#Override
#Transactional
public List<User> list(int start, int length, String search) {
Criteria criteres = sessionFactory.getCurrentSession().createCriteria(User.class);
if (!search.equals("")) {
if (NumberUtils.isNumber(search))
criteres.add(Restrictions.like("salary", Double.parseDouble(search)));
else
criteres.add(Restrictions.like("username", search, MatchMode.ANYWHERE));
}
criteres.setFirstResult(start);
criteres.setMaxResults(length);
criteres.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
#SuppressWarnings("unchecked")
List<User> listUser = (List<User>) criteres.list();
return listUser;
}
The problem is that if I write :
if (!search.equals("")) {
if (NumberUtils.isNumber(search))
criteres.add(Restrictions.like("salary", String.valueOf(Double.parseDouble(search)), MatchMode.ANYWHERE));
else
criteres.add(Restrictions.like("username", search, MatchMode.ANYWHERE));
}
then I got alert error : DataTables warning: table id=t_list - Ajax error. For more information about this error, please see http://datatables.net/tn/7
So how to make possible to make a "like" condition on the double variable ?

How to execute a JPAQuery with pagination using Spring Data and QueryDSL

I have this request working good with queryDSL :
Iterable<AO> query_result = new JPAQuery(entityManager).from(ao)
.leftJoin( ao.lots , lot )
.leftJoin( ao.acs , ac )
.where(where).distinct()
.list(ao);
But what is its equivalent if we use it with spring data jpa
ao_respository.findAll(Predicate arg0, Pageable arg1);
Because i want to return a Page and just with querydsl it doesn't implement Page without spring data jpa.
I try to put my where in Predicate arg0 but i got this exception
Undeclared path 'lot '. Add this path as a source to the query to be able to reference it
where lot is declared as QLot lot = QLot.lot;
I created my own Page class and executed the query like this:
JPAQuery query = new JPAQuery(entityManager).from(ao)
.leftJoin( .. ).fetch()
.leftJoin( .. ).fetch()
...
.where(where)
MaPage<AO> page = new MaPage<AO>();
page.number = pageNumber+1;
page.content = query.offset(pageNumber*pageSize).limit(pageSize).list(ao);
page.totalResult = query.count();
My Page class:
public class MaPage<T> {
public List<T> content;
public int number;
public Long totalResult;
public Long totalPages;
...
}
It works but I got this warning
nov. 21, 2014 6:48:54 AM
org.hibernate.hql.internal.ast.QueryTranslatorImpl list WARN:
HHH000104: firstResult/maxResults specified with collection fetch;
applying in memory!
Returning a Page:
JPAQuery query =
...
.orderBy(getOrderSpecifiers(pageable, MyEntity.class))
.limit(pageable.getPageSize())
.offset(pageable.getOffset());
long total = query.fetchCount();
List<MyEntity> content = query.fetch();
return new PageImpl<>(content, pageable, total);
And I created this function to get OrderSpecifier:
private OrderSpecifier[] getOrderSpecifiers(#NotNull Pageable pageable, #NotNull Class klass) {
// orderVariable must match the variable of FROM
String className = klass.getSimpleName();
final String orderVariable = String.valueOf(Character.toLowerCase(className.charAt(0))).concat(className.substring(1));
return pageable.getSort().stream()
.map(order -> new OrderSpecifier(
Order.valueOf(order.getDirection().toString()),
new PathBuilder(klass, orderVariable).get(order.getProperty()))
)
.toArray(OrderSpecifier[]::new);
}
If you have a working, complex query in querydsl and you want to use springdata pagination, you have to:
make your querydsl/repository method return Page<T>
Page<YourEntity> yourSelect(Pageable aPageable)
use querydsl offset and limit to page your result set
List<YourEntity> theResultList = jpaQueryFactory
.select(<whatever complext jpaquery you like>)
.offset(aPageable.getOffset())
.limit(aPageable.getPageSize())
.fetch();
provide a LongSuplier counting all available results with respect to your query and use PageableExecutionUtils to return the result as Page
final long theCount = jpaQueryFactory
.selectFrom(<your select to count all results>)
.fetchCount();
return PageableExecutionUtils.getPage(theResultList, aPageable, () -> theCount);

Resources