web-dev-qa-db-ja.com

Spring Data JPA:仕様クエリフェッチ結合の作成

TL; DR:Spring Data JPAの仕様を使用してJPQL Join-Fetch操作をどのように複製しますか?

Spring Data JPAを使用して、JPAエンティティの動的クエリ構築を処理するクラスを構築しようとしています。これを行うために、Predicateオブジェクト( Spring Data JPA docs などで推奨されている)を作成するいくつかのメソッドを定義し、適切なクエリ時にそれらをチェーンしますパラメータが送信されます。私のエンティティの中には、他のエンティティと1対多の関係を持っているものがあります。これらのエンティティは、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つのクエリを起動します:GeneGeneAttribute、およびGeneSymbolAliasテーブルに対する各クエリ。

問題は、属性とエイリアスが埋め込まれた単一のGeneエンティティを取得するために3つのクエリを実行する必要がない理由です。これはプレーンSQLで実行でき、Spring Data JPAリポジトリのJPQLクエリを使用して実行できます。

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

仕様を使用してこのフェッチ戦略を複製するにはどうすればよいですか? この質問はこちら を見つけましたが、遅延フェッチを熱心なフェッチにしかしないようです。

25
woemler

仕様クラス:

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

仕様の作成中に結合フェッチを指定できますが、同じ仕様がfindAll(Specification var1、Pageable var2)などのページング可能なメソッドによって使用されるため、カウントクエリは結合フェッチのために文句を言います。したがって、それを処理するために、CriteriaQueryのresultTypeを確認し、Long(カウントクエリの結果タイプ)でない場合にのみ結合を適用できます。以下のコードを参照してください:

    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's 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);
    };
}
6
suraj bahl

このライブラリを仕様に提案します。 https://github.com/tkaczmarzyk/specification-arg-resolver

このライブラリから: https://github.com/tkaczmarzyk/specification-arg-resolver#join-fetch

@ JoinFetchアノテーションを使用して、フェッチ結合を実行するパスを指定できます。例えば:

@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);
}
3
kafkas