I develop my project web application and I wanted to change searching profiles feature to use JPA Criteria. My idea is to ommit these searching criteria, which fields in html form were left blank. The difficult part is to write Predicate with String List of interests.
public List<Profile> searchProfiles(String sex, String city, List<String> interests) {
List<String> emptyInterests = new ArrayList<>(); emptyInterests.add("");
Session session = this.sessionFactory.getCurrentSession();
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Profile> criteriaQuery = builder.createQuery(Profile.class);
Root<Profile> root = criteriaQuery.from(Profile.class);
List<Predicate> predicates = new ArrayList<>();
if(!"".equals(sex)) {
predicates.add(builder.equal(root.get("sex"), sex ));
}
if(!"".equals(city)) {
predicates.add(builder.equal(root.get("city"), city ));
}
if(!emptyInterests.equals(interests)) {
// REASON OF ASKING THIS QUESTION
}
criteriaQuery.select(root).where(predicates.toArray(new Predicate[]{}));
return session.createQuery(criteriaQuery).list();
}
In the last "if" block I want to add Predicate which will means more or less "add Profile to results list if its String list of interests (Profile class field) contains all elements from method argument "interests" ". This condition in normal list filtering it would look like:
for(Profile profile : profiles) {
if(profile.getInterests().contains(interests))
results.add(profile);
}
Edit:
Following code causes ClassCastException: java.base/java.lang.String cannot be cast to java.base/java.util.List, in the return line.
if(!emptyInterests.equals(interests))
{
Expression<String> interestsExpression = root.get("interests");
Predicate interestsPredicate = interestsExpression.in(interests);
predicates.add(interestsPredicate);
}
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();
}
I have Spring Boot application where I am doing filtering. User can filter ads through tags(ads-tags is many-to-many with third table). And everything is okay, but when I send list of tags for filtering, my query is returning me a list of all ads which have AT LEAST 1 tag, instead returning only ads which have all tags from filter. If I make query with only one tag, everything is okay, but when I send a list case above is happening. This is my filter method:
#Override
public List<AdsDTO> findAll(AdsSubGroup adssubgroup, Long userId, String status, String adsType,
String businessType, Long adsGroupId, String region, Integer fromPrice,
Integer toPrice, Boolean fixedPrice, Boolean freeDelivery, Boolean productWarranty,
Boolean urgentSales, Boolean hasImage, Integer pageNumber, Integer pageSize, List<String> tags) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Ads> query = builder.createQuery(Ads.class);
Root<Ads> ads = query.from(Ads.class);
// query.orderBy(builder.desc(ads.get("adsDate")));
List<Predicate> predicates = new ArrayList<>();
Join<Ads, JwtUser> adsUsersJoin = ads.join("users");
Join<Ads, AdsSubGroup> adsAdsSubGroupJoin = ads.join("adssubgroup");
Join<Ads, Tag> tagsJoin = ads.join("adsTags");
In<String> in = builder.in(tagsJoin.get("name"));
if (tags != null && tags.size() > 0) {
for (String tag : tags) {
in.value(tag);
}
predicates.add(in);
}
query.select(ads);
query.distinct(true);
query.where(predicates.toArray(new Predicate[0]));
if(!(pageNumber==null && pageSize==null)) {
TypedQuery<Ads> typedQuery = em.createQuery(query);
typedQuery.setFirstResult((pageNumber-1)*pageSize);
typedQuery.setMaxResults(pageSize);
List<Ads> adsList = typedQuery.getResultList();
return AdsConverter.convertToAdsDTO(adsList);
}else {
List<Ads> adsList = em.createQuery(query).getResultList();
return AdsConverter.convertToAdsDTO(adsList);
}
}
How can I fix that query returns me only ads which have every tag from the list?
The problem is in the approach, the query is wrong.
You should for example use a subquery to check that there are N entries for each article, where N is the number of TAGS entered.
Something like that:
//Subquery with count
Subquery<Long> countTagsSq = query.subquery(Long.class);
Root<Ads> rootSQ = countTagsSq.from(Ads.class);
Join<Ads,Tag> joinTagSQ = rootSQ.join("adsTags");
//We set the where condition by Ads id (I don't know what the attribute is called)
countTagsSq.where(
cb.and(
builder.equal(rootSQ.get("id"),ads.get("id")),
joinTagSQ.get("name").in(tags)
)
);
countTagsSq.select(builder.count(joinTagSQ ));
// Change your query where clause
query.where(cb.equal(countTagsSQ.getSelection(),Long.valueOf(tags.size())))
In this way, if Tags have a size of 10, it will return those Ads that have 10 of the tags you enter.
I have an event object with following attributes:
class Event {
String name;
String location;
LocalDateTime date;
String description;
}
Lets say I get from web API a list of events:
List<Events> events = getEvents(); // e.g. 5 events
And now I want to check how many of these events I already have in my DB.
Event is unique if combination of values: name, location and date is also unique.
So basically I want to a create query to do this:
Optional<Event> getByNameAndLocationAndDate(String name, String location, LocalDate date);
but for a list of item in just one query. Something like:
Optional<Event> getByNameAndLocationAndDate(List<Events> events);
Is it possible with JPA?
There is no built-in or specially pretty way of doing this. But you could generate a query by using a loop:
public List<Event> getByNameAndLocationAndDate(List<Event> events) {
if (events.isEmpty()) {
return new ArrayList<>();
}
final StringBuilder queryBuilder = new StringBuilder("select e from Event e where ");
int i = 0;
for (final Event event : events) {
if (i > 0) {
queryBuilder.append("or")
}
queryBuilder.append(" (e.name = :name" + i);
queryBuilder.append(" and e.location = :location" + i);
queryBuilder.append(" and e.date = :date" + i + ") ");
i++;
}
final TypedQuery<Event> query = em.createQuery(queryBuilder.toString());
int j = 0;
for (final Event event : events) {
query.setParameter("name" + j, event.getName());
query.setParameter("location" + j, event.getLocation());
query.setParameter("date" + j, event.getDate());
}
return query.getResultList();
}
Like I said, not very pretty. Might be better with criteria API. Then again, unless you have very strict requirements for execution speed, you might be better off looping through the list checking one event at the time. It will result in the more queries run against the database, but also much prettier code.
Edit: Here is attempt using criteria API, haven't used it much so created just by googling, no guarantee it works as it is..
public List<Event> getByNameAndLocationAndDate(List<Event> events) {
if (events.isEmpty()) {
return new ArrayList<>();
}
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<Event> query = cb.createQuery(Event.class);
final Root<Event> root = query.from(Event.class);
final List<Predicate> predicates = new ArrayList<>();
final List<Predicate> predicates = events.stream().map(event -> {
return cb.and(cb.equal(root.get("name"), event.getName()),
cb.equal(root.get("location"), event.getLocation()),
cb.equal(root.get("date"), event.getDate()));
}).collect(Collectors.toList());
query.select(root).where(cb.or(predicates.toArray(new Predicate[]{})));
return em.createQuery(query).getResultList();
}
try
List<Event> findByNameInAndLocationInAndDateIn(List<String> names,List<String> locations,List<Date> dates);
but this returns a list, not a single event, if you need verify if one event is not in database, the only way to do this is search one by one,
you can use this function for decide if needs that.
I'm not sure if this function behaves as you wish
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();
}
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 ;)