Skip to content

Commit cdcac86

Browse files
committed
Use SelectionQuery.getResultCount() for count queries if possible.
We now use Hibernate's built-in mechanism to obtain the result count if there is an enclosing transaction. Without the transaction, the session is being closed and we cannot run the query. Closes #3456
1 parent db5a441 commit cdcac86

File tree

10 files changed

+104
-13
lines changed

10 files changed

+104
-13
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@
3030
import java.util.List;
3131
import java.util.NoSuchElementException;
3232
import java.util.Set;
33+
import java.util.function.LongSupplier;
3334

3435
import org.eclipse.persistence.config.QueryHints;
3536
import org.eclipse.persistence.jpa.JpaQuery;
3637
import org.eclipse.persistence.queries.ScrollableCursor;
3738
import org.hibernate.ScrollMode;
3839
import org.hibernate.ScrollableResults;
3940
import org.hibernate.proxy.HibernateProxy;
41+
import org.hibernate.query.SelectionQuery;
4042
import org.jspecify.annotations.Nullable;
4143

4244
import org.springframework.data.util.CloseableIterator;
@@ -117,6 +119,17 @@ public String getCommentHintKey() {
117119
return "org.hibernate.comment";
118120
}
119121

122+
@Override
123+
public long getResultCount(Query resultQuery, LongSupplier countSupplier) {
124+
125+
if (TransactionSynchronizationManager.isActualTransactionActive()
126+
&& resultQuery instanceof SelectionQuery<?> sq) {
127+
return sq.getResultCount();
128+
}
129+
130+
return super.getResultCount(resultQuery, countSupplier);
131+
}
132+
120133
},
121134

122135
/**
@@ -160,6 +173,7 @@ public String getCommentHintKey() {
160173
public String getCommentHintValue(String comment) {
161174
return "/* " + comment + " */";
162175
}
176+
163177
},
164178

165179
/**
@@ -197,6 +211,7 @@ public boolean shouldUseAccessorFor(Object entity) {
197211
public @Nullable String getCommentHintKey() {
198212
return null;
199213
}
214+
200215
};
201216

202217
private static final @Nullable Class<?> typedParameterValueClass;
@@ -406,6 +421,18 @@ public boolean isPresent() {
406421
return this.present;
407422
}
408423

424+
/**
425+
* Obtain the result count from a {@link Query} returning the result or fall back to {@code countSupplier} if the
426+
* query does not provide the result count.
427+
*
428+
* @param resultQuery the query that has returned {@link Query#getResultList()}
429+
* @param countSupplier fallback supplier to provide the count if the query does not provide it.
430+
* @return the result count.
431+
*/
432+
public long getResultCount(Query resultQuery, LongSupplier countSupplier) {
433+
return countSupplier.getAsLong();
434+
}
435+
409436
/**
410437
* Holds the PersistenceProvider specific interface names.
411438
*
@@ -427,6 +454,7 @@ interface Constants {
427454

428455
String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel";
429456
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";
457+
430458
}
431459

432460
public CloseableIterator<Object> executeQueryWithResultStream(Query jpaQuery) {
@@ -482,6 +510,7 @@ public void close() {
482510
scrollableResults.close();
483511
}
484512
}
513+
485514
}
486515

487516
/**
@@ -531,5 +560,7 @@ public void close() {
531560
scrollableCursor.close();
532561
}
533562
}
563+
534564
}
565+
535566
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
106106
} else if (method.isSliceQuery()) {
107107
return new SlicedExecution();
108108
} else if (method.isPageQuery()) {
109-
return new PagedExecution();
109+
return new PagedExecution(this.provider);
110110
} else if (method.isModifyingQuery()) {
111111
return null;
112112
} else {
@@ -120,6 +120,15 @@ public JpaQueryMethod getQueryMethod() {
120120
return method;
121121
}
122122

123+
/**
124+
* Returns {@literal true} if the query has a dedicated count query associated with it or {@literal false} if the
125+
* count query shall be derived.
126+
*
127+
* @return {@literal true} if the query has a dedicated count query {@literal false} if the * count query is derived.
128+
* @since 3.5
129+
*/
130+
public abstract boolean hasDeclaredCountQuery();
131+
123132
/**
124133
* Returns the {@link EntityManager}.
125134
*

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
6262
private final QuerySortRewriter querySortRewriter;
6363
private final Lazy<ParameterBinder> countParameterBinder;
6464
private final ValueEvaluationContextProvider valueExpressionContextProvider;
65+
private final boolean hasDeclaredCountQuery;
6566

6667
/**
6768
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -101,6 +102,7 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Decl
101102
this.valueExpressionContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters());
102103

103104
this.query = TemplatedQuery.create(query, method.getEntityInformation(), queryConfiguration);
105+
this.hasDeclaredCountQuery = countQuery != null;
104106

105107
this.countQuery = Lazy.of(() -> {
106108

@@ -130,8 +132,9 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Decl
130132
"JDBC style parameters (?) are not supported for JPA queries");
131133
}
132134

133-
private DeclaredQuery createQuery(String queryString, boolean nativeQuery) {
134-
return nativeQuery ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString);
135+
@Override
136+
public boolean hasDeclaredCountQuery() {
137+
return hasDeclaredCountQuery;
135138
}
136139

137140
@Override

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryExecution.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,20 +188,47 @@ protected Object doExecute(AbstractJpaQuery query, JpaParametersParameterAccesso
188188
*/
189189
static class PagedExecution extends JpaQueryExecution {
190190

191+
private final PersistenceProvider provider;
192+
193+
PagedExecution(PersistenceProvider provider) {
194+
this.provider = provider;
195+
}
196+
191197
@Override
192198
@SuppressWarnings("unchecked")
193199
protected Object doExecute(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) {
194200

195201
Query query = repositoryQuery.createQuery(accessor);
196202

197203
return PageableExecutionUtils.getPage(query.getResultList(), accessor.getPageable(),
198-
() -> count(repositoryQuery, accessor));
204+
() -> count(query, repositoryQuery, accessor));
205+
}
206+
207+
private long count(Query resultQuery, AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) {
208+
209+
if (repositoryQuery.hasDeclaredCountQuery()) {
210+
return doCount(repositoryQuery, accessor);
211+
}
212+
213+
return provider.getResultCount(resultQuery, () -> doCount(repositoryQuery, accessor));
199214
}
200215

