Spring Data JPA:创建规范查询提取连接

TL;DR:如何使用 Spring Data JPA 中的规范来复制 JPQL Join-Fetch 操作?

我正在尝试构建一个类,该类将使用Spring Data JPA处理JPA实体的动态查询构建。为此,我定义了许多创建对象的方法(例如Spring Data JPA文档和其他地方的建议),然后在提交适当的查询参数时将它们链接起来。我的一些实体与其他实体具有一对多关系,这些实体有助于描述它们,当查询并合并到集合或映射以创建DTO时,这些实体会被热切地获取。一个简化的例子:Predicate

@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...

}

查询字符串参数作为键值对从类传递到类,并在其中处理并组装成:ControllerServicePredicates

@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...

}

在此示例中,每次要检索记录时,我还希望将其关联和记录。这一切都按预期工作,对单个的请求将触发 3 个查询:分别针对 、 和 表。GeneGeneAttributeGeneSymbolAliasGeneGeneGeneAttributeGeneSymbolAlias

问题是,没有理由需要运行3个查询才能获得具有嵌入式属性和别名的单个实体。这可以在普通的SQL中完成,也可以在我的Spring Data JPA存储库中使用JPQL查询来完成:Gene

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

如何使用规范复制此获取策略?我在这里发现了这个问题,但它似乎只会将懒惰的抓取变成急切的抓取。


答案 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(Specification var1,Pageable var2))将使用相同的规范,并且计数查询会因为联接获取而抱怨。因此,为了处理这个问题,我们可以检查 CriteriaQuery 的结果类型,并仅在它不长时才应用联接(计数查询的结果类型)。请参阅下面的代码:

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

推荐