Add support for projections.

See #3830
This commit is contained in:
Mark Paluch
2025-04-04 11:43:42 +02:00
parent e38b219898
commit f2b0dca62b
11 changed files with 1135 additions and 653 deletions

View File

@@ -40,6 +40,9 @@ abstract class AotQuery {
*/
public abstract boolean isNative();
/**
* @return the list of parameter bindings.
*/
public List<ParameterBinding> getParameterBindings() {
return parameterBindings;
}

View File

@@ -15,10 +15,16 @@
*/
package org.springframework.data.jpa.repository.aot;
import jakarta.persistence.Tuple;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.core.CollectionFactory;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
@@ -26,6 +32,7 @@ import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.JpaParameters;
import org.springframework.data.jpa.repository.query.QueryEnhancer;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.jpa.util.TupleBackedMap;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
@@ -104,6 +111,52 @@ public class AotRepositoryFragmentSupport {
return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies()));
}
protected <T> @Nullable T convertOne(@Nullable Object result, boolean nativeQuery, Class<T> projection) {
if (result == null) {
return null;
}
if (projection.isInstance(result)) {
return projection.cast(result);
}
return projectionFactory.createProjection(projection,
result instanceof Tuple t ? new TupleBackedMap(nativeQuery ? TupleBackedMap.underscoreAware(t) : t) : result);
}
protected @Nullable Object convertMany(@Nullable Object result, boolean nativeQuery, Class<?> projection) {
if (result == null) {
return null;
}
if (projection.isInstance(result)) {
return result;
}
if (result instanceof Stream<?> stream) {
return stream.map(it -> convertOne(it, nativeQuery, projection));
}
if (result instanceof Slice<?> slice) {
return slice.map(it -> convertOne(it, nativeQuery, projection));
}
if (result instanceof Collection<?> collection) {
Collection<@Nullable Object> target = CollectionFactory.createCollection(collection.getClass(),
collection.size());
for (Object o : collection) {
target.add(convertOne(o, nativeQuery, projection));
}
return target;
}
throw new UnsupportedOperationException("Cannot create projection for %s".formatted(result));
}
private record DefaultQueryRewriteInformation(Sort sort,
ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation {

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2025 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.aot;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManagerFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.StringUtils;
/**
* Factory for {@link AotEntityGraph}.
*
* @author Mark Paluch
* @since 4.0
*/
class EntityGraphLookup {
private final EntityManagerFactory entityManagerFactory;
public EntityGraphLookup(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
@SuppressWarnings("unchecked")
public @Nullable AotEntityGraph findEntityGraph(MergedAnnotation<EntityGraph> entityGraph,
RepositoryInformation information, ReturnedType returnedType, JpaQueryMethod queryMethod) {
if (!entityGraph.isPresent()) {
return null;
}
EntityGraph.EntityGraphType type = entityGraph.getEnum("type", EntityGraph.EntityGraphType.class);
String[] attributePaths = entityGraph.getStringArray("attributePaths");
Collection<String> entityGraphNames = getEntityGraphNames(entityGraph, information, queryMethod);
List<Class<?>> candidates = Arrays.asList(returnedType.getDomainType(), returnedType.getReturnedType(),
returnedType.getTypeToRead());
for (Class<?> candidate : candidates) {
Map<String, jakarta.persistence.EntityGraph<?>> namedEntityGraphs = entityManagerFactory
.getNamedEntityGraphs(Class.class.cast(candidate));
if (namedEntityGraphs.isEmpty()) {
continue;
}
for (String entityGraphName : entityGraphNames) {
if (namedEntityGraphs.containsKey(entityGraphName)) {
return new AotEntityGraph(entityGraphName, type, Collections.emptyList());
}
}
}
if (attributePaths.length > 0) {
return new AotEntityGraph(null, type, Arrays.asList(attributePaths));
}
return null;
}
private Set<String> getEntityGraphNames(MergedAnnotation<EntityGraph> entityGraph, RepositoryInformation information,
JpaQueryMethod queryMethod) {
Set<String> entityGraphNames = new LinkedHashSet<>();
String value = entityGraph.getString("value");
if (StringUtils.hasText(value)) {
entityGraphNames.add(value);
}
entityGraphNames.add(queryMethod.getNamedQueryName());
entityGraphNames.add(getFallbackEntityGraphName(information, queryMethod));
return entityGraphNames;
}
private String getFallbackEntityGraphName(RepositoryInformation information, JpaQueryMethod queryMethod) {
Class<?> domainType = information.getDomainType();
Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class);
String entityName = entity != null && StringUtils.hasText(entity.name()) ? entity.name()
: domainType.getSimpleName();
return entityName + "." + queryMethod.getName();
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.jpa.repository.aot;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.QueryHint;
import jakarta.persistence.Tuple;
import java.lang.reflect.Type;
import java.util.List;
@@ -30,6 +31,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.data.jpa.repository.QueryHints;
@@ -37,6 +39,7 @@ import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.ParameterBinding;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
@@ -134,6 +137,11 @@ class JpaCodeBlocks {
Class<?> actualReturnType = isProjecting ? context.getActualReturnType().toClass()
: context.getRepositoryInformation().getDomainType();
String dynamicReturnType = null;
if (queryMethod.getParameters().hasDynamicProjection()) {
dynamicReturnType = context.getParameterName(queryMethod.getParameters().getDynamicProjectionIndex());
}
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
@@ -159,15 +167,16 @@ class JpaCodeBlocks {
sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName());
}
if (StringUtils.hasText(sortParameterName) && queries.result() instanceof StringAotQuery) {
builder.add(applySorting(sortParameterName, queryStringNameVariableName, actualReturnType));
if ((StringUtils.hasText(sortParameterName) || StringUtils.hasText(dynamicReturnType))
&& queries.result() instanceof StringAotQuery) {
builder.add(applyRewrite(sortParameterName, dynamicReturnType, queryStringNameVariableName, actualReturnType));
}
if (queries.result().hasExpression() || queries.count().hasExpression()) {
builder.addStatement("class ExpressionMarker{}");
}
builder.add(createQuery(queryVariableName, queryStringNameVariableName, queries.result(),
builder.add(createQuery(false, queryVariableName, queryStringNameVariableName, queries.result(),
this.sqlResultSetMapping, this.queryHints, this.entityGraph, this.queryReturnType));
builder.add(applyLimits(queries.result().isExists()));
@@ -178,7 +187,7 @@ class JpaCodeBlocks {
boolean queryHints = this.queryHints.isPresent() && this.queryHints.getBoolean("forCounting");
builder.add(createQuery(countQueryVariableName, countQueryStringNameVariableName, queries.count(), null,
builder.add(createQuery(true, countQueryVariableName, countQueryStringNameVariableName, queries.count(), null,
queryHints ? this.queryHints : MergedAnnotation.missing(), null, Long.class));
builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQueryVariableName);
@@ -190,16 +199,33 @@ class JpaCodeBlocks {
return builder.build();
}
private CodeBlock applySorting(String sort, String queryString, Class<?> actualReturnType) {
private CodeBlock applyRewrite(@Nullable String sort, @Nullable String dynamicReturnType, String queryString,
Class<?> actualReturnType) {
Builder builder = CodeBlock.builder();
builder.beginControlFlow("if ($L.isSorted())", sort);
boolean hasSort = StringUtils.hasText(sort);
if (hasSort) {
builder.beginControlFlow("if ($L.isSorted())", sort);
}
builder.addStatement("$T declaredQuery = $T.$L($L)", DeclaredQuery.class, DeclaredQuery.class,
queries != null && queries.isNative() ? "nativeQuery" : "jpqlQuery", queryString);
builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType);
builder.endControlFlow();
boolean hasDynamicReturnType = StringUtils.hasText(dynamicReturnType);
if (hasSort && hasDynamicReturnType) {
builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $L)", queryString, sort, dynamicReturnType);
} else if (hasSort) {
builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType);
} else if (hasDynamicReturnType) {
builder.addStatement("$L = rewriteQuery(declaredQuery, $T.unsorted(), $L)", queryString, Sort.class,
dynamicReturnType);
}
if (hasSort) {
builder.endControlFlow();
}
return builder.build();
}
@@ -241,14 +267,14 @@ class JpaCodeBlocks {
return builder.build();
}
private CodeBlock createQuery(String queryVariableName, @Nullable String queryStringNameVariableName,
private CodeBlock createQuery(boolean count, String queryVariableName, @Nullable String queryStringNameVariableName,
AotQuery query, @Nullable String sqlResultSetMapping, MergedAnnotation<QueryHints> queryHints,
@Nullable AotEntityGraph entityGraph, @Nullable Class<?> queryReturnType) {
Builder builder = CodeBlock.builder();
builder.add(
doCreateQuery(queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping, queryReturnType));
builder.add(doCreateQuery(count, queryVariableName, queryStringNameVariableName, query, sqlResultSetMapping,
queryReturnType));
if (entityGraph != null) {
builder.add(applyEntityGraph(entityGraph, queryVariableName));
@@ -279,12 +305,14 @@ class JpaCodeBlocks {
return builder.build();
}
private CodeBlock doCreateQuery(String queryVariableName, @Nullable String queryStringNameVariableName,
AotQuery query, @Nullable String sqlResultSetMapping, @Nullable Class<?> queryReturnType) {
private CodeBlock doCreateQuery(boolean count, String queryVariableName,
@Nullable String queryStringNameVariableName, AotQuery query, @Nullable String sqlResultSetMapping,
@Nullable Class<?> queryReturnType) {
ReturnedType returnedType = context.getReturnedType();
Builder builder = CodeBlock.builder();
if (query instanceof StringAotQuery) {
if (query instanceof StringAotQuery sq) {
if (StringUtils.hasText(sqlResultSetMapping)) {
@@ -294,24 +322,48 @@ class JpaCodeBlocks {
return builder.build();
}
if (query.isNative() && queryReturnType != null) {
if (query.isNative()) {
builder.addStatement("$T $L = this.$L.createNativeQuery($L, $T.class)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), queryStringNameVariableName, queryReturnType);
if (queryReturnType != null) {
builder.addStatement("$T $L = this.$L.createNativeQuery($L, $T.class)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), queryStringNameVariableName, queryReturnType);
} else {
builder.addStatement("$T $L = this.$L.createNativeQuery($L)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), queryStringNameVariableName);
}
return builder.build();
}
builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery",
queryStringNameVariableName);
if (sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting()
&& returnedType.getReturnedType().isInterface()) {
builder.addStatement("$T $L = this.$L.createQuery($L)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), queryStringNameVariableName);
} else {
String createQueryMethod = query.isNative() ? "createNativeQuery" : "createQuery";
if (!sq.hasConstructorExpressionOrDefaultProjection() && !count && returnedType.isProjecting()
&& returnedType.getReturnedType().isInterface()) {
builder.addStatement("$T $L = this.$L.$L($L, $T.class)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameVariableName, Tuple.class);
} else {
builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), createQueryMethod, queryStringNameVariableName);
}
}
return builder.build();
}
if (query instanceof NamedAotQuery nq) {
if (queryReturnType != null) {
if (!count && returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) {
builder.addStatement("$T $L = this.$L.createNamedQuery($S)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), nq.getName());
return builder.build();
} else if (queryReturnType != null) {
builder.addStatement("$T $L = this.$L.createNamedQuery($S, $T.class)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), nq.getName(), queryReturnType);
@@ -512,30 +564,68 @@ class JpaCodeBlocks {
builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName);
} else {
if (queryMethod.isCollectionQuery()) {
builder.addStatement("return ($T) query.getResultList()", context.getReturnTypeName());
} else if (queryMethod.isStreamQuery()) {
builder.addStatement("return ($T) query.getResultStream()", context.getReturnTypeName());
} else if (queryMethod.isPageQuery()) {
builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)",
PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName,
context.getPageableParameterName());
} else if (queryMethod.isSliceQuery()) {
builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType,
queryVariableName);
builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()",
context.getPageableParameterName(), context.getPageableParameterName());
builder.addStatement(
"return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)",
SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName());
if (context.getReturnedType().isProjecting()) {
TypeName queryResultType = TypeName.get(context.getActualReturnType().toClass());
if (queryMethod.isCollectionQuery()) {
builder.addStatement("return ($T) convertMany(query.getResultList(), $L, $T.class)",
context.getReturnTypeName(), aotQuery.isNative(), queryResultType);
} else if (queryMethod.isStreamQuery()) {
builder.addStatement("return ($T) convertMany(query.getResultStream(), $L, $T.class)",
context.getReturnTypeName(), aotQuery.isNative(), queryResultType);
} else if (queryMethod.isPageQuery()) {
builder.addStatement(
"return $T.getPage(($T<$T>) convertMany($L.getResultList(), $L, $T.class), $L, countAll)",
PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, aotQuery.isNative(),
queryResultType, context.getPageableParameterName());
} else if (queryMethod.isSliceQuery()) {
builder.addStatement("$T<$T> resultList = ($T<$T>) convertMany($L.getResultList(), $L, $T.class)",
List.class, actualReturnType, List.class, actualReturnType, queryVariableName, aotQuery.isNative(),
queryResultType);
builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()",
context.getPageableParameterName(), context.getPageableParameterName());
builder.addStatement(
"return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)",
SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName());
} else {
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
builder.addStatement("return $T.ofNullable(($T) convertOne($L.getSingleResultOrNull(), $L, $T.class))",
Optional.class, actualReturnType, queryVariableName, aotQuery.isNative(), queryResultType);
} else {
builder.addStatement("return ($T) convertOne($L.getSingleResultOrNull(), $L, $T.class)",
context.getReturnTypeName(), queryVariableName, aotQuery.isNative(), queryResultType);
}
}
} else {
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class,
actualReturnType, queryVariableName);
} else {
builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnTypeName(),
if (queryMethod.isCollectionQuery()) {
builder.addStatement("return ($T) query.getResultList()", context.getReturnTypeName());
} else if (queryMethod.isStreamQuery()) {
builder.addStatement("return ($T) query.getResultStream()", context.getReturnTypeName());
} else if (queryMethod.isPageQuery()) {
builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)",
PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName,
context.getPageableParameterName());
} else if (queryMethod.isSliceQuery()) {
builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType,
queryVariableName);
builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()",
context.getPageableParameterName(), context.getPageableParameterName());
builder.addStatement(
"return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)",
SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName());
} else {
if (Optional.class.isAssignableFrom(context.getReturnType().toClass())) {
builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class,
actualReturnType, queryVariableName);
} else {
builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnTypeName(),
queryVariableName);
}
}
}
}

View File

@@ -15,46 +15,22 @@
*/
package org.springframework.data.jpa.repository.aot;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQueryReference;
import jakarta.persistence.metamodel.Metamodel;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.EntityQuery;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.JpaCountQueryCreator;
import org.springframework.data.jpa.repository.query.JpaParameters;
import org.springframework.data.jpa.repository.query.JpaQueryCreator;
import org.springframework.data.jpa.repository.query.JpaQueryMethod;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata;
import org.springframework.data.repository.aot.generate.MethodContributor;
@@ -64,14 +40,11 @@ import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.util.TypeInformation;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* JPA-specific {@link RepositoryContributor} contributing an AOT repository fragment using the {@link EntityManager}
@@ -85,23 +58,18 @@ import org.springframework.util.StringUtils;
*/
public class JpaRepositoryContributor extends RepositoryContributor {
private final EntityManagerFactory emf;
private final Metamodel metaModel;
private final PersistenceProvider persistenceProvider;
private final QueriesFactory queriesFactory;
private final EntityGraphLookup entityGraphLookup;
public JpaRepositoryContributor(AotRepositoryContext repositoryContext) {
super(repositoryContext);
AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes());
this.metaModel = amm;
this.emf = amm.getEntityManagerFactory();
this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory());
}
public JpaRepositoryContributor(AotRepositoryContext repositoryContext, EntityManagerFactory entityManagerFactory) {
super(repositoryContext);
this.emf = entityManagerFactory;
this.metaModel = entityManagerFactory.getMetamodel();
this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(entityManagerFactory);
AotMetamodel amm = new AotMetamodel(repositoryContext.getResolvedTypes());
this.persistenceProvider = PersistenceProvider.fromEntityManagerFactory(amm.getEntityManagerFactory());
this.queriesFactory = new QueriesFactory(amm, amm.getEntityManagerFactory());
this.entityGraphLookup = new EntityGraphLookup(amm.getEntityManagerFactory());
}
@Override
@@ -138,18 +106,15 @@ public class JpaRepositoryContributor extends RepositoryContributor {
}
ReturnedType returnedType = queryMethod.getResultProcessor().getReturnedType();
// no interface/dynamic projections for now.
if (returnedType.isProjecting() && returnedType.getReturnedType().isInterface()) {
return null;
}
if (queryMethod.getParameters().hasDynamicProjection()) {
return null;
}
JpaParameters parameters = queryMethod.getParameters();
// no KeysetScrolling for now.
if (queryMethod.getParameters().hasScrollPositionParameter()) {
if (parameters.hasScrollPositionParameter()) {
return null;
}
// no dynamic projections.
if (parameters.hasDynamicProjection()) {
return null;
}
@@ -178,12 +143,13 @@ public class JpaRepositoryContributor extends RepositoryContributor {
body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
AotQueries aotQueries = getQueries(context, query, selector, queryMethod, returnedType);
AotEntityGraph aotEntityGraph = getAotEntityGraph(entityGraph, repositoryInformation, returnedType, queryMethod);
AotQueries aotQueries = queriesFactory.createQueries(context, query, selector, queryMethod, returnedType);
AotEntityGraph aotEntityGraph = entityGraphLookup.findEntityGraph(entityGraph, repositoryInformation,
returnedType, queryMethod);
body.add(JpaCodeBlocks.queryBuilder(context, queryMethod).filter(aotQueries)
.queryReturnType(getQueryReturnType(aotQueries.result(), returnedType, context)).nativeQuery(nativeQuery)
.queryHints(queryHints).entityGraph(aotEntityGraph).build());
.queryReturnType(QueriesFactory.getQueryReturnType(aotQueries.result(), returnedType, context))
.nativeQuery(nativeQuery).queryHints(queryHints).entityGraph(aotEntityGraph).build());
body.add(
JpaCodeBlocks.executionBuilder(context, queryMethod).modifying(modifying).query(aotQueries.result()).build());
@@ -192,242 +158,4 @@ public class JpaRepositoryContributor extends RepositoryContributor {
});
}
private AotQueries getQueries(AotQueryMethodGenerationContext context, MergedAnnotation<Query> query,
QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) {
if (query.isPresent() && StringUtils.hasText(query.getString("value"))) {
return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query,
queryMethod);
}
TypedQueryReference<?> namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName());
if (namedQuery != null) {
return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod);
}
return buildPartTreeQuery(returnedType, context, query, queryMethod);
}
private AotQueries buildStringQuery(Class<?> domainType, ReturnedType returnedType, QueryEnhancerSelector selector,
MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
UnaryOperator<String> operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName());
boolean isNative = query.getBoolean("nativeQuery");
Function<String, StringAotQuery> queryFunction = isNative ? StringAotQuery::nativeQuery : StringAotQuery::jpqlQuery;
queryFunction = operator.andThen(queryFunction);
String queryString = query.getString("value");
StringAotQuery aotStringQuery = queryFunction.apply(queryString);
String countQuery = query.getString("countQuery");
EntityQuery entityQuery = EntityQuery.create(aotStringQuery.getQuery(), selector);
if (entityQuery.hasConstructorExpression() || entityQuery.isDefaultProjection()) {
aotStringQuery = aotStringQuery.withReturnsDeclaredMethodType();
}
if (StringUtils.hasText(countQuery)) {
return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery));
}
String namedCountQueryName = queryMethod.getNamedCountQueryName();
TypedQueryReference<?> namedCountQuery = getNamedQuery(returnedType, namedCountQueryName);
if (namedCountQuery != null) {
return AotQueries.from(aotStringQuery, buildNamedAotQuery(namedCountQuery, queryMethod, isNative));
}
String countProjection = query.getString("countProjection");
return AotQueries.from(aotStringQuery, countProjection, selector);
}
private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector,
TypedQueryReference<?> namedQuery, MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
NamedAotQuery aotQuery = buildNamedAotQuery(namedQuery, queryMethod,
query.isPresent() && query.getBoolean("nativeQuery"));
String countQuery = query.isPresent() ? query.getString("countQuery") : null;
if (StringUtils.hasText(countQuery)) {
return AotQueries.from(aotQuery,
aotQuery.isNative() ? StringAotQuery.nativeQuery(countQuery) : StringAotQuery.jpqlQuery(countQuery));
}
TypedQueryReference<?> namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName());
if (namedCountQuery != null) {
return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, aotQuery.isNative()));
}
String countProjection = query.isPresent() ? query.getString("countProjection") : null;
return AotQueries.from(aotQuery, it -> {
return StringAotQuery.of(aotQuery.getQueryString()).getQuery();
}, countProjection, selector);
}
private NamedAotQuery buildNamedAotQuery(TypedQueryReference<?> namedQuery, JpaQueryMethod queryMethod,
boolean isNative) {
QueryExtractor queryExtractor = queryMethod.getQueryExtractor();
String queryString = queryExtractor.extractQueryString(namedQuery);
if (!isNative) {
isNative = queryExtractor.isNativeQuery(namedQuery);
}
Assert.hasText(queryString, () -> "Cannot extract Query from named query [%s]".formatted(namedQuery.getName()));
return NamedAotQuery.named(namedQuery.getName(),
isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString));
}
private @Nullable TypedQueryReference<?> getNamedQuery(ReturnedType returnedType, String queryName) {
List<Class<?>> candidates = Arrays.asList(Object.class, returnedType.getDomainType(),
returnedType.getReturnedType(), returnedType.getTypeToRead(), void.class, null, Long.class, Integer.class,
Long.TYPE, Integer.TYPE, Number.class);
for (Class<?> candidate : candidates) {
Map<String, ? extends TypedQueryReference<?>> namedQueries = emf.getNamedQueries(candidate);
if (namedQueries.containsKey(queryName)) {
return namedQueries.get(queryName);
}
}
return null;
}
private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context,
MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType());
// TODO make configurable
JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates);
if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) {
return AotQueries.from(aotQuery, StringAotQuery.jpqlQuery(query.getString("countQuery")));
}
TypedQueryReference<?> namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName());
if (namedCountQuery != null) {
return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, false));
}
AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates);
return AotQueries.from(aotQuery, partTreeCountQuery);
}
private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters,
JpqlQueryTemplates templates) {
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
templates);
JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metaModel);
return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(),
partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection());
}
private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters,
JpqlQueryTemplates templates) {
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
templates);
JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates,
metaModel);
return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), null, false, false);
}
private static @Nullable Class<?> getQueryReturnType(AotQuery query, ReturnedType returnedType,
AotQueryMethodGenerationContext context) {
Method method = context.getMethod();
RepositoryInformation repositoryInformation = context.getRepositoryInformation();
Class<?> methodReturnType = repositoryInformation.getReturnedDomainClass(method);
boolean queryForEntity = repositoryInformation.getDomainType().isAssignableFrom(methodReturnType);
Class<?> result = queryForEntity ? returnedType.getDomainType() : null;
if (query instanceof StringAotQuery sq && sq.returnsDeclaredMethodType()) {
return result;
}
if (returnedType.isProjecting()) {
if (returnedType.getReturnedType().isInterface()) {
return Tuple.class;
}
return returnedType.getReturnedType();
}
return result;
}
@SuppressWarnings("unchecked")
private @Nullable AotEntityGraph getAotEntityGraph(MergedAnnotation<EntityGraph> entityGraph,
RepositoryInformation information, ReturnedType returnedType, JpaQueryMethod queryMethod) {
if (!entityGraph.isPresent()) {
return null;
}
EntityGraph.EntityGraphType type = entityGraph.getEnum("type", EntityGraph.EntityGraphType.class);
String[] attributePaths = entityGraph.getStringArray("attributePaths");
Collection<String> entityGraphNames = getEntityGraphNames(entityGraph, information, queryMethod);
List<Class<?>> candidates = Arrays.asList(returnedType.getDomainType(), returnedType.getReturnedType(),
returnedType.getTypeToRead());
for (Class<?> candidate : candidates) {
Map<String, jakarta.persistence.EntityGraph<?>> namedEntityGraphs = emf
.getNamedEntityGraphs(Class.class.cast(candidate));
if (namedEntityGraphs.isEmpty()) {
continue;
}
for (String entityGraphName : entityGraphNames) {
if (namedEntityGraphs.containsKey(entityGraphName)) {
return new AotEntityGraph(entityGraphName, type, Collections.emptyList());
}
}
}
if (attributePaths.length > 0) {
return new AotEntityGraph(null, type, Arrays.asList(attributePaths));
}
return null;
}
private Set<String> getEntityGraphNames(MergedAnnotation<EntityGraph> entityGraph, RepositoryInformation information,
JpaQueryMethod queryMethod) {
Set<String> entityGraphNames = new LinkedHashSet<>();
String value = entityGraph.getString("value");
if (StringUtils.hasText(value)) {
entityGraphNames.add(value);
}
entityGraphNames.add(queryMethod.getNamedQueryName());
entityGraphNames.add(getFallbackEntityGraphName(information, queryMethod));
return entityGraphNames;
}
private String getFallbackEntityGraphName(RepositoryInformation information, JpaQueryMethod queryMethod) {
Class<?> domainType = information.getDomainType();
Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class);
String entityName = entity != null && StringUtils.hasText(entity.name()) ? entity.name()
: domainType.getSimpleName();
return entityName + "." + queryMethod.getName();
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright 2025 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.aot;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQueryReference;
import jakarta.persistence.metamodel.Metamodel;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.data.domain.Limit;
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.query.*;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Factory for {@link AotQueries}.
*
* @author Mark Paluch
* @since 4.0
*/
class QueriesFactory {
private final Metamodel metamodel;
private final EntityManagerFactory emf;
public QueriesFactory(AotMetamodel metamodel, EntityManagerFactory emf) {
this.metamodel = metamodel;
this.emf = emf;
}
/**
* Creates the {@link AotQueries} used within a specific {@link JpaQueryMethod}.
*
* @param context
* @param query
* @param selector
* @param queryMethod
* @param returnedType
* @return
*/
public AotQueries createQueries(AotQueryMethodGenerationContext context, MergedAnnotation<Query> query,
QueryEnhancerSelector selector, JpaQueryMethod queryMethod, ReturnedType returnedType) {
if (query.isPresent() && StringUtils.hasText(query.getString("value"))) {
return buildStringQuery(context.getRepositoryInformation().getDomainType(), returnedType, selector, query,
queryMethod);
}
TypedQueryReference<?> namedQuery = getNamedQuery(returnedType, queryMethod.getNamedQueryName());
if (namedQuery != null) {
return buildNamedQuery(returnedType, selector, namedQuery, query, queryMethod);
}
return buildPartTreeQuery(returnedType, context, query, queryMethod);
}
private AotQueries buildStringQuery(Class<?> domainType, ReturnedType returnedType, QueryEnhancerSelector selector,
MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
UnaryOperator<String> operator = s -> s.replaceAll("#\\{#entityName}", domainType.getName());
boolean isNative = query.getBoolean("nativeQuery");
Function<String, StringAotQuery> queryFunction = isNative ? StringAotQuery::nativeQuery : StringAotQuery::jpqlQuery;
queryFunction = operator.andThen(queryFunction);
String queryString = query.getString("value");
StringAotQuery aotStringQuery = queryFunction.apply(queryString);
String countQuery = query.getString("countQuery");
EntityQuery entityQuery = EntityQuery.create(aotStringQuery.getQuery(), selector);
if (entityQuery.hasConstructorExpression() || entityQuery.isDefaultProjection()) {
aotStringQuery = aotStringQuery.withConstructorExpressionOrDefaultProjection();
}
if (returnedType.isProjecting() && returnedType.hasInputProperties()
&& !returnedType.getReturnedType().isInterface()) {
QueryProvider rewritten = entityQuery.rewrite(new QueryEnhancer.QueryRewriteInformation() {
@Override
public Sort getSort() {
return Sort.unsorted();
}
@Override
public ReturnedType getReturnedType() {
return returnedType;
}
});
aotStringQuery = aotStringQuery.rewrite(rewritten);
}
if (StringUtils.hasText(countQuery)) {
return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery));
}
String namedCountQueryName = queryMethod.getNamedCountQueryName();
TypedQueryReference<?> namedCountQuery = getNamedQuery(returnedType, namedCountQueryName);
if (namedCountQuery != null) {
return AotQueries.from(aotStringQuery, buildNamedAotQuery(namedCountQuery, queryMethod, isNative));
}
String countProjection = query.getString("countProjection");
return AotQueries.from(aotStringQuery, countProjection, selector);
}
private AotQueries buildNamedQuery(ReturnedType returnedType, QueryEnhancerSelector selector,
TypedQueryReference<?> namedQuery, MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
NamedAotQuery aotQuery = buildNamedAotQuery(namedQuery, queryMethod,
query.isPresent() && query.getBoolean("nativeQuery"));
String countQuery = query.isPresent() ? query.getString("countQuery") : null;
if (StringUtils.hasText(countQuery)) {
return AotQueries.from(aotQuery,
aotQuery.isNative() ? StringAotQuery.nativeQuery(countQuery) : StringAotQuery.jpqlQuery(countQuery));
}
TypedQueryReference<?> namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName());
if (namedCountQuery != null) {
return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, aotQuery.isNative()));
}
String countProjection = query.isPresent() ? query.getString("countProjection") : null;
return AotQueries.from(aotQuery, it -> {
return StringAotQuery.of(aotQuery.getQueryString()).getQuery();
}, countProjection, selector);
}
private NamedAotQuery buildNamedAotQuery(TypedQueryReference<?> namedQuery, JpaQueryMethod queryMethod,
boolean isNative) {
QueryExtractor queryExtractor = queryMethod.getQueryExtractor();
String queryString = queryExtractor.extractQueryString(namedQuery);
if (!isNative) {
isNative = queryExtractor.isNativeQuery(namedQuery);
}
Assert.hasText(queryString, () -> "Cannot extract Query from named query [%s]".formatted(namedQuery.getName()));
return NamedAotQuery.named(namedQuery.getName(),
isNative ? DeclaredQuery.nativeQuery(queryString) : DeclaredQuery.jpqlQuery(queryString));
}
private @Nullable TypedQueryReference<?> getNamedQuery(ReturnedType returnedType, String queryName) {
List<Class<?>> candidates = Arrays.asList(Object.class, returnedType.getDomainType(),
returnedType.getReturnedType(), returnedType.getTypeToRead(), void.class, null, Long.class, Integer.class,
Long.TYPE, Integer.TYPE, Number.class);
for (Class<?> candidate : candidates) {
Map<String, ? extends TypedQueryReference<?>> namedQueries = emf.getNamedQueries(candidate);
if (namedQueries.containsKey(queryName)) {
return namedQueries.get(queryName);
}
}
return null;
}
private AotQueries buildPartTreeQuery(ReturnedType returnedType, AotQueryMethodGenerationContext context,
MergedAnnotation<Query> query, JpaQueryMethod queryMethod) {
PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType());
// TODO make configurable
JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
AotQuery aotQuery = createQuery(partTree, returnedType, queryMethod.getParameters(), templates);
if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) {
return AotQueries.from(aotQuery, StringAotQuery.jpqlQuery(query.getString("countQuery")));
}
TypedQueryReference<?> namedCountQuery = getNamedQuery(returnedType, queryMethod.getNamedCountQueryName());
if (namedCountQuery != null) {
return AotQueries.from(aotQuery, buildNamedAotQuery(namedCountQuery, queryMethod, false));
}
AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, queryMethod.getParameters(), templates);
return AotQueries.from(aotQuery, partTreeCountQuery);
}
private AotQuery createQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters,
JpqlQueryTemplates templates) {
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
templates);
JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider, templates, metamodel);
return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(),
partTree.getResultLimit(), partTree.isDelete(), partTree.isExistsProjection());
}
private AotQuery createCountQuery(PartTree partTree, ReturnedType returnedType, JpaParameters parameters,
JpqlQueryTemplates templates) {
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
templates);
JpaQueryCreator queryCreator = new JpaCountQueryCreator(partTree, returnedType, metadataProvider, templates,
metamodel);
return StringAotQuery.jpqlQuery(queryCreator.createQuery(), metadataProvider.getBindings(), Limit.unlimited(),
false, false);
}
public static @Nullable Class<?> getQueryReturnType(AotQuery query, ReturnedType returnedType,
AotQueryMethodGenerationContext context) {
Method method = context.getMethod();
RepositoryInformation repositoryInformation = context.getRepositoryInformation();
Class<?> methodReturnType = repositoryInformation.getReturnedDomainClass(method);
boolean queryForEntity = repositoryInformation.getDomainType().isAssignableFrom(methodReturnType);
Class<?> result = queryForEntity ? returnedType.getDomainType() : null;
if (query instanceof StringAotQuery sq && sq.hasConstructorExpressionOrDefaultProjection()) {
return result;
}
if (returnedType.isProjecting()) {
if (returnedType.getReturnedType().isInterface()) {
return Tuple.class;
}
return returnedType.getReturnedType();
}
return result;
}
}

