JPA Criteria Query - How to Avoiding Duplicate Joins

A suggestion for avoid it is to use a builder class to encapsulate the joins , see below.

public class AccountCriteriaBuilder {

        CriteriaBuilder cb;
        CriteriaQuery<Account> cq;

        // JOINS INSTANCE
        Root<Account> accountRoot;
        Join<Account,Person> personJoin;
        Join<Person,Address> personAddressJoin;

        public AccountCriteriaBuilder(CriteriaBuilder criteriaBuilder) {
            this.cb =  criteriaBuilder;
            this.cq = cb.createQuery(Account.class);
            this.accountRoot = cq.from(Account.class);
        }

        public CriteriaQuery buildQuery() {
            Predicate[] predicates = getPredicates();
            cq.select(accountRoot).where(predicates);
            return cq;
        }

        public Predicate[] getPredicates() {

           List<Predicate> predicates = new ArrayList<Predicate>();

           predicates.add(cb.equal(getPersonJoin().get(Person_.name), "Roger"));
           predicates.add(cb.greaterThan(getPersonJoin().get(Person_.age), 18));
           predicates.add(cb.equal(getPersonAddressJoin().get(Address_.country),"United States"));

           return predicates.toArray(new Predicate[predicates.size()]);
        }

        public Root<Account> getAccountRoot() {
            return accountRoot;
        }

        public Join<Account, Person> getPersonJoin() {
            if(personJoin == null){
                personJoin = getAccountRoot().join(Account_.person);
            }
            return personJoin;
        }

        public Join<Person, Address> getPersonAddressJoin() {
            if(personAddressJoin == null){
                personAddressJoin = getPersonJoin().join(Person_.address);
            }
            return personAddressJoin;
        }


}

The “ace in the hole” is the lazy loads for each required join instance, it will avoid duplicate joins and also to simplify the navigation process.

Finally, just call the builder like below :

AccountCriteriaBuilder criteriaBuilder = new AccountCriteriaBuilder(em.getCriteriaBuilder());
TypedQuery<Account> query = em.createQuery(criteriaBuilder.buildQuery());
List<Account> result = query.getResultList();

Enjoy :)


We use the following utility method to avoid duplicate joins

public class CriteriaApiUtils {
  public static <X, Y> ListJoin<X, Y> join(Root<X> criteriaRoot,
                                             ListAttribute<? super X, Y> attribute,
                                             JoinType joinType
  ) {
    return (ListJoin<X, Y>) criteriaRoot.getJoins().stream()
        .filter(j -> j.getAttribute().getName().equals(attribute.getName()) && j.getJoinType().equals(joinType))
        .findFirst()
        .orElseGet(() -> criteriaRoot.join(attribute, joinType));
  }
}