spring-boothibernatespring-data-jpa

Why hibernate 6 throw an excetion "java.lang.IllegalArgumentException: Already registered a copy: SqmBasicValuedSimplePath" when adding a predicate?


Here is my code:

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

import com.abned.forms.GetAllForm;
import com.abned.forms.PaginationForm;
import com.abned.SortingForm;
import com.abned.forms.SortingForm.SortingDirection;

public abstract class GetAllService<T> {
    private final EntityManager em;
    public GetAllService(EntityManager em) {
        this.em = em;
    }

    public GetAllResponse<T> getAll(GetAllForm form) {
        final CriteriaBuilder cb = em.getCriteriaBuilder();
        final CriteriaQuery<T> cq = cb.createQuery(getEntityClass());
        final Root<T> query = buildRootQuery(cq);
        final List<Predicate> predicates = buildPredicates(cb, query, form);

        if (!predicates.isEmpty()) {
            cq.where(predicates.toArray(new Predicate[predicates.size()]));
        }
        cq.distinct(true);
        cq.orderBy(buildOrderBy(cb, query, form));
        
        final TypedQuery<T> typedQuery = em.createQuery(cq);
        buildPagination(form.getPagination(), typedQuery);
        addAdditionalBuiltins(form, typedQuery);
    
        final List<T> results = typedQuery.getResultList();
        return new GetAllResponse<T>(results, nbTotal(predicates));
    }

    protected Root<T> buildRootQuery(CriteriaQuery<T> cq) {
        return cq.from(getEntityClass());
    }

    protected Long nbTotal(final List<Predicate> predicates) {
        final CriteriaBuilder cb = em.getCriteriaBuilder();
        final CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
        final Root<T> query = countQuery.from(getEntityClass());
        if (!predicates.isEmpty()) {
            countQuery.where(predicates.toArray(new Predicate[0]));
        }
        final List<Long> totals = em.createQuery(countQuery.select(cb.countDistinct(query))).getResultList();
        if (!totals.isEmpty()) {
            return totals.get(0);
        }
        return 0L;
    }

    protected void buildPagination(PaginationForm pagination, TypedQuery<T> typedQuery) {
        if (pagination == null) {
            return;
        }
        if (null != pagination.getLimit() && pagination.getLimit() > 0) {
            if (null != pagination.getPage() && pagination.getPage() > 0) {
                typedQuery.setFirstResult((pagination.getPage().intValue() - 1) * pagination.getLimit());
            }
            typedQuery.setMaxResults(pagination.getLimit());
        }
    }

    protected void addAdditionalBuiltins(GetAllForm form, TypedQuery<T> typedQuery) {}

    protected abstract List<Predicate> buildPredicates(CriteriaBuilder cb, Root<T> query, GetAllForm form);
    protected abstract Class<T> getEntityClass();

    protected Order[] buildOrderBy(CriteriaBuilder cb, Root<T> query, GetAllForm form) {
        final List<Order> orders = new ArrayList<>();
        final SortingForm orderBy = form.getSorting();
        if (orderBy != null) {
            final Expression<? extends Serializable> order = getOrderByExpression(query, orderBy);
            if (orderBy.getDir() == null || orderBy.getDir() == SortingDirection.ASC) {
                orders.add(cb.asc(order));
            } else {
                orders.add(cb.desc(order));
            }
        }
        return orders.toArray(new Order[orders.size()]);
    }

    protected Expression<? extends Serializable> getOrderByExpression(Root<T> query, SortingForm orderBy) {
        return query.get(orderBy.getColumn());
    }
}

And the concrete class that extends it:

import org.springframework.stereotype.Service;

import com.abned.entities.ExtractionTable;
import com.abned.forms.GetAllExtractionTableForm;
import com.abned.forms.GetAllForm;

import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

@Service
public class GetAllExtractionTable extends GetAllService<ExtractionTable> {
    public GetAllExtractionTable(EntityManager em) {
        super(em);
    }