201-
private long count(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) {
216+
long doCount(AbstractJpaQuery repositoryQuery, JpaParametersParameterAccessor accessor) {
202217

203218
List<?> totals = repositoryQuery.createCountQuery(accessor).getResultList();
204-
return (totals.size() == 1 ? CONVERSION_SERVICE.convert(totals.get(0), Long.class) : totals.size());
219+
220+
if (totals.size() == 1) {
221+
Object result = totals.get(0);
222+
223+
if (result instanceof Number n) {
224+
return n.longValue();
225+
}
226+
227+
return CONVERSION_SERVICE.convert(result, Long.class);
228+
}
229+
230+
// group by count
231+
return totals.size();
205232
}
206233
}
207234

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ static boolean hasNamedQuery(EntityManager em, String queryName) {
182182
return query;
183183
}
184184

185+
@Override
186+
public boolean hasDeclaredCountQuery() {
187+
return namedCountQueryIsPresent;
188+
}
189+
185190
@Override
186191
protected Query doCreateQuery(JpaParametersParameterAccessor accessor) {
187192

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public class PartTreeJpaQuery extends AbstractJpaQuery {
112112
}
113113
}
114114

115+
@Override
116+
public boolean hasDeclaredCountQuery() {
117+
return false;
118+
}
119+
115120
@Override
116121
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
117122
return queryPreparer.createQuery(accessor);

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StoredProcedureJpaQuery.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ private static boolean useNamedParameters(QueryMethod method) {
8181
return false;
8282
}
8383

