Skip to content

Commit 4c2a304

Browse files
committed
Deduplicate fetch join results at QueryDSL level
Since Hibernate 6 removed automatic result deduplication for fetch joins, and Hibernate 7.3 fully removed the feature, queries with fetchJoin() can return duplicate parent entities from JOIN results. Add deduplication via LinkedHashSet in fetch(), fetchOne(), and fetchResults() when fetchJoin is detected, applied at the QueryDSL level so it works across all JPA providers. Closes #1596
1 parent c6e8fe8 commit 4c2a304

File tree

2 files changed

+71
-6
lines changed

2 files changed

+71
-6
lines changed

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/hibernate/AbstractHibernateQuery.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
import com.querydsl.jpa.FactoryExpressionTransformer;
3131
import com.querydsl.jpa.HQLTemplates;
3232
import com.querydsl.jpa.JPAQueryBase;
33+
import com.querydsl.jpa.JPAQueryMixin;
3334
import com.querydsl.jpa.JPQLTemplates;
3435
import com.querydsl.jpa.ScrollableResultsIterator;
36+
import java.util.ArrayList;
3537
import java.util.HashMap;
38+
import java.util.LinkedHashSet;
3639
import java.util.List;
3740
import java.util.Map;
3841
import java.util.logging.Level;
@@ -213,7 +216,11 @@ public Stream<T> stream() {
213216
@SuppressWarnings("unchecked")
214217
public List<T> fetch() {
215218
try {
216-
return createQuery().list();
219+
List<T> results = createQuery().list();
220+
if (hasFetchJoin()) {
221+
results = new ArrayList<>(new LinkedHashSet<>(results));
222+
}
223+
return results;
217224
} finally {
218225
reset();
219226
}
@@ -230,6 +237,9 @@ public QueryResults<T> fetchResults() {
230237
var query = createQuery(modifiers, false);
231238
@SuppressWarnings("unchecked")
232239
List<T> list = query.list();
240+
if (hasFetchJoin()) {
241+
list = new ArrayList<>(new LinkedHashSet<>(list));
242+
}
233243
return new QueryResults<>(list, modifiers, total);
234244
} else {
235245
return QueryResults.emptyResults();
@@ -239,6 +249,16 @@ public QueryResults<T> fetchResults() {
239249
}
240250
}
241251

252+
/**
253+
* Check if any join in this query has a fetch join flag.
254+
*
255+
* @return true if at least one join uses fetchJoin
256+
*/
257+
private boolean hasFetchJoin() {
258+
return getMetadata().getJoins().stream()
259+
.anyMatch(join -> join.getFlags().contains(JPAQueryMixin.FETCH));
260+
}
261+
242262
protected void logQuery(String queryString) {
243263
if (logger.isLoggable(Level.FINE)) {
244264
var normalizedQuery = queryString.replace('\n', ' ');
@@ -363,6 +383,17 @@ public T fetchOne() throws NonUniqueResultException {
363383
try {
364384
var modifiers = getMetadata().getModifiers();
365385
var query = createQuery(modifiers, false);
386+
if (hasFetchJoin()) {
387+
List<T> results = query.list();
388+
results = new ArrayList<>(new LinkedHashSet<>(results));
389+
if (results.isEmpty()) {
390+
return null;
391+
} else if (results.size() == 1) {
392+
return results.get(0);
393+
} else {
394+
throw new NonUniqueResultException();
395+
}
396+
}
366397
try {
367398
return (T) query.uniqueResult();
368399
} catch (org.hibernate.NonUniqueResultException e) {

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/impl/AbstractJPAQuery.java

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.querydsl.core.types.Expression;
2323
import com.querydsl.core.types.FactoryExpression;
2424
import com.querydsl.jpa.JPAQueryBase;
25+
import com.querydsl.jpa.JPAQueryMixin;
2526
import com.querydsl.jpa.JPQLSerializer;
2627
import com.querydsl.jpa.JPQLTemplates;
2728
import com.querydsl.jpa.QueryHandler;
@@ -179,10 +180,11 @@ protected Query createQuery(@Nullable QueryModifiers modifiers, boolean forCount
179180
*/
180181
private List<?> getResultList(Query query) {
181182
// TODO : use lazy fetch here?
183+
List<?> results;
182184
if (projection != null) {
183-
List<?> results = query.getResultList();
184-
List<Object> rv = new ArrayList<>(results.size());
185-
for (Object o : results) {
185+
List<?> raw = query.getResultList();
186+
List<Object> rv = new ArrayList<>(raw.size());
187+
for (Object o : raw) {
186188
if (o != null) {
187189
if (!o.getClass().isArray()) {
188190
o = new Object[] {o};
@@ -192,10 +194,29 @@ private List<?> getResultList(Query query) {
192194
rv.add(projection.newInstance(new Object[] {null}));
193195
}
194196
}
195-
return rv;
197+
results = rv;
196198
} else {
197-
return query.getResultList();
199+
results = query.getResultList();
198200
}
201+
202+
// Deduplicate results when fetchJoin is used.
203+
// Since Hibernate 6, automatic deduplication on fetch joins was removed,
204+
// so we handle it at the QueryDSL level for all JPA providers.
205+
if (hasFetchJoin()) {
206+
results = new ArrayList<>(new LinkedHashSet<>(results));
207+
}
208+
209+
return results;
210+
}
211+
212+
/**
213+
* Check if any join in this query has a fetch join flag.
214+
*
215+
* @return true if at least one join uses fetchJoin
216+
*/
217+
private boolean hasFetchJoin() {
218+
return getMetadata().getJoins().stream()
219+
.anyMatch(join -> join.getFlags().contains(JPAQueryMixin.FETCH));
199220
}
200221

201222
/**
@@ -336,6 +357,19 @@ protected void reset() {}
336357
@Override
337358
public T fetchOne() throws NonUniqueResultException {
338359
try {
360+
if (hasFetchJoin()) {
361+
// When fetchJoin is used, use getResultList with deduplication
362+
// to avoid NonUniqueResultException caused by JOIN duplicates
363+
var query = createQuery(getMetadata().getModifiers(), false);
364+
var results = (List<T>) getResultList(query);
365+
if (results.isEmpty()) {
366+
return null;
367+
} else if (results.size() == 1) {
368+
return results.get(0);
369+
} else {
370+
throw new NonUniqueResultException();
371+
}
372+
}
339373
var query = createQuery(getMetadata().getModifiers(), false);
340374
return (T) getSingleResult(query);
341375
} catch (jakarta.persistence.NoResultException e) {

0 commit comments

Comments
 (0)