adeelmahmood
adeelmahmood

Reputation: 2431

How to properly apply join fetch in JPA Criteria API

I am using JPA Criteria API in a Spring data JPA based application. My service class uses static methods to retrieve Specifications which can then be combined together to form a particular query. E.g.

repository.findAll(where(matchById(str)).or(matchByName(str)))

Here, I am using two methods that return Specifications with the appropriate criteria applied to it. This is how the two methods look like

public static Specification<SomeEntity> matchById(String str) {
    return (root, criteriaQuery, cb) -> 
        cb.like(root.get(SomeEntity_.id).as(String.class), str + "%");
}

public static Specification<SomeEntity> matchByName(String str) {
    return (root, criteriaQuery, cb) -> {
        cb.or(cb.like(cb.lower(root.get(SomeEntity_.firstName)), str.toLowerCase() + "%"),
              cb.like(cb.lower(root.get(SomeEntity_.lastName)), str.toLowerCase() + "%")
        );
}

This works fine. I want to add a

root.fetch(SomeEntity_.employee, JoinType.INNER);

in such a way that all queries that are built using any combination of static Specifications methods, utilize the FETCH JOIN.

If I add this statement to both static methods, then the INNER JOIN is applied twice which doesnt seem right. Ideally, I think I should have another static method that only applies the FETCH JOIN and return the Specifications but I cant seem to figure out how to return a Predicate without using any of the criteriaBuilder methods. To clarify, this is how my method should look like:

public static Specification<SomeEntity> start() {
    return (root, criteriaQuery, criteriaBuilder) -> {
        root.fetch(SomeEntity_.employee, JoinType.INNER);

        // NOW RETURN WHAT ???
        return null;
    };
}

Any help would be appreciated.

Upvotes: 2

Views: 5340

Answers (1)

Naros
Naros

Reputation: 21163

One solution I used in the past was to introduce a CriteriaQueryHelper class that allowed me to provide it with several JPA classes and it would determine whether a new join or fetch should be constructed or reuse an existing one.

With the use of the following, your Specification implementations would simply use the helper class by calling #getOrCreateJoin(...) and it would return either (a) an existing join without creating a new one or (b) a newly created instance if one didn't exist.

This avoids the issue you described with multiple joins quite easily.

public class CriteriaQueryHelper {

  // for List<> attributes, get or create a join
  // other implementations would be needed for other container types likely.
  public static <X, Y, Z> ListJoin<X, Y> getOrCreateJoin(
                From<Z, X> root, 
                ListAttribute<X, Y> attribute,
                JoiNType joinType) {
    ListJoin<X, Y> join = (ListJoin<X, Y>) getJoin( root, attribute, joinType );
    return join != null ? join : root.join( attribute, joinType );
  }

  // gets the join, looking at join-fetch first, followed by joins
  private static <X, Y, Z> Join<X, Y> getJoin(
                From<Z,X> root, 
                Attribute<?, Y> attribute, 
                JoinType joinType) {
    Join<X, Y> fetchJoin = getJoinFromFetches( root, attribute );
    if ( fetchJoin != null ) {
      return fetchJoin; 
    }
    Join<X, Y> join = getJoinFromJoins( root, attribute, joinType );
    return join;
  }

  // gets a join from fetch
  private static <X, Y, Z> Join<X, Y> getJoinFromFetches(
                 From<Z, X> root, 
                 Attribute<?, Y> attribute) {
    for ( Fetch<X, ?> fetch : root.getFetches() ) {
      final Class<?> attributeClass = fetch.getAttribute().getClass();
      if ( attributeClass.isAssignableFrom( attribute.getClass() ) ) {       
        final String name = attribute.getName();
        if ( name.equals( fetch.getAttribute().getName() ) ) {
          return (Join<X, Y>) fetch;
        }
      }
    }
    return null;
  }      

  // gets a join from joins
  private static <X, Y, Z> Join<X, Y> getJoinFromJoins(
                 From<Z, X> root, 
                 Attribute<?, Y> attribute,
                 JoinType joinType) {
    for ( Join<?, ?> fetch : root.getJoins() ) {
      final String joinName = join.getAttribute().getName();
      if ( joinName.equals( attribute.getName() ) ) {
        if ( join.getJoinType().equals( joinType ) ) {
          return (Join<X, Y>) join;
        }
      }
    }
    return null;
  }
}

Upvotes: 1

Related Questions