3

I am using Spring Data JPA as the persistence layer and I'm facing the N+1 problem. I'm also using the Specifications API because of which I'm finding difficult to solve N+1 problem. Please help.

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

In the below class, the root.join() method, I added this later, this creates the left join in the query but, N+1 problem still happens. Please refer to below log output:

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

Log Output with N+1 queries. The first query has a Left join But still there is N+1 problem.

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=?

After using the fetch() instead of a join() in the Specification class, I get below issue:

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}}]
Bharat Nanwani
  • 653
  • 4
  • 11
  • 27
  • But its just plain JPA so you can eg join fetch relation since `root` is avalilable to you so instead of `join` use `fetch`. I think that something else is processing the result (result serialization eg?) and that is causing relations to be initialized – Antoniossss May 06 '20 at 14:22
  • When I use fetch instead of a join, for some reason, in the Specification class, in the switch statement, GREATER_THAN condition breaks. It gives me the below error - org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [...] – Bharat Nanwani May 06 '20 at 14:33
  • As far I know you can't fetch child when you use specification and join doesn't mean it will fetch data. – Eklavya May 06 '20 at 15:06

1 Answers1

2

I don't think Spring-Data can do any better here since it tries to do a count query first in order to provide the total count information in the Page object. You could use Slice to avoid the count query.

If you want something more advanced you can take a look at the Blaze-Persistence integration with Spring-Data. It will use a different pagination mechanism that allows this to work and is also more efficient. Using Entity-Views will even give you an additional performance boost.

Christian Beikov
  • 15,141
  • 2
  • 32
  • 58