speedment / jpa-streamer

JPAstreamer is a lightweight library for expressing JPA queries as Java Streams
GNU Lesser General Public License v2.1
345 stars 35 forks source link

Illegal reuse of criteria for count()-operator #352

Closed julgus closed 1 year ago

julgus commented 1 year ago

Describe the bug When performing repeated .count() queries, an exception is thrown. This seems to be related to differences between Hibernate 5 and 6, and how JPAStreamer reuses certain Criteria objects when creating the queries.

Expected behavior The query should execute without exceptions.

Actual behavior The query yields an exception, see below.

How To Reproduce Performing the following test reveals the exception:

    @Test
    void countTest() {
        final List<Film> collect = jpaStreamer.stream(Film.class).collect(Collectors.toList());

        final long expected = collect.stream()
                .filter(f -> f.getTitle().startsWith("A"))
                .count(); 

        final long actual = jpaStreamer.stream(Film.class)
                .filter(Film$.title.startsWith("A"))
                .count(); 

        assertEquals(expected, actual);
    }

Exception:

java.lang.IllegalArgumentException: Already registered a copy: SqmBasicValuedSimplePath(com.speedment.jpastreamer.integration.test.model.Film(1295155256344291).title)

    at org.hibernate.query.sqm.tree.SqmCopyContext$1.registerCopy(SqmCopyContext.java:33)
    at org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath.copy(SqmBasicValuedSimplePath.java:53)
    at org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath.copy(SqmBasicValuedSimplePath.java:26)
    at org.hibernate.query.sqm.tree.predicate.SqmLikePredicate.copy(SqmLikePredicate.java:90)
    at org.hibernate.query.sqm.tree.predicate.SqmLikePredicate.copy(SqmLikePredicate.java:19)
    at org.hibernate.query.sqm.tree.predicate.SqmWhereClause.copy(SqmWhereClause.java:33)
    at org.hibernate.query.sqm.tree.select.SqmQuerySpec.copy(SqmQuerySpec.java:104)
...

Build tool e.g. Maven 3.9.0

JPAStreamer version e.g. JPAStreamer 3.0.2

JPA Provider e.g. Hibernate 6.0.2.Final

Java Version e.g. Java 11.0.17

Additional context The thread that potentially explains the issue: https://hibernate.atlassian.net/browse/HHH-15951?jql=project%20%3D%20HHH%20AND%20component%20%3D%20hibernate-entitymanager%20AND%20resolution%20%3D%20Unresolved%20AND%20fixVersion%20is%20EMPTY%20ORDER%20BY%20priority%20DESC.

julgus commented 1 year ago

The issue is how we copy restrictions from the original Criteria when building the count Criteria. These restrictions are associated with a certain Path element that cannot be reused from one Criteria to another.

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

I fixed it by recreating the predicate from the internal pipeline. This adds a bit of redundant work but seems like the simplest solution at this point.

if (!filters.isEmpty()) {
    // There can only be one JPAStreamer filter after the filter merge (see FilterCriteriaModifier). 
    IntermediateOperation<?, ?> filter = filters.stream().findFirst().get();
    this.<T>getPredicate(filter).ifPresent(speedmentPredicate -> {
        final Predicate predicate = predicateFactory.createPredicate(countCriteria, speedmentPredicate);
        countQuery.where(predicate);
    });
}