Запрос из комбинированной спецификации данных Spring имеет несколько соединений в одной таблице

Извините, если моя терминология неверна.

Мы используем данные весны, JpaRepositories и критериальные запросы в качестве нашего метода для запроса данных из нашей базы данных.

У меня есть проблема, когда я объединяю две спецификации, такие как hasTimeZone и hasCity в hasCityAndTimeZone в приведенном ниже примере кода, он дважды выполняет соединение с одной и той же таблицей, поэтому приведенный ниже запрос будет выглядеть примерно так:

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

person Dan King    schedule 15.02.2014    source источник


Ответы (5)


К сожалению, нестандартного способа нет. 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 вы также должны это сделать, но я просто хочу убедиться, что никто не считает это общим решением для всех случаев.

person Oliver Drotbohm    schedule 17.02.2014

Основываясь на ответе @Oliver, я создал расширение для интерфейса Specification.

Присоединяемая спецификация.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);
        }
    }
}
person oak    schedule 02.05.2016
comment
Интересно, но я не понимаю, где вы используете методы joinList()... - person maxxyme; 09.12.2016
comment
@maxxyme, ты прав. У меня опечатка. должно быть this.joinList - person oak; 18.12.2016

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

....

person Flavio Troia    schedule 31.01.2018

Это старый вопрос, но я написал этот ответ для людей, у которых есть эта проблема.

Я реализовал общий метод для поиска псевдонима соединения, если нет соединения с этим псевдонимом, тогда он создает соединение с заданным pathFunction.

Как сказал Оливер, вы можете использовать определенные соединения, но если у вас есть несколько соединений для объекта, вам нужно знать псевдоним вашего определенного соединения.

//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");
person Burak Akyıldız    schedule 09.10.2019

Я немного модифицировал реализацию, чтобы не было необходимости копипастить псевдонимы и функции

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));
        }
    };
}
person Дмитрий Коваленко    schedule 02.08.2021