JPA CriteriaSepcification IN clause - spring-boot

I'm using SpringBoot 2.2.6 with JPA and I need to do query with IN clause as mentioned in Title. I have try with:
#Override
public Predicate toPredicate(Root<Distinta> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
.....
.....
for (DistintaCriteria criteria : list) {
switch(criteria.getOperation()) {
case TEST:
Join<Entity, JoinEntity> join = root.join("joinEntity");
predicates.add(join.<Integer>get("id").in(criteria.getValue()));
}
}
where criteria.getValue() is a Integer[] array but it doesn't work. Can you help me?
Thank you all.
UPDATE
If I try the same Query with List<String> it works! With Integer I had this error:
Unaware how to convert value [[2, 3, 4, 5] : java.util.ArrayList] to requested type [java.lang.Integer]

I have solved as follows:
Join<Entity, JoinEntity> join = root.join("joinEntity");
Predicate in = join.get("id").in((List<Integer>)criteria.getValue());
predicates.add(in);
I don't know why with List<String> I don't need to cast.
Hope helps.

For in clause we need to pass a list always.
You need to convert your Integer array to Integer list using Java-8 like
List<Integer> values = Arrays.asList(criteria.getValue())
#Override
public Predicate toPredicate(Root<Distinta> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<>();
.....
.....
for (DistintaCriteria criteria : list) {
List<Integer> values = Arrays.asList(criteria.getValue());
switch(criteria.getOperation()) {
case TEST:
Join<Entity, JoinEntity> join = root.join("joinEntity");
predicates.add(join.<Integer>get("id").in(values));
}
}
In eclipse, we will get the warning like if we pass an array
Type Integer[] of the last argument to a method in(Object...) doesn't exactly match the vararg parameter type. Cast to Object[] to confirm the non-varargs invocation, or pass individual arguments of type Object for a varargs invocation.

Related

CriteraQuery in toPredicate method is not working

I wanted to fetch only select column from the DB using multiple where clause below is my simple implementation. Am getting the result but am getting data for all the column instead of my requested fileName column alone.
List<ImageSiloVO> queryResult = imageSiloRepo.findAll(new Specification<ImageSiloVO>() {
#Override
public Predicate toPredicate(Root<ImageSiloVO> root, CriteriaQuery<?>query, CriteriaBuilder criteriaBuilder) {
query.select(root.get("fileName"));
List<Predicate> predicates = new ArrayList<>();
if(StringUtils.isNoneBlank(imageSiloVO.getEntryNumber())) {
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.get("entryNumber"), imageSiloVO.getEntryNumber())));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
});

Specification OR clause, in and isEmpty/isNull

I have workers that have competences (driving licenses and such) and then there are mechanisms that require certain competences. Sometimes the mechanisms require no competences at all.
Currently I have a Specification with an in clause that works fine, but I would like it to also send out mechanisms that require no competences to operate.
public static Specification<Mechanism> hasCompetences(String searchTerm) {
return (root, query, criteriaBuilder) -> {
query.distinct(true);
List<String> list = new ArrayList<>(Arrays.asList(searchTerm.split(",")));
return root.join("competences").get("name").in(list);
};
}
If I have 3 mechanisms with competences like
Car | B-Category |
Van | C-Category |
Bicycle |(no data here) |
After requesting mechanisms?competences=B-Category it returns Car as expected, but I would like to get the Bicycle too.
Or is there a way to get all all mechanisms that don't require competences? I tried mechanisms?competences= but that returned [].
Edit:
This is where I'm at right now:
public static Specification<Mechanism> hasCompetences(List<String> list) {
return (root, query, cb) -> {
query.distinct(true);
return cb.or(
cb.isEmpty(root.join("competences")),
root.join("competences").get("name").in(list)
);
};
}
But the isEmpty is giving me this error:
java.lang.IllegalArgumentException: unknown collection expression type [org.hibernate.query.criteria.internal.path.SetAttributeJoin]
Edit2:
public static Specification<Mechanism> hasCompetences(List<String> list) {
return (root, query, cb) -> {
query.distinct(true);
Join<Mechanism, Set<Competence>> competences = root.join("competences", JoinType.LEFT);
return cb.or(
root.join("competences").get("name").in(list),
cb.isEmpty(competences)
);
};
}
Error:
unknown collection expression type [org.hibernate.query.criteria.internal.path.SetAttributeJoin];
You have 2 errors:
The criteria to match empty collection is cb.isEmpty(root.get("competences"))
You need to specify left join. root.join("competences", JoinType.LEFT)
Without the second amendment, you make an inner join, so you will never retrieve Mechanisms with empty competences.
Update
You proposed
Join<Mechanism, Set<Competence>> competences = root.join("competences", JoinType.LEFT);
return cb.or(
root.join("competences").get("name").in(list),
cb.isEmpty(competences)
);
isEmpty won't work on SetAttributeJoin (the result of root.join) - look point 1. above
Try
Join<Mechanism, Set<Competence>> competences = root.join("competences", JoinType.LEFT);
return cb.or(
competences.get("name").in(list),
cb.isEmpty(root.get("competences"))
);

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

CriteriaBuilder - Sum using SelectCase

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;
}

Trouble with a LINQ 'filter' code throwing an error

I've got the following code in my Services project, which is trying to grab a list of posts based on the tag ... just like what we have here at SO (without making this a meta.stackoverflow.com question, with all due respect....)
This service code creates a linq query, passes it to the repository and then returns the result. Nothing too complicated. My LINQ filter method is failing with the following error :-
Method 'Boolean
Contains(System.String)' has no
supported translation to SQL.
I'm not sure how i should be changing my linq filter method :( Here's the code...
public IPagedList<Post> GetPosts(string tag, int index, int pageSize)
{
var query = _postRepository.GetPosts()
.WithMostRecent();
if (!string.IsNullOrEmpty(tag))
{
query = from q in query
.WithTag(tag) // <--- HERE'S THE FILTER
select q;
}
return query.ToPagedListOrNull(index, pageSize);
}
and the Filter method...
public static IQueryable<Post> WithTag(this IQueryable<Post> query,
string tag)
{
// 'TagList' (property) is an IList<string>
return from p in query
where p.TagList.Contains(tag)
select p;
}
Any ideas? I'm at a loss :(
Try with Any:
public static IQueryable<Post> WithTag(this IQueryable<Post> query,
string tag)
{
// 'TagList' (property) is an IList<string>
return from p in query
where p.TagList.Any(t => t == tag)
select p;
}
.
UPDATE (by PureKrome)
Another suggestion by Ahmad (in a comment below). This uses the Contains method so it will return all posts that contain the tag 'Test', eg. Post with Tag 'Testicle' :-
public static IQueryable<Post> WithTag(this IQueryable<Post> query,
string tag)
{
// 'TagList' (property) is an IList<string>
return from p in query
where p.TagList.Any(t => t.Contains(tag))
select p;
}
In WithTag try changing the query to use a List rather than an IList:
return from p in query
let taglist = p.TagList as List<string>
where taglist.Contains(tag)
select p;
Also check out this answer, which is similar to my suggestion: Stack overflow in LINQ to SQL and the Contains keyword

Resources