mercredi 17 mai 2023

Spring data jpa et Hibernate’s Warning “firstResult/maxResults specified with collection fetch”

Nous verrons dans cet article comment corrigé le problème 
Hibernate’s Warning “firstResult/maxResults specified with collection fetch” qui peut survenir lorsqu'il y a un fetch d'effectuer et qu'il y a du paging. Nous utiliserons spring data jpa.

Détail structure

Imaginons une relation 1 à plusieurs


Lorsqu'on obtient un parent, on désire obtenir aussi sa liste d'enfants. En sql, il y aurait une jointure à effectuer. Il est question d'une recherche avancé où on pourrait recherche par plusieurs critères.

Spécification

Création d'une spécification pour construire une requête dynamique

@Component
public class ParentSpecification {

    public Specification<Parent> recherche(RechercheParent recherche) {
        return (root, cq, cb) -> {
            Join<Parent, enfant> enfantJoin;

             List<Predicate> rootPredicates = new ArrayList<>();

             if (Long.class != cq.getResultType()) {
                enfantJoin = (Join) root.fetch("enfants", JoinType.LEFT);
             } else {
                enfantJoin = (Join) root.join("enfants", JoinType.LEFT);
             }

             sqlLikeCondition("nom", recherche.nomParent(), root, cb, rootPredicates);
             sqlEqualCondition("age", recherche.age(), root, cb, rootPredicates);
             return  cb.and(rootPredicates.toArray(new Predicate[rootPredicates.size()]));

    }

}

Repository

Spring data Jpa a une méthode findAll qui accepte en paramètre une spécification. Il y a même une version de la méthode qui accepte aussi un objet Pageable. Afin de pouvoir l'utiliser, le repository dois hérité de JpaSpecificationExecutor.

@Repository
@Transactional(readOnly = true)
public interface ParentRepository extends JpaRepository<Parent, Long>, JpaSpecificationExecutor<Parent> {
}


Il sera alors possible d'effectuer un appel comme celui çi

 Page<Parent> pageParent = parentRepository.findAll(parentSpecification.search(parentRecherche), pageable);

Détail du problème

Par contre, dans les logs il y aura le message org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory


Dans la base de donnée avec une requête sql le résultat serait par exemple
Chaque enfant est relié à son parent.

Habituellement, Hibernate, le orm par défaut utilisé par spring boot, lorsque le paging est utilisé il va prendre x première valeurs jusqu'à y. Par exemple si on mentionne qu'on veut les 5 premières résultats on obtiendrait


Ce qui est pas vraiment ce qu'on voudrait, mais bien les 5 premiers parents.

Hibernate donne le message “firstResult/maxResults specified with collection fetch” car il applique alors la pagination en mémoire sans appliquer la pagination et ensuite retourne le résult voulue.
Si votre table à énormément de donnée, vous pouvez obtenir rapidement des problèmes de performance, surtout si vous effectuer des rêquetes sans aucun critères de recherche.

Solution

Afin de palier à ce problème, nous allons redéfinir une méthode spring data jpa

Dans la classe de spécification, il faudra ajouter

public Specification<Parent> idIn(Set<Long> parentIds) {
        return (root, cq, cb) -> {

            if (parentIds == null || parentIds.isEmpty()) {
                return null;
            }

            return root.get("parentId").in(parentIds);

        };
    }

Il faut créer une nouvelle interface avec la méthode findEntityIds

@NoRepositoryBean
public interface CustomJpaSpecificationExecutor<E, ID extends Serializable> extends JpaSpecificationExecutor<E> {

    Page<ID> findEntityIds(Specification<E> specification, Pageable pageable);
}

Par la suite il faut créer une classe implémentant cette interface


public class CustomBaseSimpleJpaRepository<E, ID extends Serializable> extends SimpleJpaRepository<E, ID> implements CustomJpaSpecificationExecutor<E, ID> {

    private final EntityManager entityManager;
    private final JpaEntityInformation<E, ID> entityInformation;

    public CustomBaseSimpleJpaRepository(JpaEntityInformation<E, ID> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityManager = entityManager;
        this.entityInformation = entityInformation;
    }

    private static long executeCountQuery(TypedQuery<Long> query) {

        Assert.notNull(query, "TypedQuery must not be null");

        List<Long> totals = query.getResultList();
        long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }

        return total;
    }

    @Override
    public Page<ID> findEntityIds(Specification<E> specification, Pageable pageable) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<ID> criteriaQuery = criteriaBuilder.createQuery(entityInformation.getIdType());
        Root<E> root = criteriaQuery.from(getDomainClass());

        // Get the entities ID only
        criteriaQuery.select((Path<ID>) root.get(entityInformation.getIdAttribute()));

        // Apply specification(s) to restrict results or to write dynamic queries
        Predicate predicate = specification.toPredicate(root, criteriaQuery, criteriaBuilder);

        if (predicate != null) {
            criteriaQuery.where(predicate);
        }

        // Update Sorting
        Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
        if (sort.isSorted()) {
            criteriaQuery.orderBy(toOrders(sort, root, criteriaBuilder));
        }
        TypedQuery<ID> typedQuery = entityManager.createQuery(criteriaQuery);

        // Update Pagination attributes
        if (pageable.isPaged()) {
            typedQuery.setFirstResult((int) pageable.getOffset());
            typedQuery.setMaxResults(pageable.getPageSize());
        }

        return PageableExecutionUtils.getPage(typedQuery.getResultList(), pageable,
                () -> this.executeCountQuery(getCountQuery(specification, getDomainClass())));

    }

}

Il faudra faire deux appels

Page<Long> pageParentId = parentRepository.findEntityIds(parentSpecification.search(parentRecherche), pageable);

List<Parent> parents = parentRepository.findAll(parentSpecification.search(parentRecherche).and(parentSpecification.idIn(Set.copyOf(pageParentId.getContent()))));

return new PageImpl<>(parents, pageable, pageParentId.getTotalElements());

Spring data jpa lancera 3 requêtes.

Une première requête pour obtenir les id des parents.
Une autre requête pour obtenir le nombre total de donnée 
Une troisième requête nécessitant les id de la première requête sera effectué



Aucun commentaire:

Enregistrer un commentaire