@@ -40,6 +40,9 @@ abstract class AotQuery {
|
||||
*/
|
||||
public abstract boolean isNative();
|
||||
|
||||
/**
|
||||
* @return the list of parameter bindings.
|
||||
*/
|
||||
public List<ParameterBinding> getParameterBindings() {
|
||||
return parameterBindings;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user