diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/AbstractHibernateQuery.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/AbstractHibernateQuery.java index 20f7733ed..3f5349904 100644 --- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/AbstractHibernateQuery.java +++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/AbstractHibernateQuery.java @@ -30,9 +30,12 @@ import com.querydsl.jpa.FactoryExpressionTransformer; import com.querydsl.jpa.HQLTemplates; import com.querydsl.jpa.JPAQueryBase; +import com.querydsl.jpa.JPAQueryMixin; import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.ScrollableResultsIterator; +import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -213,7 +216,11 @@ public Stream stream() { @SuppressWarnings("unchecked") public List fetch() { try { - return createQuery().list(); + List results = createQuery().list(); + if (hasFetchJoin()) { + results = new ArrayList<>(new LinkedHashSet<>(results)); + } + return results; } finally { reset(); } @@ -230,6 +237,9 @@ public QueryResults fetchResults() { var query = createQuery(modifiers, false); @SuppressWarnings("unchecked") List list = query.list(); + if (hasFetchJoin()) { + list = new ArrayList<>(new LinkedHashSet<>(list)); + } return new QueryResults<>(list, modifiers, total); } else { return QueryResults.emptyResults(); @@ -239,6 +249,16 @@ public QueryResults fetchResults() { } } + /** + * Check if any join in this query has a fetch join flag. + * + * @return true if at least one join uses fetchJoin + */ + private boolean hasFetchJoin() { + return getMetadata().getJoins().stream() + .anyMatch(join -> join.getFlags().contains(JPAQueryMixin.FETCH)); + } + protected void logQuery(String queryString) { if (logger.isLoggable(Level.FINE)) { var normalizedQuery = queryString.replace('\n', ' '); @@ -363,6 +383,17 @@ public T fetchOne() throws NonUniqueResultException { try { var modifiers = getMetadata().getModifiers(); var query = createQuery(modifiers, false); + if (hasFetchJoin()) { + List results = query.list(); + results = new ArrayList<>(new LinkedHashSet<>(results)); + if (results.isEmpty()) { + return null; + } else if (results.size() == 1) { + return results.get(0); + } else { + throw new NonUniqueResultException(); + } + } try { return (T) query.uniqueResult(); } catch (org.hibernate.NonUniqueResultException e) { diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java index 438d85847..63a87ef18 100644 --- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java +++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java @@ -22,6 +22,7 @@ import com.querydsl.core.types.Expression; import com.querydsl.core.types.FactoryExpression; import com.querydsl.jpa.JPAQueryBase; +import com.querydsl.jpa.JPAQueryMixin; import com.querydsl.jpa.JPQLSerializer; import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.QueryHandler; @@ -179,10 +180,11 @@ protected Query createQuery(@Nullable QueryModifiers modifiers, boolean forCount */ private List getResultList(Query query) { // TODO : use lazy fetch here? + List results; if (projection != null) { - List results = query.getResultList(); - List rv = new ArrayList<>(results.size()); - for (Object o : results) { + List raw = query.getResultList(); + List rv = new ArrayList<>(raw.size()); + for (Object o : raw) { if (o != null) { if (!o.getClass().isArray()) { o = new Object[] {o}; @@ -192,10 +194,29 @@ private List getResultList(Query query) { rv.add(projection.newInstance(new Object[] {null})); } } - return rv; + results = rv; } else { - return query.getResultList(); + results = query.getResultList(); } + + // Deduplicate results when fetchJoin is used. + // Since Hibernate 6, automatic deduplication on fetch joins was removed, + // so we handle it at the QueryDSL level for all JPA providers. + if (hasFetchJoin()) { + results = new ArrayList<>(new LinkedHashSet<>(results)); + } + + return results; + } + + /** + * Check if any join in this query has a fetch join flag. + * + * @return true if at least one join uses fetchJoin + */ + private boolean hasFetchJoin() { + return getMetadata().getJoins().stream() + .anyMatch(join -> join.getFlags().contains(JPAQueryMixin.FETCH)); } /** @@ -336,6 +357,19 @@ protected void reset() {} @Override public T fetchOne() throws NonUniqueResultException { try { + if (hasFetchJoin()) { + // When fetchJoin is used, use getResultList with deduplication + // to avoid NonUniqueResultException caused by JOIN duplicates + var query = createQuery(getMetadata().getModifiers(), false); + var results = (List) getResultList(query); + if (results.isEmpty()) { + return null; + } else if (results.size() == 1) { + return results.get(0); + } else { + throw new NonUniqueResultException(); + } + } var query = createQuery(getMetadata().getModifiers(), false); return (T) getSingleResult(query); } catch (jakarta.persistence.NoResultException e) {