组合弹簧数据规范的查询在同一个表上有多个连接

时间:2014-02-15 00:23:26

标签: spring jpa spring-data spring-data-jpa

很抱歉,如果我的术语不正确。

我们使用spring数据,JpaRepositories和条件查询作为查询数据库数据的方法。

我有一个问题,当我在下面的代码示例中结合两个规范,例如我在hasCityAndTimeZone中使用hasTimeZone和hasCity时,它会在同一个表上连接两次,所以下面的查询看起来像

select * from Staff, Location, Location

有没有办法让两个规范使用相同的连接而不是每个连接定义它们本身相同的连接?

对不起,代码可能不完整我只是想展示一个简单的例子。

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));
    }
}

5 个答案:

答案 0 :(得分:12)

不幸的是没有开箱即用的方式。 Spring Data在内部使用QueryUtils.getOrCreateJoin(…)内的一些连接重用。您可以在根上找到可能已存在的连接,并在适当的地方重用它们:

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);
}

请注意,这只有在我们有效地知道自己添加哪些联接时才有效。使用Specifications时你也应该这样做,但我只是想确保没有人认为这是所有情况的一般解决方案。

答案 1 :(得分:3)

基于@Oliver回答,我创建了Specification接口

的扩展名

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);
  }

}

如何使用

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);
        }
    }
}

答案 2 :(得分:0)

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);
}

在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() + "%");

...

答案 3 :(得分:0)

这是个老问题,但我为有此问题的人写了这个答案。

如果没有使用该别名的联接,我实现了一种通用方法来搜索联接别名,然后使用给定的pathFunction创建联接。

正如奥利弗(Oliver)所说的,您可以使用定义的联接,但是如果实体有多个联接,则需要知道定义的联接的别名。

//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();
}

示例用法;

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");

答案 4 :(得分:0)

我稍微修改了实现,以便不需要复制粘贴别名和函数

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();
}

加入数据:

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

用法:

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));
        }
    };
}