2

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.

1
  • 1
    Replace the return null to criteriaBuilder.conjunction() . It's mean Create a conjunction (with zero conjuncts). A conjunction with zero conjuncts is true.` and return predicate. Commented Jul 2, 2021 at 2:39

1 Answer 1

1

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;
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.