Search code examples
javajpaeclipselinkcriteria-api

Using JPA CriteriaBuilder to generate query where attribute is either in a list or is empty


I am trying to use the JPA CriteriaBuilder to generate a query for an entity called "TestContact" that has a many-to-many join with another entity called "SystemGroup" where the attribute for this join called "groups". The objective of the query is to retrieve records from the "TestContact" entity where the "groups" attribute is either in a list or is empty.

The code I'm using is as follows

public List<TestContact> findWithCriteriaQuery(List<SystemGroup> groups) {

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<TestContact> cq = cb.createQuery(TestContact.class);
    Root<TestContact> testContact = cq.from(TestContact.class);
    cq.select(testContact);

    Path<List<SystemGroup>> groupPath = testContact.get("groups");

    // cq.where(groupPath.in(groups));
    // cq.where(cb.isEmpty(groupPath));
    cq.where(cb.or(cb.isEmpty(groupPath), groupPath.in(groups)));

    TypedQuery<TestContact> tq = em.createQuery(cq);

    return tq.getResultList();
}

The problem is this query only returns results where group is in the list "groups" but for some reason isn't also returning the results where group is empty (i.e. there is no entry in the join table)

If I change the where clause to cq.where(cb.isEmpty(groupPath)); then the query correctly returns the results where group is empty.

If I change the where clause to cq.where(groupPath.in(groups)); then the query correctly returns the results where the group is in the list "groups".

What I don't understand is why when I try to combine these two predicates using the CriteriaBuilder or method the results don't include the records where the group is either in the list or is empty.

The groups attribute in the "TestContact" entity is declared as follows

@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(name = "TEST_CONTACT_GROUPS", joinColumns = { @JoinColumn(name = "CONTACT_ID", referencedColumnName = "CONTACT_ID") }, inverseJoinColumns = { @JoinColumn(name = "GROUP_ID", referencedColumnName = "GROUP_ID") })
private List<SystemGroup> groups;

The JPA provider is EclipseLink 2.5.0, the Java EE application server is GlassFish 4 and the database is Oracle 11gR2.

Can anyone please point out where I'm going wrong?

Update

I've tried the suggestion from @Chris but Eclipse is returning the following error on Join<List<SystemGroup>> groupPath = testContact.join("groups", JoinType.LEFT)

Incorrect number of arguments for type Join; it cannot be parameterized with arguments >

Looking at the JavaDoc for Join it says the type parameters are...

Z - the source type of the join, X - the target type of the join

I've tried Join<TestContact, SystemGroup> groupPath = testContact.join("groups", JoinType.LEFT); which then causes Eclipse to return the following error on cb.isEmpty

Bound mismatch: The generic method isEmpty(Expression) of type CriteriaBuilder is not applicable for the arguments (Join). The inferred type SystemGroup is not a valid substitute for the bounded parameter >


Solution

  • The testContact.get("groups"); clause forces an inner join from testContact to groups, which filters out testContacts with no groups. You need to specify a left outer join, and use that in your isEmpty and in clauses.

    Root<TestContact> testContact = cq.from(TestContact.class);
    cq.select(testContact);
    
    Join<TestContact, SystemGroup> groupPath = testContact.join("groups", JoinType.LEFT);
    cq.where(cb.or(cb.isEmpty(testContact.get("groups")), groupPath.in(groups)));
    

    I usually refer to https://en.wikibooks.org/wiki/Java_Persistence/Criteria#Join for examples