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
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 jpaDans 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