Avoid repeated query parsing.
This commit makes sure to avoid unnecessary parsing of queries when sorting must not be appended. Additionally the parsed result is cached on a per query string with sort expression basis to avoid parsing repeating occurrences of the same expressions eg. while paging through results. Closes: #3310 Original pull request: #3321
This commit is contained in:
committed by
Mark Paluch
parent
005c507eaa
commit
804e65cf5d
@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.query;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.repository.QueryRewriter;
|
||||
@@ -28,6 +30,7 @@ import org.springframework.data.util.Lazy;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentLruCache;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -41,6 +44,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Mark Paluch
|
||||
* @author Diego Krupitza
|
||||
* @author Greg Turnquist
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
|
||||
@@ -50,6 +54,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
private final SpelExpressionParser parser;
|
||||
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
|
||||
private final QueryRewriter queryRewriter;
|
||||
private ConcurrentLruCache<CachableQuery, String> queryCache = new ConcurrentLruCache<>(16, this::applySorting);
|
||||
private final Lazy<ParameterBinder> countParameterBinder;
|
||||
|
||||
/**
|
||||
@@ -104,12 +109,12 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
@Override
|
||||
public Query doCreateQuery(JpaParametersParameterAccessor accessor) {
|
||||
|
||||
String sortedQueryString = QueryEnhancerFactory.forQuery(query) //
|
||||
.applySorting(accessor.getSort(), query.getAlias());
|
||||
Sort sort = accessor.getSort();
|
||||
String sortedQueryString = applySortingIfNecessary(query, sort);
|
||||
|
||||
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
|
||||
|
||||
Query query = createJpaQuery(sortedQueryString, accessor.getSort(), accessor.getPageable(),
|
||||
processor.getReturnedType());
|
||||
Query query = createJpaQuery(sortedQueryString, sort, accessor.getPageable(), processor.getReturnedType());
|
||||
|
||||
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(sortedQueryString, query);
|
||||
|
||||
@@ -118,6 +123,10 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
return parameterBinder.get().bindAndPrepare(query, metadata, accessor);
|
||||
}
|
||||
|
||||
protected String applySorting(DeclaredQuery query, Sort sort) {
|
||||
return queryCache.get(new CachableQuery(query, sort));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ParameterBinder createBinder() {
|
||||
return createBinder(query);
|
||||
@@ -194,4 +203,77 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
|
||||
? queryRewriter.rewrite(originalQuery, pageable) //
|
||||
: queryRewriter.rewrite(originalQuery, sort);
|
||||
}
|
||||
|
||||
String applySorting(CachableQuery cachableQuery) {
|
||||
|
||||
return QueryEnhancerFactory.forQuery(cachableQuery.getDeclaredQuery()).applySorting(cachableQuery.getSort(),
|
||||
cachableQuery.getAlias());
|
||||
}
|
||||
|
||||
private String applySortingIfNecessary(DeclaredQuery query, Sort sort) {
|
||||
|
||||
if (sort.isUnsorted()) {
|
||||
return query.getQueryString();
|
||||
}
|
||||
return applySorting(query, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Value object with optimized {@link Object#equals(Object)} to cache a query based on its query string and
|
||||
* {@link Sort sorting}.
|
||||
*
|
||||
* @since 3.2.3
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
static class CachableQuery {
|
||||
|
||||
private DeclaredQuery declaredQuery;
|
||||
private final String queryString;
|
||||
private final Sort sort;
|
||||
|
||||
CachableQuery(DeclaredQuery query, Sort sort) {
|
||||
|
||||
this.declaredQuery = query;
|
||||
this.queryString = query.getQueryString();
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
DeclaredQuery getDeclaredQuery() {
|
||||
return declaredQuery;
|
||||
}
|
||||
|
||||
Sort getSort() {
|
||||
return sort;
|
||||
}
|
||||
|
||||
String getAlias() {
|
||||
return declaredQuery.getAlias();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CachableQuery that = (CachableQuery) o;
|
||||
|
||||
if (!Objects.equals(queryString, that.queryString)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(sort, that.sort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = queryString != null ? queryString.hashCode() : 0;
|
||||
result = 31 * result + (sort != null ? sort.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jpa.repository.query;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.metamodel.Metamodel;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.provider.QueryExtractor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.jpa.repository.QueryRewriter;
|
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
|
||||
import org.springframework.data.repository.query.ParametersSource;
|
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.ReturnedType;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class AbstractStringBasedJpaQueryUnitTests {
|
||||
|
||||
@Test // GH-3310
|
||||
void shouldNotAttemptToAppendSortIfNoSortArgumentPresent() {
|
||||
|
||||
InvocationCapturingStringQueryStub stringQuery = forMethod(TestRepo.class, "find");
|
||||
stringQuery.createQueryWithArguments();
|
||||
|
||||
stringQuery.neverCalled("applySorting");
|
||||
}
|
||||
|
||||
@Test // GH-3310
|
||||
void shouldNotAttemptToAppendSortIfSortIndicatesUnsorted() {
|
||||
|
||||
InvocationCapturingStringQueryStub stringQuery = forMethod(TestRepo.class, "find", Sort.class);
|
||||
stringQuery.createQueryWithArguments(Sort.unsorted());
|
||||
|
||||
stringQuery.neverCalled("applySorting");
|
||||
}
|
||||
|
||||
@Test // GH-3310
|
||||
void shouldAppendSortIfSortPresent() {
|
||||
|
||||
InvocationCapturingStringQueryStub stringQuery = forMethod(TestRepo.class, "find", Sort.class);
|
||||
stringQuery.createQueryWithArguments(Sort.by("name"));
|
||||
|
||||
stringQuery.called("applySorting").times(1);
|
||||
}
|
||||
|
||||
@Test // GH-3311
|
||||
void cachesInvocationBasedOnSortArgument() {
|
||||
|
||||
InvocationCapturingStringQueryStub stringQuery = forMethod(TestRepo.class, "find", Sort.class);
|
||||
stringQuery.createQueryWithArguments(Sort.by("name"));
|
||||
stringQuery.called("applySorting").times(1);
|
||||
|
||||
stringQuery.createQueryWithArguments(Sort.by("name"));
|
||||
stringQuery.called("applySorting").times(1);
|
||||
|
||||
stringQuery.createQueryWithArguments(Sort.by("age"));
|
||||
stringQuery.called("applySorting").times(2);
|
||||
}
|
||||
|
||||
interface TestRepo extends Repository<Object, Object> {
|
||||
|
||||
@Query("SELECT e FROM Employee e")
|
||||
Object find();
|
||||
|
||||
@Query("SELECT e FROM Employee e")
|
||||
Object find(Sort sort);
|
||||
}
|
||||
|
||||
static InvocationCapturingStringQueryStub forMethod(Class<?> repository, String method, Class<?>... args) {
|
||||
|
||||
Method respositoryMethod = ReflectionUtils.findMethod(repository, method, args);
|
||||
RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(repository);
|
||||
SpelAwareProxyProjectionFactory projectionFactory = Mockito.mock(SpelAwareProxyProjectionFactory.class);
|
||||
QueryExtractor queryExtractor = Mockito.mock(QueryExtractor.class);
|
||||
JpaQueryMethod queryMethod = new JpaQueryMethod(respositoryMethod, repositoryMetadata, projectionFactory,
|
||||
queryExtractor);
|
||||
|
||||
Query query = AnnotatedElementUtils.getMergedAnnotation(respositoryMethod, Query.class);
|
||||
|
||||
return new InvocationCapturingStringQueryStub(respositoryMethod, queryMethod, query.value(), query.countQuery(),
|
||||
new SpelExpressionParser());
|
||||
|
||||
}
|
||||
|
||||
static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQuery {
|
||||
|
||||
private final Method targetMethod;
|
||||
private final MultiValueMap<String, Arguments> capturedArguments = new LinkedMultiValueMap<>(3);
|
||||
|
||||
InvocationCapturingStringQueryStub(Method targetMethod, JpaQueryMethod queryMethod, String queryString,
|
||||
@Nullable String countQueryString, SpelExpressionParser parser) {
|
||||
super(queryMethod, new Supplier<EntityManager>() {
|
||||
|
||||
@Override
|
||||
public EntityManager get() {
|
||||
|
||||
EntityManager em = Mockito.mock(EntityManager.class);
|
||||
|
||||
Metamodel meta = mock(Metamodel.class);
|
||||
when(em.getMetamodel()).thenReturn(meta);
|
||||
when(em.getDelegate()).thenReturn(new Object()); // some generic jpa
|
||||
|
||||
return em;
|
||||
}
|
||||
}.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class),
|
||||
Mockito.mock(QueryMethodEvaluationContextProvider.class), parser);
|
||||
|
||||
this.targetMethod = targetMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String applySorting(CachableQuery query) {
|
||||
|
||||
captureInvocation("applySorting", query);
|
||||
|
||||
return super.applySorting(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected jakarta.persistence.Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
|
||||
ReturnedType returnedType) {
|
||||
|
||||
captureInvocation("createJpaQuery", queryString, sort, pageable, returnedType);
|
||||
|
||||
jakarta.persistence.Query jpaQuery = super.createJpaQuery(queryString, sort, pageable, returnedType);
|
||||
return jpaQuery == null ? Mockito.mock(jakarta.persistence.Query.class) : jpaQuery;
|
||||
}
|
||||
|
||||
// --> convenience for tests
|
||||
|
||||
JpaParameters getParameters() {
|
||||
return new JpaParameters(ParametersSource.of(targetMethod));
|
||||
}
|
||||
|
||||
JpaParametersParameterAccessor getParameterAccessor(Object... args) {
|
||||
return new JpaParametersParameterAccessor(getParameters(), args);
|
||||
}
|
||||
|
||||
jakarta.persistence.Query createQueryWithArguments(Object... args) {
|
||||
return doCreateQuery(getParameterAccessor(args));
|
||||
}
|
||||
|
||||
// --> capturing methods
|
||||
|
||||
private void captureInvocation(String key, Object... args) {
|
||||
capturedArguments.add(key, new Arguments(args));
|
||||
}
|
||||
|
||||
// --> verification methdos
|
||||
|
||||
int getInvocationCount(String method) {
|
||||
|
||||
List<Arguments> invocations = capturedArguments.get(method);
|
||||
return invocations != null ? invocations.size() : 0;
|
||||
}
|
||||
|
||||
public void neverCalled(String method) {
|
||||
called(method).never();
|
||||
}
|
||||
|
||||
public Times called(String method) {
|
||||
|
||||
return (invocationCount -> {
|
||||
|
||||
int actualCount = getInvocationCount(method);
|
||||
Assertions.assertThat(actualCount)
|
||||
.withFailMessage(
|
||||
() -> "Expected %d invocations for %s, but recorded %d".formatted(invocationCount, method, actualCount))
|
||||
.isEqualTo(invocationCount);
|
||||
});
|
||||
}
|
||||
|
||||
static class Arguments {
|
||||
|
||||
List<Object> values = new ArrayList<>(3);
|
||||
|
||||
public Arguments(Object... values) {
|
||||
this.values = Arrays.asList(values);
|
||||
}
|
||||
}
|
||||
|
||||
interface Times {
|
||||
|
||||
void times(int invocationCount);
|
||||
|
||||
default void never() {
|
||||
times(0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user