Spring Data JPA:创建规范查询Fetch Joins

50

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

我正在尝试构建一个类,使用Spring Data JPA处理JPA实体的动态查询。为此,我定义了一些方法,创建Predicate对象(如Spring Data JPA文档和其他地方建议的那样),并在适当的查询参数提交时链接它们。我的一些实体与其他实体具有一对多的关系,这些实体在查询时会被急切地获取,并被合并到集合或映射中以便于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记录时,我也想要它相关的GeneAttributeGeneSymbolAlias记录。这一切都按预期工作,对单个Gene的请求将触发3个查询:分别查询GeneGeneAttributeGeneSymbolAlias表。
问题在于获取单个带有嵌入属性和别名的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);

我该如何使用规范来复制这个获取策略?我在这里找到了这个问题, 但它似乎只能将惰性获取转换为急切获取。

你尝试在 toPredicate() 内部使用 root.fetch() 了吗?像这样:root.fetch("attributes", JoinType.LEFT) - Predrag Maric
@PredragMaric:这将热切地获取“attributes”,但仍需要额外的查询。我希望所有的获取都是单个查询的一部分。 - woemler
是的,但是另一个针对aliases的获取应该可以解决它:root.fetch("aliases", JoinType.LEFT) - Predrag Maric
我之前尝试过这个方法,就像我在链接的问题中提到的那样,但它并没有达到预期的结果。问题不在于无法使用单个规范查询获取链接实体,而是单个规范查询需要3个SQL查询来获取这些实体,这完全是不必要的。 - woemler
我没有完全理解?你需要的确切是什么,需要编写规范来列出基因实体及其别名和属性集合吗? 如果您需要带规格的基因列表,那么我可以为您提供适当的解决方案。 - Harshal Patil
我编写了一个库,用于在实体上使用参数进行搜索,它使用参数对象创建HQL查询。我使用参数类而不是哈希映射。https://github.com/ekremucar/hqlplus/ 如果您设置参数对象的属性,则会将其添加到where子句中。您可以设置搜索类型(如,eq)并设置别名的获取类型。 - Ekrem
2个回答

33

你可以在创建Specification时指定join fetch,但由于相同的Specification也将被分页方法使用,例如findAll(Specification var1, Pageable var2),并且计数查询会因为join fetch而出现问题。因此,为了处理这个问题,我们可以检查CriteriaQuery的resultType,并仅在它不是Long(计数查询的结果类型)时应用join。请参见下面的代码:

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

这个答案拯救了我的灵魂。 - Jin Kwon

33

规格类别:

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
如何使规范与SDJPA可分页查询配合使用的好技巧。+1。 - Mariano D'Ascanio
1
它可以工作,但问题在于Hibernate在内存中进行分页,在控制台上我看到了这条消息“HHH000104:使用集合获取指定了firstResult/maxResults;正在应用于内存中!” 我尝试使用fetch join和entity graph来解决它,但我没有找到完美的解决方案。 - hicham abdedaime
Hibernate是否执行内存操作是由于当它将规范用作计数查询时,谓词为空吗?对于此问题的另一个答案不会返回null,而是在被调用为计数查询时避免应用连接。对我来说,这看起来更安全,但我并不确定这是否可以解决内存分页问题。我不是Hibernate专家,这只是我的猜测 ^^ - LostMekkaSoft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接