如何在Spring Data JPA中解决N+1问题?

4

我正在使用Spring Data JPA作为持久层,但是我遇到了N+1问题。同时我也在使用Specifications API,因此我发现很难解决N+1问题。请帮助。

@Entity
public class PopulationHealth {

    @Id
    private int caseId;

    @OneToMany(mappedBy = "caseId", fetch = FetchType.LAZY)
    private List<CostSaving> costSavings;
}

public class CostSaving {

    @Id
    private int caseId;
}

@Transactional(readOnly = true)
public interface PopulationHealthRepository extends JpaRepository<PopulationHealth, Integer>, JpaSpecificationExecutor<PopulationHealth> {

    Page<PopulationHealth> findAll(Specification<PopulationHealth> spec, Pageable pageable);
}

在下面的类中,我后来添加了root.join()方法,这将在查询中创建左连接,但N+1问题仍然存在。请参考以下日志输出:
public class PopulationHealthSearchSpec implements Specification<PopulationHealth> {

    private List<PopulationHealthCriteriaDto> criteria;

    public PopulationHealthSearchSpec() {
        criteria = new ArrayList<>();
    }

    public void addCriteria(List<PopulationHealthCriteriaDto> criteria) {
        this.criteria.addAll(criteria);
    }

    @Override
    public Predicate toPredicate(Root<PopulationHealth> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        List<Predicate> predicates = new ArrayList<>();
        criteria.forEach(p -> {
            SearchOperation operation = p.getOperation();
            root.join("costSavings", JoinType.LEFT);
            switch (operation) {
                case GREATER_THAN:
                    predicates.add(cb.greaterThan(root.get(p.getKey().toString()), convertToDate(p.getValue())));
                    break;
                case GREATER_THAN_EQUAL:
                    predicates.add(cb.greaterThanOrEqualTo(root.get(p.getKey().toString()), convertToDate(p.getValue())));
                    break;
                case LESS_THAN:
                    predicates.add(cb.lessThan(root.get(p.getKey().toString()), convertToDate(p.getValue())));
                    break;
                case LESS_THAN_EQUAL:
                    predicates.add(cb.lessThanOrEqualTo(root.get(p.getKey().toString()), convertToDate(p.getValue())));
                    break;
                case EQUAL:
                    predicates.add(cb.equal(root.get(p.getKey().toString()), p.getValue()));
                    break;
                case NOT_EQUAL:
                    predicates.add(cb.notEqual(root.get(p.getKey().toString()), p.getValue()));
                    break;
                case IN:
                    predicates.add(getInPredicates(cb, root, p));
                    break;
                case NOT_IN:
                    predicates.add(getInPredicates(cb, root, p).not());
                    break;
            }
        });
        return cb.and(predicates.toArray(new Predicate[0]));
    }

使用N+1查询记录输出。第一次查询使用了左连接,但仍存在N+1问题。

2020-05-06 19:46:41,527 DEBUG org.hibernate.SQL : select population0_.CaseId as CaseId1_3_, population0_.CaseCreateDate as CaseCrea2_3_ from POPULATION_HEALTH_UNMASKED population0_ left outer join COST_SAVINGS costsaving1_ on population0_.CaseId=costsaving1_.CaseId where population0_.CaseId in (3098584 , 3098587 , 3098591) order by population0_.CaseCreateDate asc limit ?
2020-05-06 19:46:41,709 DEBUG org.hibernate.SQL : select costsaving0_.CaseId as CaseId1_1_0_, costsaving0_.CaseId as CaseId1_1_1_ from COST_SAVINGS costsaving0_ where costsaving0_.CaseId=?
2020-05-06 19:46:41,744 DEBUG org.hibernate.SQL : select costsaving0_.CaseId as CaseId1_1_0_, costsaving0_.CaseId as CaseId1_1_1_ from COST_SAVINGS costsaving0_ where costsaving0_.CaseId=?
2020-05-06 19:46:41,781 DEBUG org.hibernate.SQL : select costsaving0_.CaseId as CaseId1_1_0_, costsaving0_.CaseId as CaseId1_1_1_ from COST_SAVINGS costsaving0_ where costsaving0_.CaseId=?

在Specification类中使用fetch()而不是join()后,我遇到了以下问题:
2020-05-06 20:29:25,315 ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.cambia.mmt.cdqs.api.entity.PopulationHealth.costSavings,tableName=COST_SAVINGS,tableAlias=costsaving1_,origin=POPULATION_HEALTH_UNMASKED population0_,columns={population0_.CaseId ,className=com.cambia.mmt.cdqs.api.entity.CostSaving}}] [select count(generatedAlias0) from com.cambia.mmt.cdqs.api.entity.PopulationHealth as generatedAlias0 left join fetch generatedAlias0.costSavings as generatedAlias1 where generatedAlias0.caseCreateDate>:param0]; nested exception is java.lang.IllegalArgumentException: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.cambia.mmt.cdqs.api.entity.PopulationHealth.costSavings,tableName=COST_SAVINGS,tableAlias=costsaving1_,origin=POPULATION_HEALTH_UNMASKED population0_,columns={population0_.CaseId ,className=com.cambia.mmt.cdqs.api.entity.CostSaving}}] [select count(generatedAlias0) from com.cambia.mmt.cdqs.api.entity.PopulationHealth as generatedAlias0 left join fetch generatedAlias0.costSavings as generatedAlias1 where generatedAlias0.caseCreateDate>:param0]] with root cause
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias1,role=com.cambia.mmt.cdqs.api.entity.PopulationHealth.costSavings,tableName=COST_SAVINGS,tableAlias=costsaving1_,origin=POPULATION_HEALTH_UNMASKED population0_,columns={population0_.CaseId ,className=com.cambia.mmt.cdqs.api.entity.CostSaving}}]

但这只是普通的JPA,因此您可以加入关系,例如使用fetch而不是join,因为root对您可用。我认为还有其他东西正在处理结果(例如结果序列化?),这会导致关系被初始化。 - Antoniossss
当我使用fetch而不是join时,在Specification类中的switch语句中,GREATER_THAN条件会出现问题。它会给我以下错误 - org.hibernate.QueryException:查询指定了联接获取,但获取的关联所有者未在选择列表中出现[...]。 - Bharat Nanwani
据我所知,当您使用规范时,无法获取子级,并且连接并不意味着它将获取数据。 - Eklavya
1个回答

2
我认为Spring-Data在这里无法做得更好,因为它首先尝试执行计数查询以提供Page对象中的总计数信息。您可以使用Slice来避免计数查询。
如果您想要更高级的东西,可以看一下Blaze-Persistence integration with Spring-Data。它将使用不同的分页机制,使其能够工作并且效率更高。使用Entity-Views甚至还可以为您提供额外的性能提升。

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