Question

Sorry if my terminology isn't correct.

We are using spring data, JpaRepositories and criteria queries as our method to query data from our database.

I have a problem that when I combine two specifications such as I do with hasTimeZone and hasCity in hasCityAndTimeZone in the code example below it does a join on the same table twice, so the query below will look something like

select * from Staff, Location, Location

Is there any way to have the two specifications use the same join instead of each defining their own join that is essentially the same?

Sorry the code probably isn't complete I was just trying to show a quick example.

class Staff {
    private Integer id;
    private Location location;
}

class Location {
    private Integer id; 
    private Integer timeZone;
    private Integer city;
}

class StaffSpecs {
    public static Specification<Staff> hasTimeZone(Integer timeZone) {
        return new Specification<Staff>() {
            @Override
            public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Integer> timeZonePath = root.join(Staff_.location).get(Location_.timeZone);
                return cb.equal(timeZonePath, timeZone);
            }
        }
    }

    public static Specification<Staff> hasCity(Integer city) {
        return new Specification<Staff>() {
            @Override
            public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Integer> cityPath = root.join(Staff_.location).get(Location_.city);
                return cb.equal(cityPath, city);
            }
        }
    }

    public static Specification<Staff> hasCityAndTimeZone(Integer city, Integer timeZone) {
        return where(hasCity(city)).and(hasTimeZone(timeZone));
    }
}
Was it helpful?

Solution

There's no out of the box way unfortunately. Spring Data internally uses some reuse of joins within QueryUtils.getOrCreateJoin(…). You could find out about potentially already existing joins on the root and reuse them where appropriate:

private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {

  for (Join<?, ?> join : from.getJoins()) {

    boolean sameName = join.getAttribute().getName().equals(attribute);

    if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
      return join;
    }
  }

  return from.join(attribute, JoinType.LEFT);
}

Note, that this only works as we effectively know which joins we add ourselves. When using Specifications you should also do, but I just want to make sure nobody considers this a general solution for all cases.

OTHER TIPS

Based on @Oliver answer I created an extension to Specification interface

JoinableSpecification.java

public interface JoinableSpecification<T> extends Specification<T>{

  /**
   * Allow reuse of join when possible
   * @param <K>
   * @param <Z>
   * @param query
   * @return
   */

  @SuppressWarnings("unchecked")
  public default <K, Z> ListJoin<K, Z> joinList(From<?, K> from, ListAttribute<K,Z> attribute,JoinType joinType) {

    for (Join<K, ?> join : from.getJoins()) {

      boolean sameName = join.getAttribute().getName().equals(attribute.getName());

      if (sameName && join.getJoinType().equals(joinType)) {

        return (ListJoin<K, Z>) join; //TODO verify Z type it should be of Z after all its ListAttribute<K,Z>
      }
    }
    return from.join(attribute, joinType);
  }

  /**
   * Allow reuse of join when possible
   * @param <K>
   * @param <Z>
   * @param query
   * @return
   */
  @SuppressWarnings("unchecked")
  public default <K, Z> SetJoin<K, Z> joinList(From<?, K> from, SetAttribute<K,Z> attribute,JoinType joinType) {

    for (Join<K, ?> join : from.getJoins()) {

      boolean sameName = join.getAttribute().getName().equals(attribute.getName());

      if (sameName && join.getJoinType().equals(joinType)) {
        return (SetJoin<K, Z>) join; //TODO verify Z type it should be of Z after all its ListAttribute<K,Z>
      }
    }
    return from.join(attribute, joinType);
  }

  /**
   * Allow reuse of join when possible
   * @param <K>
   * @param <Z>
   * @param query
   * @return
   */
  @SuppressWarnings("unchecked")
  public default <K, Z> Join<K, Z> joinList(From<?, K> from, SingularAttribute<K,Z> attribute,JoinType joinType) {

    for (Join<K, ?> join : from.getJoins()) {

      boolean sameName = join.getAttribute().getName().equals(attribute.getName());

      if (sameName && join.getJoinType().equals(joinType)) {
        return (Join<K, Z>) join; //TODO verify Z type it should be of Z after all its ListAttribute<K,Z>
      }
    }
    return from.join(attribute, joinType);
  }

}

How to use

class StaffSpecs {
 public static Specification<Staff> hasTimeZone(Integer timeZone) {
    return new JoinableSpecification<Staff>() {
        @Override
        public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            Path<Integer> timeZonePath = this.joinList(root,Staff_.location,JoinType.INNER).get(Location_.timeZone);
            return cb.equal(timeZonePath, timeZone);
        }
    }
}

 public static Specification<Staff> hasCity(Integer city) {
    return new JoinableSpecification<Staff>() {
        @Override
        public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            Path<Integer> cityPath = this.joinList(root,Staff_.location,JoinType.INNER).get(Location_.city);
            return cb.equal(cityPath, city);
        }
    }
}
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {

    for (Join<?, ?> join : from.getJoins()) {

        boolean sameName = join.getAttribute().getName().equals(attribute);

        if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
            return join;
        }
    }

    return from.join(attribute, JoinType.LEFT);
}