    @Override
    protected List<Predicate> buildPredicates(CriteriaBuilder cb, Root<ExtractionTable> query, GetAllForm form) {
        final List<Predicate> predicates = new ArrayList<>();
        if (form instanceof GetAllExtractionTableForm) {
            final GetAllExtractionTableForm criteria = (GetAllExtractionTableForm) form;
            if (null != criteria.getOnglet()) {
                predicates.add(cb.equal(query.get("onglet"), criteria.getOnglet()));
            }
            if (null != criteria.getType()) {
                predicates.add(cb.equal(query.get("type"), criteria.getType()));
            }
            if (null != criteria.getIds() && !criteria.getIds().isEmpty()) {
                predicates.add(query.get("id").in(criteria.getIds()));
            }
        }
        return predicates;
    }

    @Override
    protected Class<ExtractionTable> getEntityClass() {
        return ExtractionTable.class;
    }
}

So, why nbTotal function throw an exception java.lang.IllegalArgumentException: Already registered a copy: SqmBasicValuedSimplePath(com.louis.scrapping.api.entities.ExtractionTable(6762665638604).onglet) ?

With hibernate 5.6 (Spring boot 2.7), no exception was thrown.

Edit: 20 Mey 2023 - my solution

As @ChristianBeikov explain in comment, with hibernate 6, we can't reuse the built predicates from a query to another query.

So, my solution is to rebuild the predicate by using the new query for nbTotal method. nbTotal function is now like this:

protected Long nbTotal(GetAllForm form) {
        final CriteriaBuilder cb = em.getCriteriaBuilder();
        final CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
        final Root<T> query = countQuery.from(getEntityClass());
        final List<Predicate> predicates = buildPredicates(cb, query, form);
        if (!predicates.isEmpty()) {
            countQuery.where(predicates.toArray(new Predicate[0]));
        }
        final List<Long> totals = em.createQuery(countQuery.select(cb.countDistinct(query))).getResultList();
        if (!totals.isEmpty()) {
            return totals.get(0);
        }
        return 0L;
    }

Solution

  • You will need something like this instead:

    public class JpaUtil {
    
        public static <E, D> long count(final EntityManager entityManager,
                                        final CriteriaBuilder cb,
                                        final CriteriaQuery<D> criteria,
                                        final Root<E> root) {
            CriteriaQuery<Long> query = createCountQuery(cb, criteria, root, root.getModel().getJavaType());
            return entityManager.createQuery(query).getSingleResult();
        }
    
        private static <T, D> CriteriaQuery<Long> createCountQuery(final CriteriaBuilder cb,
                                                                   final CriteriaQuery<D> criteria,
                                                                   final Root<T> root,
                                                                   final Class<T> entityClass) {
    
            final CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
            final SqmCopyContext copyContext = SqmCopyContext.simpleContext();
            final Root<T> countRoot = countQuery.from(entityClass);
            copyContext.registerCopy(root, countRoot);
    
            doJoins(root.getJoins(), countRoot, copyContext);
            doJoinsOnFetches(root.getFetches(), countRoot, copyContext);
    
            countQuery.select(cb.count(countRoot));
            countQuery.where(((SqmPredicate) criteria.getRestriction()).copy(copyContext));
    
            countRoot.alias(root.getAlias());
    
            return countQuery.distinct(criteria.isDistinct());
        }
    
        @SuppressWarnings("unchecked")
        private static void doJoinsOnFetches(Set<? extends Fetch<?, ?>> joins, Root<?> root, SqmCopyContext copyContext) {
            doJoins((Set<? extends Join<?, ?>>) joins, root, copyContext);
        }
    
        private static void doJoins(Set<? extends Join<?, ?>> joins, Root<?> root, SqmCopyContext copyContext) {
            for (Join<?, ?> join : joins) {
                Join<?, ?> joined = root.join(join.getAttribute().getName(), join.getJoinType());
                joined.alias(join.getAlias());
                copyContext.registerCopy(join, joined);
                doJoins(join.getJoins(), joined);
            }
        }
    
        private static void doJoins(Set<? extends Join<?, ?>> joins, Join<?, ?> root, SqmCopyContext copyContext) {
            for (Join<?, ?> join : joins) {
                Join<?, ?> joined = root.join(join.getAttribute().getName(), join.getJoinType());
                joined.alias(join.getAlias());
                copyContext.registerCopy(join, joined);
                doJoins(join.getJoins(), joined);
            }
        }
    }