View File

@@ -21,6 +21,7 @@ import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.ParameterBinding;
import org.springframework.data.jpa.repository.query.PreprocessedQuery;
import org.springframework.data.jpa.repository.query.QueryProvider;
/**
* An AOT query represented by a string.
@@ -59,7 +60,7 @@ abstract class StringAotQuery extends AotQuery {
*/
public static StringAotQuery jpqlQuery(String queryString, List<ParameterBinding> bindings, Limit resultLimit,
boolean delete, boolean exists) {
return new LimitedAotQuery(queryString, bindings, resultLimit, delete, exists);
return new DerivedAotQuery(queryString, bindings, resultLimit, delete, exists);
}
/**
@@ -83,28 +84,34 @@ abstract class StringAotQuery extends AotQuery {
* @return {@literal true} if query is expected to return the declared method type directly; {@literal false} if the
* result requires projection post-processing. See also {@code NativeJpaQuery#getTypeToQueryFor}.
*/
public abstract boolean returnsDeclaredMethodType();
public abstract boolean hasConstructorExpressionOrDefaultProjection();
public abstract StringAotQuery withReturnsDeclaredMethodType();
/**
* @return a new {@link StringAotQuery} using constructor expressions or containing the default (primary alias)
* projection.
*/
public abstract StringAotQuery withConstructorExpressionOrDefaultProjection();
@Override
public String toString() {
return getQueryString();
}
public abstract StringAotQuery rewrite(QueryProvider rewritten);
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
static class DeclaredAotQuery extends StringAotQuery {
private static class DeclaredAotQuery extends StringAotQuery {
private final PreprocessedQuery query;
private final boolean returnsDeclaredMethodType;
private final boolean constructorExpressionOrDefaultProjection;
DeclaredAotQuery(PreprocessedQuery query, boolean returnsDeclaredMethodType) {
DeclaredAotQuery(PreprocessedQuery query, boolean constructorExpressionOrDefaultProjection) {
super(query.getBindings());
this.query = query;
this.returnsDeclaredMethodType = returnsDeclaredMethodType;
this.constructorExpressionOrDefaultProjection = constructorExpressionOrDefaultProjection;
}
@Override
@@ -123,30 +130,35 @@ abstract class StringAotQuery extends AotQuery {
}
@Override
public boolean returnsDeclaredMethodType() {
return returnsDeclaredMethodType;
public boolean hasConstructorExpressionOrDefaultProjection() {
return constructorExpressionOrDefaultProjection;
}
@Override
public StringAotQuery withReturnsDeclaredMethodType() {
return new DeclaredAotQuery(query, returnsDeclaredMethodType);
public StringAotQuery withConstructorExpressionOrDefaultProjection() {
return new DeclaredAotQuery(query, true);
}
@Override
public StringAotQuery rewrite(QueryProvider rewritten) {
return new DeclaredAotQuery(query.rewrite(rewritten.getQueryString()), constructorExpressionOrDefaultProjection);
}
}
/**
* Query with a limit associated.
* PartTree (derived) Query with a limit associated.
*
* @author Mark Paluch
*/
static class LimitedAotQuery extends StringAotQuery {
private static class DerivedAotQuery extends StringAotQuery {
private final String queryString;
private final Limit limit;
private final boolean delete;
private final boolean exists;
LimitedAotQuery(String queryString, List<ParameterBinding> parameterBindings, Limit limit, boolean delete,
DerivedAotQuery(String queryString, List<ParameterBinding> parameterBindings, Limit limit, boolean delete,
boolean exists) {
super(parameterBindings);
this.queryString = queryString;
@@ -186,14 +198,19 @@ abstract class StringAotQuery extends AotQuery {
}
@Override
public boolean returnsDeclaredMethodType() {
return true;
public boolean hasConstructorExpressionOrDefaultProjection() {
return false;
}
@Override
public StringAotQuery withReturnsDeclaredMethodType() {
public StringAotQuery withConstructorExpressionOrDefaultProjection() {
return this;
}
@Override
public StringAotQuery rewrite(QueryProvider rewritten) {
return new DerivedAotQuery(rewritten.getQueryString(), this.getParameterBindings(), getLimit(), delete, exists);
}
}
}

View File

@@ -25,18 +25,13 @@ import jakarta.persistence.TypedQuery;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.springframework.beans.BeanUtils;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.provider.PersistenceProvider;
@@ -50,13 +45,13 @@ import org.springframework.data.jpa.repository.query.JpaQueryExecution.SlicedExe
import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution;
import org.springframework.data.jpa.repository.support.QueryHints;
import org.springframework.data.jpa.util.JpaMetamodel;
import org.springframework.data.jpa.util.TupleBackedMap;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.model.PreferredConstructorDiscoverer;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.util.Lazy;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -344,7 +339,7 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
Assert.notNull(type, "Returned type must not be null");
this.type = type;
this.tupleWrapper = nativeQuery ? FallbackTupleWrapper::new : UnaryOperator.identity();
this.tupleWrapper = nativeQuery ? TupleBackedMap::underscoreAware : UnaryOperator.identity();
this.dtoProjection = type.isProjecting() && !type.getReturnedType().isInterface()
&& !type.getInputProperties().isEmpty();
@@ -468,180 +463,6 @@ public abstract class AbstractJpaQuery implements RepositoryQuery {
return ClassUtils.isAssignable(to, from);
}
/**
* A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided
* {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the
* key/entry set.
*
* @author Jens Schauder
*/
private static class TupleBackedMap implements Map<String, Object> {
private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified";
private final Tuple tuple;
TupleBackedMap(Tuple tuple) {
this.tuple = tuple;
}
@Override
public int size() {
return tuple.getElements().size();
}
@Override
public boolean isEmpty() {
return tuple.getElements().isEmpty();
}
/**
* If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}.
* Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}.
*
* @param key the key for which to get the value from the map.
* @return whether the key is an element of the backing tuple.
*/
@Override
public boolean containsKey(Object key) {
try {
tuple.get((String) key);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
@Override
public boolean containsValue(Object value) {
return Arrays.asList(tuple.toArray()).contains(value);
}
/**
* If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}.
* Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}.
*
* @param key the key for which to get the value from the map.
* @return the value of the backing {@link Tuple} for that key or {@code null}.
*/
@Override
public @Nullable Object get(Object key) {
if (!(key instanceof String)) {
return null;
}
try {
return tuple.get((String) key);
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public Object put(String key, Object value) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public Object remove(Object key) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public void putAll(Map<? extends String, ?> m) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public void clear() {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public Set<String> keySet() {
return tuple.getElements().stream() //
.map(TupleElement::getAlias) //
.collect(Collectors.toSet());
}
@Override
public Collection<Object> values() {
return Arrays.asList(tuple.toArray());
}
@Override
public Set<Entry<String, Object>> entrySet() {
return tuple.getElements().stream() //
.map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e))) //
.collect(Collectors.toSet());
}
}
}
private static class FallbackTupleWrapper implements Tuple {
private final Tuple delegate;
private final UnaryOperator<String> fallbackNameTransformer = JdbcUtils::convertPropertyNameToUnderscoreName;
FallbackTupleWrapper(Tuple delegate) {
this.delegate = delegate;
}
@Override
public <X> X get(TupleElement<X> tupleElement) {
return get(tupleElement.getAlias(), tupleElement.getJavaType());
}
@Override
public <X> X get(String s, Class<X> type) {
try {
return delegate.get(s, type);
} catch (IllegalArgumentException original) {
try {
return delegate.get(fallbackNameTransformer.apply(s), type);
} catch (IllegalArgumentException next) {
original.addSuppressed(next);
throw original;
}
}
}
@Override
public Object get(String s) {
try {
return delegate.get(s);
} catch (IllegalArgumentException original) {
try {
return delegate.get(fallbackNameTransformer.apply(s));
} catch (IllegalArgumentException next) {
original.addSuppressed(next);
throw original;
}
}
}
@Override
public <X> X get(int i, Class<X> aClass) {
return delegate.get(i, aClass);
}
@Override
public Object get(int i) {
return delegate.get(i);
}
@Override
public Object[] toArray() {
return delegate.toArray();
}
@Override
public List<TupleElement<?>> getElements() {
return delegate.getElements();
}
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2025 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.util;
import jakarta.persistence.Tuple;
import jakarta.persistence.TupleElement;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.jdbc.support.JdbcUtils;
/**
* A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided {@link Tuple}
* implementation it might return the same value for various keys of which only one will appear in the key/entry set.
*
* @author Jens Schauder
* @since 4.0
*/
public class TupleBackedMap implements Map<String, Object> {
private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified";
private final Tuple tuple;
public TupleBackedMap(Tuple tuple) {
this.tuple = tuple;
}
/**
* Creates a underscore-aware {@link Tuple} wrapper applying {@link JdbcUtils#convertPropertyNameToUnderscoreName}
* conversion to leniently look up properties from query results whose columns follow snake-case syntax.
*
* @param delegate the tuple to wrap.
* @return
*/
public static Tuple underscoreAware(Tuple delegate) {
return new FallbackTupleWrapper(delegate);
}
@Override
public int size() {
return tuple.getElements().size();
}
@Override
public boolean isEmpty() {
return tuple.getElements().isEmpty();
}
/**
* If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. Otherwise
* this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}.
*
* @param key the key for which to get the value from the map.
* @return whether the key is an element of the backing tuple.
*/
@Override
public boolean containsKey(Object key) {
try {
tuple.get((String) key);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
@Override
public boolean containsValue(Object value) {
return Arrays.asList(tuple.toArray()).contains(value);
}
/**
* If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. Otherwise
* the value from the backing {@code Tuple} is returned, which also might be {@code null}.
*
* @param key the key for which to get the value from the map.
* @return the value of the backing {@link Tuple} for that key or {@code null}.
*/
@Override
public @Nullable Object get(Object key) {
if (!(key instanceof String)) {
return null;
}
try {
return tuple.get((String) key);
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public Object put(String key, Object value) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public Object remove(Object key) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public void putAll(Map<? extends String, ?> m) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public void clear() {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}
@Override
public Set<String> keySet() {
return tuple.getElements().stream() //
.map(TupleElement::getAlias) //
.collect(Collectors.toSet());
}
@Override
public Collection<Object> values() {
return Arrays.asList(tuple.toArray());
}
@Override
public Set<Entry<String, Object>> entrySet() {
return tuple.getElements().stream() //
.map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e))) //
.collect(Collectors.toSet());
}
static class FallbackTupleWrapper implements Tuple {
private final Tuple delegate;
private final UnaryOperator<String> fallbackNameTransformer = JdbcUtils::convertPropertyNameToUnderscoreName;
FallbackTupleWrapper(Tuple delegate) {
this.delegate = delegate;
}
@Override
public <X> X get(TupleElement<X> tupleElement) {
return get(tupleElement.getAlias(), tupleElement.getJavaType());
}
@Override
public <X> X get(String s, Class<X> type) {
try {
return delegate.get(s, type);
} catch (IllegalArgumentException original) {
try {
return delegate.get(fallbackNameTransformer.apply(s), type);
} catch (IllegalArgumentException next) {
original.addSuppressed(next);
throw original;
}
}
}
@Override
public Object get(String s) {
try {
return delegate.get(s);
} catch (IllegalArgumentException original) {
try {
return delegate.get(fallbackNameTransformer.apply(s));
} catch (IllegalArgumentException next) {
original.addSuppressed(next);
throw original;
}
}
}
@Override
public <X> X get(int i, Class<X> aClass) {
return delegate.get(i, aClass);
}
@Override
public Object get(int i) {
return delegate.get(i);
}
@Override
public Object[] toArray() {
return delegate.toArray();
}
@Override
public List<TupleElement<?>> getElements() {
return delegate.getElements();
}
}
}

View File

@@ -24,6 +24,7 @@ import java.util.Optional;
import java.util.stream.Stream;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.query.QueryTypeMismatchException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -35,6 +36,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.domain.sample.SpecialUser;
import org.springframework.data.jpa.domain.sample.User;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
@@ -100,6 +102,13 @@ class JpaRepositoryContributorIntegrationTests {
em.clear();
}
@Test
void testDerivedFinderWithoutArguments() {
List<User> users = fragment.findUserNoArgumentsBy();
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
}
@Test
void testFindDerivedQuerySingleEntity() {
@@ -107,40 +116,6 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(user.getLastname()).isEqualTo("Skywalker");
}
@Test
void shouldUseNamedQuery() {
User user = fragment.findByEmailAddress("luke@jedi.org");
assertThat(user.getLastname()).isEqualTo("Skywalker");
}
@Test
void shouldUseNamedQueryAndDeriveCountQuery() {
Page<User> user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
assertThat(user).hasSize(1);
assertThat(user.getTotalElements()).isEqualTo(1);
}
@Test
void shouldUseNamedQueryAndProvidedCountQuery() {
Page<User> user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
assertThat(user).hasSize(1);
assertThat(user.getTotalElements()).isEqualTo(1);
}
@Test
void shouldUseNamedQueryAndNamedCountQuery() {
Page<User> user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
assertThat(user).hasSize(1);
assertThat(user.getTotalElements()).isEqualTo(1);
}
@Test
void testFindDerivedFinderOptionalEntity() {
@@ -163,13 +138,6 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(exists).isTrue();
}
@Test
void testDerivedFinderWithoutArguments() {
List<User> users = fragment.findUserNoArgumentsBy();
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
}
@Test
void testDerivedFinderReturningList() {
@@ -397,33 +365,6 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
@Test
void shouldApplyQueryHints() {
assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker"))
.withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo");
}
@Test
void shouldApplyNamedEntityGraph() {
User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca");
assertThat(chewie.getManager()).isInstanceOf(HibernateProxy.class);
assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class);
}
@Test
void shouldApplyDeclaredEntityGraph() {
User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca");
assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class);
User han = chewie.getManager();
assertThat(han.getRoles()).isNotInstanceOf(HibernateProxy.class);
assertThat(han.getManager()).isInstanceOf(HibernateProxy.class);
}
@Test
void testDerivedFinderReturningPageOfProjections() {
@@ -441,7 +382,128 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(noResults).isEmpty();
}
// modifying
@Test
void shouldApplySqlResultSetMapping() {
User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId());
assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyNamedDto() {
// named queries cannot be rewritten
assertThatExceptionOfType(QueryTypeMismatchException.class)
.isThrownBy(() -> fragment.findNamedDtoEmailAddress(kylo.getEmailAddress()));
}
@Test
void shouldApplyDerivedDto() {
UserRepository.Names names = fragment.findDtoByEmailAddress(kylo.getEmailAddress());
assertThat(names.lastname()).isEqualTo(kylo.getLastname());
assertThat(names.firstname()).isEqualTo(kylo.getFirstname());
}
@Test
void shouldApplyDerivedDtoPage() {
Page<UserRepository.Names> names = fragment.findDtoPageByEmailAddress(kylo.getEmailAddress(), PageRequest.of(0, 1));
assertThat(names).hasSize(1);
assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname());
}
@Test
void shouldApplyAnnotatedDto() {
UserRepository.Names names = fragment.findAnnotatedDtoEmailAddress(kylo.getEmailAddress());
assertThat(names.lastname()).isEqualTo(kylo.getLastname());
assertThat(names.firstname()).isEqualTo(kylo.getFirstname());
}
@Test
void shouldApplyAnnotatedDtoPage() {
Page<UserRepository.Names> names = fragment.findAnnotatedDtoPageByEmailAddress(kylo.getEmailAddress(),
PageRequest.of(0, 1));
assertThat(names).hasSize(1);
assertThat(names.getContent().get(0).lastname()).isEqualTo(kylo.getLastname());
}
@Test
void shouldApplyDerivedQueryInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findEmailProjectionById(kylo.getId());
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyInterfaceProjectionPage() {
Page<UserRepository.EmailOnly> result = fragment.findProjectedPageByEmailAddress(kylo.getEmailAddress(),
PageRequest.of(0, 1));
assertThat(result).hasSize(1);
assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyInterfaceProjectionSlice() {
Slice<UserRepository.EmailOnly> result = fragment.findProjectedSliceByEmailAddress(kylo.getEmailAddress(),
PageRequest.of(0, 1));
assertThat(result).hasSize(1);
assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyInterfaceProjectionToDerivedQueryStream() {
Stream<UserRepository.EmailOnly> result = fragment.streamProjectedByEmailAddress(kylo.getEmailAddress());
assertThat(result).hasSize(1).map(UserRepository.EmailOnly::getEmailAddress).contains(kylo.getEmailAddress());
}
@Test
void shouldApplyAnnotatedQueryInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findAnnotatedEmailProjectionByEmailAddress(kylo.getEmailAddress());
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyAnnotatedInterfaceProjectionQueryPage() {
Page<UserRepository.EmailOnly> result = fragment.findAnnotatedProjectedPageByEmailAddress(kylo.getEmailAddress(),
PageRequest.of(0, 1));
assertThat(result).hasSize(1);
assertThat(result.getContent().get(0).getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyNativeInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findEmailProjectionByNativeQuery(kylo.getId());
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void shouldApplyNamedQueryInterfaceProjection() {
UserRepository.EmailOnly result = fragment.findNamedProjectionEmailAddress(kylo.getEmailAddress());
assertThat(result.getEmailAddress()).isEqualTo(kylo.getEmailAddress());
}
@Test
void testDerivedDeleteSingle() {
@@ -476,8 +538,6 @@ class JpaRepositoryContributorIntegrationTests {
assertThat(yodaShouldBeGone).isNull();
}
// native queries
@Test
void nativeQuery() {
@@ -489,22 +549,86 @@ class JpaRepositoryContributorIntegrationTests {
}
@Test
void shouldApplySqlResultSetMapping() {
void shouldUseNamedQuery() {
User.EmailDto result = fragment.findEmailDtoByNativeQuery(kylo.getId());
assertThat(result.getOne()).isEqualTo(kylo.getEmailAddress());
User user = fragment.findByEmailAddress("luke@jedi.org");
assertThat(user.getLastname()).isEqualTo("Skywalker");
}
// old stuff below
@Test
void shouldUseNamedQueryAndDeriveCountQuery() {
Page<User> user = fragment.findPagedByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
assertThat(user).hasSize(1);
assertThat(user.getTotalElements()).isEqualTo(1);
}
@Test
void shouldUseNamedQueryAndProvidedCountQuery() {
Page<User> user = fragment.findPagedWithCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
assertThat(user).hasSize(1);
assertThat(user.getTotalElements()).isEqualTo(1);
}
@Test
void shouldUseNamedQueryAndNamedCountQuery() {
Page<User> user = fragment.findPagedWithNamedCountByEmailAddress(PageRequest.of(0, 1), "luke@jedi.org");
assertThat(user).hasSize(1);
assertThat(user.getTotalElements()).isEqualTo(1);
}
@Test
void shouldApplyQueryHints() {
assertThatIllegalArgumentException().isThrownBy(() -> fragment.findHintedByLastname("Skywalker"))
.withMessageContaining("No enum constant jakarta.persistence.CacheStoreMode.foo");
}
@Test
void shouldApplyNamedEntityGraph() {
User chewie = fragment.findWithNamedEntityGraphByFirstname("Chewbacca");
assertThat(chewie.getManager()).isInstanceOf(HibernateProxy.class);
assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class);
}
@Test
void shouldApplyDeclaredEntityGraph() {
User chewie = fragment.findWithDeclaredEntityGraphByFirstname("Chewbacca");
assertThat(chewie.getRoles()).isNotInstanceOf(HibernateProxy.class);
User han = chewie.getManager();
assertThat(han.getRoles()).isNotInstanceOf(HibernateProxy.class);
assertThat(han.getManager()).isInstanceOf(HibernateProxy.class);
}
@Test
void shouldQuerySubtype() {
SpecialUser snoopy = new SpecialUser();
snoopy.setFirstname("Snoopy");
snoopy.setLastname("n/a");
snoopy.setEmailAddress("dog@home.com");
em.persist(snoopy);
SpecialUser result = fragment.findByEmailAddress("dog@home.com", SpecialUser.class);
assertThat(result).isNotNull();
assertThat(result).isInstanceOf(SpecialUser.class);
}
void todo() {
// interface projections
// dynamic projections
// class type parameter
// dynamic projections: Not implemented
// keyset scrolling
// synthetic parameters (keyset scrolling! yuck!)
}
}

View File

@@ -38,7 +38,6 @@ import org.springframework.data.repository.CrudRepository;
* @author Christoph Strobl
* @author Mark Paluch
*/
// TODO: Querydsl, query by example
interface UserRepository extends CrudRepository<User, Integer> {
List<User> findUserNoArgumentsBy();
@@ -71,7 +70,9 @@ interface UserRepository extends CrudRepository<User, Integer> {
Stream<User> streamByLastnameLike(String lastname);
/* Annotated Queries */
// -------------------------------------------------------------------------
// Declared Queries
// -------------------------------------------------------------------------
@Query("select u from User u where u.emailAddress = ?1")
User findAnnotatedQueryByEmailAddress(String username);
@@ -112,7 +113,9 @@ interface UserRepository extends CrudRepository<User, Integer> {
@Query("select u from User u where u.lastname like ?1%")
Slice<User> findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable);
// -------------------------------------------------------------------------
// Value Expressions
// -------------------------------------------------------------------------
@Query("select u from #{#entityName} u where u.emailAddress = ?1")
User findTemplatedByEmailAddress(String emailAddress);
@@ -123,11 +126,58 @@ interface UserRepository extends CrudRepository<User, Integer> {
@Query("select u from User u where u.emailAddress = ?#{[0]} or u.firstname = ?${user.dir}")
User findValueExpressionPositionalByEmailAddress(String emailAddress);
// -------------------------------------------------------------------------
// Projections: DTO
// -------------------------------------------------------------------------
List<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname);
Page<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
Names findDtoByEmailAddress(String emailAddress);
Page<Names> findDtoPageByEmailAddress(String emailAddress, Pageable pageable);
@Query("select u from User u where u.emailAddress = ?1")
Names findAnnotatedDtoEmailAddress(String emailAddress);
@Query("select u from User u where u.emailAddress = ?1")
Page<Names> findAnnotatedDtoPageByEmailAddress(String emailAddress, Pageable pageable);
@NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1",
sqlResultSetMapping = "emailDto")
User.EmailDto findEmailDtoByNativeQuery(Integer id);
// modifying
@Query(name = "User.findByEmailAddress")
Names findNamedDtoEmailAddress(String emailAddress);
// -------------------------------------------------------------------------
// Projections: Interface
// -------------------------------------------------------------------------
EmailOnly findEmailProjectionById(Integer id);
Page<EmailOnly> findProjectedPageByEmailAddress(String emailAddress, Pageable page);
Slice<EmailOnly> findProjectedSliceByEmailAddress(String lastname, Pageable page);
Stream<EmailOnly> streamProjectedByEmailAddress(String lastname);
@Query("select u from User u where u.emailAddress = ?1")
EmailOnly findAnnotatedEmailProjectionByEmailAddress(String emailAddress);
@Query("select u from User u where u.emailAddress = ?1")
Page<EmailOnly> findAnnotatedProjectedPageByEmailAddress(String emailAddress, Pageable page);
@NativeQuery(value = "SELECT emailaddress as emailAddress FROM SD_User WHERE id = ?1")
EmailOnly findEmailProjectionByNativeQuery(Integer id);
@Query(name = "User.findByEmailAddress")
EmailOnly findNamedProjectionEmailAddress(String emailAddress);
// -------------------------------------------------------------------------
// Modifying
// -------------------------------------------------------------------------
User deleteByEmailAddress(String username);
@@ -136,46 +186,22 @@ interface UserRepository extends CrudRepository<User, Integer> {
@Query("delete from User u where u.emailAddress = ?1")
User deleteAnnotatedQueryByEmailAddress(String username);
// native queries
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("update User u set u.lastname = ?1")
int renameAllUsersTo(String lastname);
// -------------------------------------------------------------------------
// Native Queries
// -------------------------------------------------------------------------
@Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User",
nativeQuery = true)
Page<String> findByNativeQueryWithPageable(Pageable pageable);
// projections
// -------------------------------------------------------------------------
// Named Queries
// -------------------------------------------------------------------------
List<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname);
Page<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
// old ones
@Query("select u from User u where u.firstname = ?1")
List<User> findAllUsingAnnotatedJpqlQuery(String firstname);
List<User> findByLastname(String lastname);
@QueryHints(value = { @QueryHint(name = "jakarta.persistence.cache.storeMode", value = "foo") }, forCounting = false)
List<User> findHintedByLastname(String lastname);
@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "User.overview")
User findWithNamedEntityGraphByFirstname(String firstname);
@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = { "roles", "manager.roles" })
User findWithDeclaredEntityGraphByFirstname(String firstname);
List<User> findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable page);
List<User> findByLastnameOrderByFirstname(String lastname);
/**
* Retrieve users by their email address. The finder {@literal User.findByEmailAddress} is declared as annotation at
* {@code User}.
*/
User findByEmailAddress(String emailAddress);
@Query(name = "User.findByEmailAddress")
@@ -187,8 +213,27 @@ interface UserRepository extends CrudRepository<User, Integer> {
@Query(name = "User.findByEmailAddress", countName = "User.findByEmailAddress.count-provided")
Page<User> findPagedWithNamedCountByEmailAddress(Pageable pageable, String emailAddress);
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("update User u set u.lastname = ?1")
int renameAllUsersTo(String lastname);
// -------------------------------------------------------------------------
// Query Hints
// -------------------------------------------------------------------------
@QueryHints(value = { @QueryHint(name = "jakarta.persistence.cache.storeMode", value = "foo") }, forCounting = false)
List<User> findHintedByLastname(String lastname);
@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "User.overview")
User findWithNamedEntityGraphByFirstname(String firstname);
@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = { "roles", "manager.roles" })
User findWithDeclaredEntityGraphByFirstname(String firstname);
@Query("select u from User u where u.emailAddress = ?1 AND TYPE(u) = ?2")
<T extends User> T findByEmailAddress(String emailAddress, Class<T> type);
interface EmailOnly {
String getEmailAddress();
}
record Names(String firstname, String lastname) {
}
}