nehacharya
nehacharya

Reputation: 957

How to pass a HashMap to CriteriaBuilder and add to Predicates list

I am currently receiving a Map<String, String> in the API request body. I have an entity,

class TagEntity {
    @Column(name = "name", length = 128, nullable = false)
    private String name;

    @Column(name = "value", length = 256, nullable = false)
    private String value;
}

There is a MachineEntity which has a Set<TagEntity>. It's a @OneToMany mapping. I am trying to fetch the machine entities according to the name and value passed in the HashMap (in request) which correspond to the TagEntity.

I tried this code below. However, it works only if I pass one name-value pair in the request. Suppose, if the HashMap contains 2 elements, the query returns an empty list when it should return 2 Machine Entities.

SetJoin<MachineEntity, TagEntity> join = root.join(MachineEntity_.tagEntities);
for (Map.Entry element : request.getTags().entrySet()) {   
    predicates.add(criteriaBuilder.and(
                   criteriaBuilder.equal(join.get(TagEntity_.name), element.getKey()),
                   criteriaBuilder.equal(join.get(TagEntity_.value), element.getValue())
    ));
}                                                     

Is there a way I can set the HashMap in CriteriaBuilder without iterating through the Map? I am clueless what can be done to solve this problem. Will greatly appreciate some help.

Upvotes: 0

Views: 1237

Answers (1)

Nikos Paraskevopoulos
Nikos Paraskevopoulos

Reputation: 40298

As always, writing the statement in (roughly) SQL helps:

SELECT ...
FROM Machine m
WHERE -- repeat for each map entry
      EXISTS (SELECT ... FROM Tag t WHERE t.machine_id = m.id AND t.name = ?1 AND t.value = ?2)
  AND EXISTS (SELECT ... FROM Tag t WHERE t.machine_id = m.id AND t.name = ?3 AND t.value = ?4)
  ...

If this suits your needs, it can be translated to criteria API roughly as follows (you will probably need to tweak it):

private Subquery<TagEntity> makeExistsSubquery(CriteriaQuery<MachineEntity> q, CriteriaBuilder cb, Map.Entry<String,String> e) {
  Subquery<TagEntity> subquery = q.subquery(TagEntity.class);
  Root<TagEntity> tagRoot = subquery.from(TagEntity.class);
  subquery.where(cb.and(
    cb.equal(tagRoot.get(TagEntity_.name), e.getKey()),
    cb.equal(tagRoot.get(TagEntity_.value), e.getValue())
  ));
  return subquery;
}

private void your_method() {
  // assuming something like this exists:
  CriteriaQuery<MachineEntity> query = criteriaBuilder.createQuery(MachineEntity.class);

  // do this:
  for (Map.Entry element : request.getTags().entrySet()) {
    predicates.add(criteriaBuilder.exists(makeExistsSubquery(query, criteriaBuilder, element)));
  }

  ...
}

I am not sure how efficient is this query going to be. But I hope this hints to a solution.

Additionally I am assuming you want machines that match ALL the given tags. If you want machines matching ANY of the given tags, the SQL query would be simpler:

SELECT ...
FROM Machine m JOIN Tag t ON t.machine_id = m.id
WHERE -- repeat for each map entry
     t.name = ?1 AND t.value = ?2
  OR t.name = ?3 AND t.value = ?4
  ...
Predicate tagsPredicate = criteriaBuilder.or( 
  request.getTags().entrySet().stream()
    .map(e -> criteriaBuilder.and(
      criteriaBuilder.equal(join.get(TagEntity_.name), e.getKey()),
      criteriaBuilder.equal(join.get(TagEntity_.value), e.getValue())
    ))
    .collect(Collectors.toList())
    .toArray(new Predicate[0])
);
// add the tagsPredicate to your where clause, if the user has actually defined tag criteria

Upvotes: 1

Related Questions