Подтвердить что ты не робот

Spring Данные JPA: создание спецификаций.

TL; DR: как вы реплицируете операции JPY Join-Fetch с использованием спецификаций в Spring Data JPA?

Я пытаюсь создать класс, который будет обрабатывать динамическое построение запросов для объектов JPA, используя Spring Data JPA. Для этого я определяю несколько методов, которые создают объекты Predicate (такие как предлагается в Spring Data JPA docs и в другом месте), а затем связывание их при отправке соответствующего параметра запроса. Некоторые из моих сущностей имеют отношения "один ко многим" с другими сущностями, которые помогают их описать, которые охотно извлекаются при запросе и объединении в коллекции или карты для создания DTO. Упрощенный пример:

@Entity
public class Gene {

    @Id 
    @Column(name="entrez_gene_id")
    privateLong id;

    @Column(name="gene_symbol")
    private String symbol;

    @Column(name="species")
    private String species;

    @OneToMany(mappedBy="gene", fetch=FetchType.EAGER) 
    private Set<GeneSymbolAlias> aliases;

    @OneToMany(mappedBy="gene", fetch=FetchType.EAGER) 
    private Set<GeneAttributes> attributes;

    // etc...

}

@Entity
public class GeneSymbolAlias {

    @Id 
    @Column(name = "alias_id")
    private Long id;

    @Column(name="gene_symbol")
    private String symbol;

    @ManyToOne(fetch=FetchType.LAZY) 
    @JoinColumn(name="entrez_gene_id")
    private Gene gene;

    // etc...

}

Параметры строки запроса передаются из класса Controller классу Service в качестве пар ключ-значение, где они обрабатываются и собираются в Predicates:

@Service
public class GeneService {

    @Autowired private GeneRepository repository;
    @Autowired private GeneSpecificationBuilder builder;

    public List<Gene> findGenes(Map<String,Object> params){
        return repository.findAll(builder.getSpecifications(params));
    }

    //etc...

}

@Component
public class GeneSpecificationBuilder {

    public Specifications<Gene> getSpecifications(Map<String,Object> params){
        Specifications<Gene> = null;
        for (Map.Entry param: params.entrySet()){
            Specification<Gene> specification = null;
            if (param.getKey().equals("symbol")){
                specification = symbolEquals((String) param.getValue());
            } else if (param.getKey().equals("species")){
                specification = speciesEquals((String) param.getValue());
            } //etc
            if (specification != null){
               if (specifications == null){
                   specifications = Specifications.where(specification);
               } else {
                   specifications.and(specification);
               }
            }
        } 
        return specifications;
    }

    private Specification<Gene> symbolEquals(String symbol){
        return new Specification<Gene>(){
            @Override public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder builder){
                return builder.equal(root.get("symbol"), symbol);
            }
        };
    }

    // etc...

}

В этом примере каждый раз, когда я хочу получить запись Gene, я также хочу иметь связанные записи GeneAttribute и GeneSymbolAlias. Все это работает так, как ожидалось, и запрос на один Gene будет сбрасывать 3 запроса: каждый из них соответствует таблицам Gene, GeneAttribute и GeneSymbolAlias.

Проблема заключается в том, что нет причин, по которым необходимо запросить 3 запроса, чтобы получить единственный объект Gene со встроенными атрибутами и псевдонимами. Это можно сделать в простом SQL, и это можно сделать с помощью JPQL-запроса в моем репозитории данных JPA Spring:

@Query(value = "select g from Gene g left join fetch g.attributes join fetch g.aliases where g.symbol = ?1 order by g.entrezGeneId")
List<Gene> findBySymbol(String symbol);

Как я могу реплицировать эту стратегию выбора с помощью спецификаций? Я нашел этот вопрос здесь, но он только кажется, делает ленивые выборки в нетерпеливые выборки.

4b9b3361

Ответ 1

Класс спецификации:

public class MatchAllWithSymbol extends Specification<Gene> {
    private String symbol;

    public CustomSpec (String symbol) {
    this.symbol = symbol;
    }

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

        //This part allow to use this specification in pageable queries
        //but you must be aware that the results will be paged in   
        //application memory!
        Class clazz = query.getResultType();
        if (clazz.equals(Long.class) || clazz.equals(long.class))
            return null;

        //building the desired query
        root.fetch("aliases", JoinType.LEFT);
        root.fetch("attributes", JoinType.LEFT);
        query.distinct(true);        
        query.orderBy(cb.asc(root.get("entrezGeneId")));
        return cb.equal(root.get("symbol"), symbol);
    }
}

Использование:

    List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol"));

Ответ 2

Вы можете указать выбор соединения при создании Спецификации, но поскольку эта же спецификация будет использоваться также с помощью методов, например findAll (спецификация var1, pageable var2) и запрос count будут жаловаться из-за подключения. Поэтому, чтобы обработать это, мы можем проверить resultType of CriteriaQuery и применить join, только если он не длинный (тип результата для запроса count). см. ниже код:

    public static Specification<Item> findByCustomer(Customer customer) {
    return (root, criteriaQuery, criteriaBuilder) -> {
        /*
            Join fetch should be applied only for query to fetch the "data", not for "count" query to do pagination.
            Handled this by checking the criteriaQuery.getResultType(), if it long that means query is
            for count so not appending join fetch else append it.
         */
        if (Long.class != criteriaQuery.getResultType()) {
            root.fetch(Person_.itemInfo.getName(), JoinType.LEFT);
        }
        return criteriaBuilder.equal(root.get(Person_.customer), customer);
    };
}

Ответ 3

Я предлагаю эту библиотеку для спецификации. https://github.com/tkaczmarzyk/specification-arg-resolver

Из этой библиотеки: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch

Вы можете использовать аннотацию @JoinFetch, чтобы указать пути для выполнения соединения fetch. Например:

@RequestMapping("/customers")
public Object findByOrderedOrFavouriteItem(
        @Joins({
            @Join(path = "orders", alias = "o")
            @Join(path = "favourites", alias = "f")
        })
        @Or({
            @Spec(path="o.itemName", params="item", spec=Like.class),
            @Spec(path="f.itemName", params="item", spec=Like.class)}) Specification<Customer> customersByItem) {

    return customerRepo.findAll(customersByItem);
}