And in CustomSpecification

@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {

    query.distinct(true);

    String[] parts = criteria.getKey().split("\\.");
    Path<?> path = root;
    for (String part : parts) {
        if(path.get(part).getJavaType() == Set.class){
            path = getOrCreateJoin(root, part);
        }else{
            path = path.get(part);
            }
        }
    }

....

if (path.getJavaType() == String.class) {
            return builder.like(path.as(String.class), "%" + criteria.getValue().toString() + "%");

....

This is old question but i wrote this answer for people who having this problem.

I implemented a generic method to search a join alias if there is no join with this alias then it creates the join with given pathFunction.

As Oliver say you can use defined joins but if you have multiple joins for an entity you need to know alias of your defined join.

//Get or create join with name of alias    
protected <F extends From<FF, FR>, FF, FR, J extends Join<JF, JR>, JF, JR>J getOrCreateCriteriaJoin(F from, String alias, BiFunction<F, CriteriaBuilder, J> pathFunction) {

    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    Set<Join<FR, ?>> joins = from.getJoins();
    Optional<J> optionalJoin = findJoin((Set) joins, alias);

    return optionalJoin.orElseGet(() -> {
            J join = pathFunction.apply(from, criteriaBuilder);
            join.alias(alias);
            return join;
        }
    );
}

//Recursively searches for 'alias' named join
protected Optional<Join> findJoin(Set<Join> joins, String alias) {

    List<Join> joinList = new ArrayList<>(joins);

    for (Join j : joinList) {
        if (j.getAlias() != null && j.getAlias().equals(alias)) {
            return Optional.of(j);
        }
    }

    //  Breadth first search
    for (Join j : joinList) {
        Optional<Join> res = findJoin(j.getJoins(), alias);

        if (res.isPresent()) {
            return res;
        }
    }


    return Optional.empty();
}

Example Usage;

private Join<E, ExampleEntity> getOrCreateExampleEntityJoin(Root<E> mainRoot, String alias) {
    return getOrCreateCriteriaJoin(mainRoot, alias, (root, cb) -> root.join(ExampleEntity_.someFieldName));
}

specification = (root, query, criteriaBuilder) -> criteriaBuilder.equal(getOrCreateExampleEntityJoin(root, "exampleAlias").get(ExampleEntity_.someAnOtherField), "ExampleData");

I have slightly modified the implementation so that there is no need to copy-paste aliases and functions

abstract class ReusableJoinSpecification<T> implements Specification<T> {

protected <F extends From<FF, FR>, FF, FR, J extends Join<JF, JR>, JF, JR> J getOrCreateJoin(F from,
                                                                                             JoinData<F, J> joinData) {

    Set<Join<FR, ?>> joins = from.getJoins();
    //noinspection unchecked
    Optional<J> optionalJoin = (Optional<J>) findJoin(joins, joinData.getAlias());

    return optionalJoin.orElseGet(() -> {
                J join = joinData.getCreationFunction().apply(from);
                join.alias(joinData.getAlias());
                return join;
            }
    );
}

private Optional<Join<?, ?>> findJoin(@NotNull Set<? extends Join<?, ?>> joins, @NotNull String alias) {

    List<Join<?, ?>> joinList = new ArrayList<>(joins);

    for (Join<?, ?> join : joinList) {
        if (alias.equals(join.getAlias())) {
            return Optional.of(join);
        }
    }

    for (Join<?, ?> j : joinList) {
        Optional<Join<?, ?>> res = findJoin(j.getJoins(), alias);

        if (res.isPresent()) {
            return res;
        }
    }

    return Optional.empty();
}

JoinData:

@Data
class JoinData<F extends From<?, ?>, J extends Join<?, ?>> {
    @NotNull
    private final String alias;
    @NotNull
    private final Function<F, J> creationFunction;
}

Usage:

private final JoinData<Root<Project>, Join<Project, Contractor>> contractorJoinData =
        new JoinData<>("contractor", root -> root.join(Project_.contractor));

private final JoinData<Join<Project, Contractor>, Join<Contractor, Address>> contractorLegalAddressJoinData =
        new JoinData<>("contractorLegalAddress", root -> root.join(Contractor_.legalAddress));

public Specification<Project> contractorLegalAddressCityLike(String address) {
    if (address == null)
        return null;

    return new ReusableJoinSpecification<>() {
        @Override
        public Predicate toPredicate(Root<Project> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {

            Join<Project, Contractor> contractorJoin = getOrCreateJoin(root, contractorJoinData);
            Join<Contractor, Address> contractorAddressJoin = getOrCreateJoin(contractorJoin, contractorLegalAddressJoinData);

            return criteriaBuilder.like(contractorAddressJoin.get(Address_.city), simpleLikePattern(address));
        }
    };
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top