JPA Criteria builder How to group logical operator AND
Current
select * from Offers marketingo0_ where (upper(marketingo0_.SOURCE_KEY_UID)=? OR marketingo0_.STATUS=? and marketingo0_.STATUS=? )
Expected
select * from Offers marketingo0_ where upper(marketingo0_.SOURCE_KEY_UID)=? OR (marketingo0_.STATUS=? and marketingo0_.STATUS=? )
List<Predicate> innerPredicates = new ArrayList<Predicate>();
List<Predicate> predicates = new ArrayList<Predicate>();
List<Predicate> outerPredicates = new ArrayList<Predicate>();
//Create all outer predicates
createPredicates(filter.getRootBooleanClause(),builder, marketingOffer, outerPredicates);
//Create all sub clauses predicates
for (BooleanClause subClause : filter.getRootBooleanClause().getSubClauses()) {
List<Predicate> groupPredicates = new ArrayList<Predicate>();
createPredicates(subClause,builder, marketingOffer, groupPredicates);
if(groupPredicates!=null && groupPredicates.size()>0 && filter.getOperator().equals(LogicOperator.OR)){
innerPredicates.add(builder.and(groupPredicates.toArray(new Predicate[groupPredicates.size()])));
}else if(groupPredicates!=null && groupPredicates.size()>0 && filter.getRootBooleanClause().getOperator().equals(LogicOperator.AND)){
innerPredicates.add(builder.or(groupPredicates.toArray(new Predicate[groupPredicates.size()])));
}
}
if(innerPredicates.size()>0){
outerPredicates.addAll(innerPredicates);
}
if(outerPredicates.size()>0 && filter.getRootBooleanClause().getOperator().equals(LogicOperator.OR)){
predicates.add(builder.or(outerPredicates.toArray(new Predicate[outerPredicates.size()])));
}else if(outerPredicates.size()>0 && filter.getRootBooleanClause().getOperator().equals(LogicOperator.AND)){
predicates.add(builder.and(outerPredicates.toArray(new Predicate[outerPredicates.size()])));
}
Try it like this:
builder.or(
builder.and(
<Column1 predicate>,
<Column2 predicate>
),
<ColumnC predicate>
);
The CriteriaBuilder will take care of correctly nesting the sub-expressions.
To achieve
upper(marketingo0_.SOURCE_KEY_UID)=?
OR
(marketingo0_.STATUS=? and marketingo0_.STATUS=? )
I think this should work
criteriaBuilder.or(
criteriaBuilder.equal(criteriaBuilder.upper(root.<String>get("SOURCE_KEY_UID")), key),
criteriaBuilder.and(
criteriaBuilder.equal(root.get("STATUS"), status),
criteriaBuilder.equal(root.get("STATUS"), status) // This doesnot make sense but sticking to OP
)
)
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 months ago.
Improve this question
In my Spring Boot app, I created a custom filtering using JPA Specification as mentioned on Searching And Filtering Using JPA Specification - Spring Boot. However, I need to join multiple tables and build a WHERE clause for my specific search via #Query.
I checked https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications page, but could not build a proper structure.
So, how can create a dynamic WHERE clause for my query?
You can use Specification. To create:
Specification<Entity> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// Add conditions to the predicates list
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
The predicates list is used to hold conditions for the WHERE. You can add conditions to this list using the cb (CriteriaBuilder) object and the root and query parameters. These parameters provide access to the entity and the query being constructed.
You can then use the Specification object in a #Query annotation on your repository method to apply the dynamic WHERE clause to the query.
Repository example:
#Repository
public interface EntityRepository extends JpaRepository<Entity, Long>, JpaSpecificationExecutor<Entity> {
// Other repository methods
List<Entity> findAll(Specification<Entity> spec, Pageable pageable);
}
The above repository extends the JpaSpecificationExecutor to allow working with the JPA criteria API. The findByFields method also takes a Specification object as an argument. This Specification dynamically constructs the WHERE clause for the query.
So running the query:
List<Entity> entities = entityRepository.finAll(spec, pageable);
It's something like this:
Specification<BugData> bugDataSpecification = new Specification<BugData>() {
#Override
public Predicate toPredicate(Root<BugData> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Object> bugName = root.get("bugName");
Path<Object> bugType = root.get("bugType");
Path<Object> bugLevel = root.get("bugLevel");
List<Predicate> predicateListAnd = new ArrayList<>();
List<Predicate> predicateListOr = new ArrayList<>();
if (!StringUtils.isNullOrEmpty(bugRequestParam.getBugLevel())) {
Predicate pLevel = cb.equal(bugLevel, bugRequestParam.getBugLevel()); // ==
predicateListAnd.add(pLevel);
}
for (int i = 0; i < bugRequestParam.getBugTypeList().size(); i++) {
Predicate p1 = cb.equal(bugType, bugRequestParam.getBugTypeList().get(i));
predicateListOr.add(p1);
}
if (!StringUtils.isNullOrEmpty(bugRequestParam.getBugName())) {
Expression<Integer> findStr = cb.locate(bugName.as(String.class), bugRequestParam.getBugName()); //LOCATE
Predicate pName = cb.greaterThan(findStr, 0); // >
predicateListAnd.add(pName);
}
Predicate resultAnd[] = predicateListAnd.toArray(new Predicate[predicateListAnd.size()]);
Predicate resultOr[] = predicateListOr.toArray(new Predicate[predicateListOr.size()]);
Predicate end = cb.and(cb.and(resultAnd), cb.or(resultOr));
return end;
}
};
The whole part of this code:
#GetMapping(value = "specification")
public List<BugData> whereTiaojian() {
BugRequestParam bugRequestParam = new BugRequestParam();
bugRequestParam.setBugLevel("mid");
bugRequestParam.setBugName("CVE-2019-8331");
bugRequestParam.setLimit(100);
bugRequestParam.setPage(0);
List<String> bugTypeList = new ArrayList<>(4);
bugTypeList.add("CWE-79");
bugTypeList.add("CWE-502");
bugTypeList.add("CWE-284");
bugRequestParam.setBugTypeList(bugTypeList);
Pageable pageable = PageRequest.of(bugRequestParam.getPage(), bugRequestParam.getLimit());
Specification<BugData> bugDataSpecification = new Specification<BugData>() {
#Override
public Predicate toPredicate(Root<BugData> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Object> bugName = root.get("bugName");
Path<Object> bugType = root.get("bugType");
Path<Object> bugLevel = root.get("bugLevel");
List<Predicate> predicateListAnd = new ArrayList<>();
List<Predicate> predicateListOr = new ArrayList<>();
if (!StringUtils.isNullOrEmpty(bugRequestParam.getBugLevel())) {
Predicate pLevel = cb.equal(bugLevel, bugRequestParam.getBugLevel());
predicateListAnd.add(pLevel);
}
for (int i = 0; i < bugRequestParam.getBugTypeList().size(); i++) {
Predicate p1 = cb.equal(bugType, bugRequestParam.getBugTypeList().get(i));
predicateListOr.add(p1);
}
if (!StringUtils.isNullOrEmpty(bugRequestParam.getBugName())) {
Expression<Integer> findStr = cb.locate(bugName.as(String.class), bugRequestParam.getBugName());
Predicate pName = cb.greaterThan(findStr, 0);
predicateListAnd.add(pName);
}
Predicate resultAnd[] = predicateListAnd.toArray(new Predicate[predicateListAnd.size()]);
Predicate resultOr[] = predicateListOr.toArray(new Predicate[predicateListOr.size()]);
Predicate end = cb.and(cb.and(resultAnd), cb.or(resultOr));
return end;
}
};
Page<BugData> bugDataPage = bugDataVersionFiveDao.findAll(bugDataSpecification, pageable);
// This findAll method is the most important part of this all;
return bugDataPage.getContent();
}
My code is currently working using the query below and I am converting the query to JPA Specification.
#Query("SELECT DISTINCT h, SUM(m.annualIncome) " +
"FROM Household h LEFT JOIN h.familyMemberList m " +
"GROUP BY h.id " +
"HAVING SUM(m.annualIncome) < 100000 " +
"AND (:householdSize IS NULL OR COUNT(m) = :householdSize) " +
"AND (:householdIncome IS NULL OR SUM(m.annualIncome) = :householdIncome)")
List<Household> findGrantEligibleHouseholds(#Param("householdSize") long householdSize, #Param("householdIncome") long householdIncome);
This is what I have done so far which is working but in an unclean manner.
public static Specification<Household> grantEligibleHouseholdsSpecification(HouseholdCriteria criteria) {
return Specification.where(
(root, query, builder) -> {
List<Predicate> searchCriteria = new ArrayList<>();
final Join<Household, FamilyMember> householdFamilyMemberJoin = root.join(Household_.familyMemberList, JoinType.LEFT);
if(criteria.getHousingType() != null) {
searchCriteria.add(builder.equal(root.get(Household_.housingType), criteria.getHousingType()));
}
query.groupBy(root.get(Household_.id));
if(criteria.getHouseholdIncome() != null && criteria.getHouseholdSize() != null) {
query.having(builder.lt(builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),100000)
,builder.equal(builder.count(householdFamilyMemberJoin),criteria.getHouseholdSize())
,builder.equal(builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),criteria.getHouseholdIncome()));
}
else if(criteria.getHouseholdIncome() != null) {
query.having(builder.lt(builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),100000)
,builder.equal(builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),criteria.getHouseholdIncome() ));
}
else if(criteria.getHouseholdSize() != null) {
query.having(builder.lt(builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),100000)
,builder.equal(builder.count(householdFamilyMemberJoin),criteria.getHouseholdSize()));
}
else {
query.having(builder.lt(builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),100000));
}
query.multiselect();
return builder.and(searchCriteria.toArray(new Predicate[searchCriteria.size()]));
}
);
}
How do I improve this code so in the future it can accept more criteria without going through so many null checks like this? Thanks!
You can use same approach as for searchCriteria - collect multiple predicates into list:
final List<Predicate> havingPredicates = new ArrayList<>();
// default predicates
havingPredicates.add(builder.lt(
builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),
100000));
// custom predicates
if (criteria.getHouseholdIncome() != null) {
havingPredicates.add(builder.equal(
builder.sum(householdFamilyMemberJoin.get(FamilyMember_.annualIncome)),
criteria.getHouseholdIncome()));
}
if (criteria.getHouseholdSize() != null) {
havingPredicates.add(builder.equal(
builder.count(householdFamilyMemberJoin),
criteria.getHouseholdSize()));
}
query.having(havingPredicates.toArray(new Predicate[0]));
I am triying to do a "like query" with a variable set of strings, in order to retrieve in a single query all texts that contains a set of words, that is:
public long countByTextLike(Set<String> strings) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Example> root = query.from(Example.class);
query.select(builder.count(root.get("id"))).where(
builder.and(
builder.equal(root.get("lang"), "EN")
)
);
//this does not work
for (String word : strings) {
query.where(builder.or(builder.like(root.get("text"), word)));
}
return manager.createQuery(query).getSingleResult();
}
unfortunately this does not work because the where is overwritten in each loop. Only the last word of loop is used and "AND" restictions are being overwriten.
How is possible to do a "like query" with a variable number of strings? It is not posible?
I am using the spring framework but i think that the question could be extendable to hibernate
You can use predicates, and then add them all with only one where clause
public long countByTextLike(Set<String> strings) {
CriteriaBuilder builder = currentSession().getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Example> root = query.from(Example.class);
Predicate[] predicates = new Predicate[strings.size()];
query.select(builder.count(root.get("id")));
Predicate langPredicate = builder.equal(root.get("lang"), "EN");
int cont = 0;
for (String word : strings) {
Predicate pred = builder.like(root.get("text"), "%" + word + "%");
predicates[cont++] = pred;
}
Predicate orPredicate = builder.or(predicates);
Predicate finalPredicate = builder.and(orPredicate, langPredicate);
return manager.createQuery(query).where(finalPredicate).getSingleResult();
}
In my Spring Data/JPA query I need to add filtering with many criteria and the user can choose whatever he want.
Is there a way to get working together QueryDSL and JPA 2.1 Entity Graph ? If so, could you please show an example ?
This is some code from my project using JPA Criteria API. Main idea that user can choose any field as filter and in service layer all filters are passed as List<Map<String, Object>>, where String key in map is a name of field and Object value is filter value. Maybe it will be helpfull:
public List<DocumentsShort> findAllByCriteria(Integer firstResult, Integer maxResult, String sort, String condition, List<Map<String, Object>> conditions) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<EntityClass> criteriaQuery = cb.createQuery(EntityClass.class);
Root<EntityClass> root = criteriaQuery.from(EntityClass.class);
Join<EntityClass, AnotherEntityClass> AnotherEntityClassJoin = root.join("fieldOfEntity", JoinType.LEFT);
Predicate predicate = cb.conjunction();
List<Predicate> predicateList = new ArrayList<>();
for (Map<String, Object> map : conditions) {
Predicate tempPredicate = cb.conjunction();
tempPredicate = cb.and(predicate, cb.equal(root.get("deleted"), 0)); // only entities not marked as deleted
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Path pathToField = null;
pathToField = root.get(key);
Object value = entry.getValue();
if (value == null) {
tempPredicate = cb.and(tempPredicate, cb.isNull(pathToField));
} else if (value instanceof String) {
tempPredicate = cb.and(tempPredicate, cb.like(pathToField, "%" + value + "%"));
} else if (value instanceof List) {
tempPredicate = cb.and(tempPredicate, pathToField.in(((List) value)));
} else {
tempPredicate = cb.and(tempPredicate, cb.equal(pathToField, value));
}
}
predicateList.add(tempPredicate);
}
criteriaQuery.where(cb.or(predicateList.toArray(new Predicate[predicateList.size()])));
TypedQuery query = entityManager.createQuery(criteriaQuery);
query.setFirstResult(firstResult != null ? firstResult : 0);
query.setMaxResults(maxResult != null ? maxResult : 500);
return query.getResultList();
}
I'm a bit confused while creating a criteriaQuery with JPA 2.0.
Prerequisites:
I have a Gui, where the user can mark some checkboxes of (let us say) wheatherstations with some options like temperature/wind/timeperiod/etc...
Now I want to set up a criteriaQuery to pick just the selected items from a sql database and return it as an object/Map/List for building some DataModels (this will be used for generating a few primefaces charts).
What i have so far:
// for presentation purposes just this mockup-data
Calendar start = new GregorianCalendar(2011, Calendar.APRIL, 1);
Calendar end = new GregorianCalendar(2011, Calendar.MAY, 1);
List<String> selectedStations = new LinkedList<String>() {{
add("PS1");
add("PS2");
add("PS3");
}};
Map<String, Object selectedOptions = new LinkedHashMap<String, Object>() {{
put("opt1","val1");
put("opt2","val2");
put("opt3","val3");
}};
List<String> sel = new LinkedList<String>() {{
add("selOpt1");
add("selOpt2");
add("selOpt3");
}};
criteriaBuilder, criteriaQuery and the mapping class:
// go for the criteriaBuilder
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<StationItem> r = cq.from(StationItem.class);
Setting up the predicates:
// ... where (name="PS1" or name="PS2" or name="PS3") ...
Predicate p1 = cb.disjunction();
for (String s : selectedStations) {
p1 = cb.or(p1, cb.equal(r.get("name").as(String.class), s));
}
Predicate p2 = cb.between(r.get("fetchDate").as(Date.class),
start.getTime(), end.getTime());
Predicate p3 = cb.conjunction();
for (Map.Entry<String, Object> param : selectedOptions.entrySet())
p3 = cb.and(p3, cb.equal(r.get(param.getKey()), param.getValue()));
And the final step to run the query and fetching the results:
At this point I do not know what is the best approach to fill the multiselect criteria with my selections. I would like to insert all items/selections from the List sel to cq.multiselect() with some kind of a loop in a dynamic way...Any idea is welcome!
// This is working but static :(
cq.multiselect(r.get(sel.get(0)), r.get(sel.get(1)), r.get(sel.get(2)));
// i would prefer to have something like
for (int i=0;i<sel.size();i++) {
cq.multiselect().add(r.get(sel.get(i)));
}
Concatenating my WHERE-clause and executing the query:
cq.where(cb.and(p1,p2,p3));
List<Tuple> res = em.createQuery(cq).getResultList();
for (Tuple t : res) {
// do something ...
};
return <something useful>
Following a pseudo SQL query to sum up what I want to achieve:
SELECT {items from List<String> sel}
FROM MyStationDatabase
WHERE (name = selectedStation.get(0) OR ... OR name = selectedStation.get(last))
AND {items from Map<String,Object> selectedOptions}
Well, sometimes it's too trivial to be true -.-
One way to fill the cq.multiselect() with my dynamic list is to just create a list of selections and pass this over to my multiselect-query.
List<Selection<?>> s = new LinkedList<Selection<?>>();
for (String item : sel) {
s.add(r.get(item));
}
cq.multiselect(s);
easy, but maybe someone has the same struggles with this :)
and even if not, see it as an example for a criteriaQuery ;)