84+
@Override
85+
public boolean hasDeclaredCountQuery() {
86+
return false;
87+
}
88+
8489
@Override
8590
protected StoredProcedureQuery createQuery(JpaParametersParameterAccessor accessor) {
8691
return applyHints(doCreateQuery(accessor), getQueryMethod());

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractJpaQueryTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ protected Query doCreateQuery(JpaParametersParameterAccessor accessor) {
230230
return query;
231231
}
232232

233+
@Override
234+
public boolean hasDeclaredCountQuery() {
235+
return true;
236+
}
237+
233238
@Override
234239
protected TypedQuery<Long> doCreateCountQuery(JpaParametersParameterAccessor accessor) {
235240
return countQuery;

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryExecutionUnitTests.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
import org.springframework.data.domain.PageRequest;
4242
import org.springframework.data.domain.Pageable;
43+
import org.springframework.data.jpa.provider.PersistenceProvider;
4344
import org.springframework.data.jpa.provider.QueryExtractor;
4445
import org.springframework.data.jpa.repository.Modifying;
4546
import org.springframework.data.jpa.repository.query.JpaQueryExecution.ModifyingExecution;
@@ -183,7 +184,7 @@ void pagedExecutionRetrievesObjectsForPageableOutOfRange() throws Exception {
183184
when(jpaQuery.createQuery(Mockito.any())).thenReturn(query);
184185
when(countQuery.getResultList()).thenReturn(Arrays.asList(20L));
185186

186-
PagedExecution execution = new PagedExecution();
187+
PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA);
187188
execution.doExecute(jpaQuery,
188189
new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(2, 10) }));
189190

@@ -199,7 +200,7 @@ void pagedExecutionShouldNotGenerateCountQueryIfQueryReportedNoResults() throws
199200
when(jpaQuery.createQuery(Mockito.any())).thenReturn(query);
200201
when(query.getResultList()).thenReturn(Arrays.asList(0L));
201202

202-
PagedExecution execution = new PagedExecution();
203+
PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA);
203204
execution.doExecute(jpaQuery,
204205
new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(0, 10) }));
205206

@@ -215,7 +216,7 @@ void pagedExecutionShouldUseCountFromResultIfOffsetIsZeroAndResultsWithinPageSiz
215216
when(jpaQuery.createQuery(Mockito.any())).thenReturn(query);
216217
when(query.getResultList()).thenReturn(Arrays.asList(new Object(), new Object(), new Object(), new Object()));
217218

218-
PagedExecution execution = new PagedExecution();
219+
PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA);
219220
execution.doExecute(jpaQuery,
220221
new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(0, 10) }));
221222

@@ -230,7 +231,7 @@ void pagedExecutionShouldUseCountFromResultWithOffsetAndResultsWithinPageSize()
230231
when(jpaQuery.createQuery(Mockito.any())).thenReturn(query);
231232
when(query.getResultList()).thenReturn(Arrays.asList(new Object(), new Object(), new Object(), new Object()));
232233

233-
PagedExecution execution = new PagedExecution();
234+
PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA);
234235
execution.doExecute(jpaQuery,
235236
new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(5, 10) }));
236237

@@ -247,7 +248,7 @@ void pagedExecutionShouldUseRequestCountFromResultWithOffsetAndResultsHitLowerPa
247248
when(jpaQuery.createCountQuery(Mockito.any())).thenReturn(query);
248249
when(countQuery.getResultList()).thenReturn(Arrays.asList(20L));
249250

250-
PagedExecution execution = new PagedExecution();
251+
PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA);
251252
execution.doExecute(jpaQuery,
252253
new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(4, 4) }));
253254

@@ -264,7 +265,7 @@ void pagedExecutionShouldUseRequestCountFromResultWithOffsetAndResultsHitUpperPa
264265
when(jpaQuery.createCountQuery(Mockito.any())).thenReturn(query);
265266
when(countQuery.getResultList()).thenReturn(Arrays.asList(20L));
266267

267-
PagedExecution execution = new PagedExecution();
268+
PagedExecution execution = new PagedExecution(PersistenceProvider.GENERIC_JPA);
268269
execution.doExecute(jpaQuery,
269270
new JpaParametersParameterAccessor(parameters, new Object[] { PageRequest.of(4, 4) }));
270271

src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ Sometimes, no matter how many features you try to apply, it seems impossible to
294294
You have the ability to get your hands on the query, right before it's sent to the `EntityManager` and "rewrite" it.
295295
That is, you can make any alterations at the last moment.
296296
Query rewriting applies to the actual query and, when applicable, to count queries.
297-
Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery`.
297+
Count queries are optimized and therefore, either not necessary or a count is obtained through other means, such as derived from a Hibernate `SelectionQuery` if there is an enclosing transaction.
298298

299299
.Declare a QueryRewriter using `@Query`
300300
====

0 commit comments

Comments
 (0)