Refactoring.

Introduce AotRepositoryFragmentSupport, adopt to FragmentCreationContext.
Reduce visibility. Refactor CodeBlocks builder. Simplify query rewriting and use base class methods.
Use typed verifier through a JDK proxy to avoid reflective frontend.
Revise testing to a plain old Spring test but testing the AOT fragment through its interface by forwarding reflective calls to the AOT fragment.
Refactor AotQuery into AotQueries to support a wider range of possible queries.

See #3830
This commit is contained in:
Mark Paluch
2025-03-24 15:52:23 +01:00
parent 831d04dd2c
commit c399ca2b54
20 changed files with 1200 additions and 967 deletions

View File

@@ -37,7 +37,7 @@ import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
/**
* @author Christoph Strobl
*/
public class AotMetaModel implements Metamodel {
class AotMetaModel implements Metamodel {
private final String persistenceUnit;
private final Set<Class<?>> managedTypes;
@@ -105,7 +105,7 @@ public class AotMetaModel implements Metamodel {
@Override
public void addTransformer(ClassTransformer classTransformer) {
// just ingnore it
// just ignore it
}
};

View File

@@ -0,0 +1,58 @@
/*
* 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.generated;
import jakarta.validation.constraints.Null;
import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.QueryEnhancer;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.util.StringUtils;
/**
* Value object capturing queries used for repository query methods.
*
* @author Mark Paluch
* @since 4.0
*/
record AotQueries(AotQuery result, AotQuery count) {
/**
* Derive a count query from the given query.
*/
public static AotQueries from(StringAotQuery query, @Null String countProjection, QueryEnhancerSelector selector) {
QueryEnhancer queryEnhancer = selector.select(query.getQuery()).create(query.getQuery());
String derivedCountQuery = queryEnhancer
.createCountQueryFor(StringUtils.hasText(countProjection) ? countProjection : null);
DeclaredQuery countQuery = query.getQuery().rewrite(derivedCountQuery);
return new AotQueries(query, StringAotQuery.of(countQuery));
}
/**
* Create new {@code AotQueries} for the given queries.
*/
public static AotQueries from(AotQuery result, AotQuery count) {
return new AotQueries(result, count);
}
public boolean isNative() {
return result().isNative();
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.generated;
import java.util.List;
import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.query.ParameterBinding;
/**
* AOT query value object along with its parameter bindings.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 4.0
*/
abstract class AotQuery {
private final List<ParameterBinding> parameterBindings;
AotQuery(List<ParameterBinding> parameterBindings) {
this.parameterBindings = parameterBindings;
}
/**
* @return whether the query is a {@link jakarta.persistence.EntityManager#createNativeQuery native} one.
*/
public abstract boolean isNative();
public List<ParameterBinding> getParameterBindings() {
return parameterBindings;
}
/**
* @return the preliminary query limit.
*/
public Limit getLimit() {
return Limit.unlimited();
}
/**
* @return whether the query is limited (e.g. {@code findTop10By}).
*/
public boolean isLimited() {
return getLimit().isLimited();
}
}

View File

@@ -17,22 +17,11 @@ package org.springframework.data.jpa.repository.aot.generated;
import jakarta.persistence.metamodel.Metamodel;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.JpaParameters;
import org.springframework.data.jpa.repository.query.JpaQueryCreator;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
/**
* @author Christoph Strobl
* @since 2025/01
*/
public class AotQueryCreator {
class AotQueryCreator {
Metamodel metamodel;
@@ -40,23 +29,6 @@ public class AotQueryCreator {
this.metamodel = metamodel;
}
AotStringQuery createQuery(PartTree partTree, ReturnedType returnedType,
AotRepositoryMethodGenerationContext context) {
ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod());
JpaParameters parameters = new JpaParameters(parametersSource);
ParameterMetadataProvider metadataProvider = new ParameterMetadataProvider(parameters, EscapeCharacter.DEFAULT,
JpqlQueryTemplates.UPPER);
JpaQueryCreator queryCreator = new JpaQueryCreator(partTree, returnedType, metadataProvider,
JpqlQueryTemplates.UPPER, metamodel);
AotStringQuery query = AotStringQuery.bindable(queryCreator.createQuery(), metadataProvider.getBindings());
if (partTree.isLimiting()) {
query.setLimit(partTree.getResultLimit());
}
query.setCountQuery(context.annotationValue(Query.class, "countQuery"));
return query;
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.generated;
import java.lang.reflect.Method;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Sort;
import org.springframework.data.expression.ValueEvaluationContextProvider;
import org.springframework.data.expression.ValueExpression;
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.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.util.ConcurrentLruCache;
/**
* @author Mark Paluch
*/
public class AotRepositoryFragmentSupport {
private final RepositoryMetadata repositoryMetadata;
private final ValueExpressionDelegate valueExpressions;
private final ProjectionFactory projectionFactory;
private final ConcurrentLruCache<DeclaredQuery, QueryEnhancer> enhancers;
private final ConcurrentLruCache<String, ValueExpression> expressions;
private final ConcurrentLruCache<Method, ValueEvaluationContextProvider> contextProviders;
protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector,
RepositoryFactoryBeanSupport.FragmentCreationContext context) {
this(selector, context.getRepositoryMetadata(), context.getValueExpressionDelegate(),
context.getProjectionFactory());
}
protected AotRepositoryFragmentSupport(QueryEnhancerSelector selector, RepositoryMetadata repositoryMetadata,
ValueExpressionDelegate valueExpressions, ProjectionFactory projectionFactory) {
this.repositoryMetadata = repositoryMetadata;
this.valueExpressions = valueExpressions;
this.projectionFactory = projectionFactory;
this.enhancers = new ConcurrentLruCache<>(32, query -> selector.select(query).create(query));
this.expressions = new ConcurrentLruCache<>(32, valueExpressions::parse);
this.contextProviders = new ConcurrentLruCache<>(32, it -> valueExpressions
.createValueContextProvider(new JpaParameters(ParametersSource.of(repositoryMetadata, it))));
}
/**
* Rewrite a {@link DeclaredQuery} to apply {@link Sort} and {@link Class} projection.
*
* @param query
* @param sort
* @param returnedType
* @return
*/
protected String rewriteQuery(DeclaredQuery query, Sort sort, Class<?> returnedType) {
QueryEnhancer queryStringEnhancer = this.enhancers.get(query);
return queryStringEnhancer.rewrite(new DefaultQueryRewriteInformation(sort,
ReturnedType.of(returnedType, repositoryMetadata.getDomainType(), projectionFactory)));
}
/**
* Evaluate a Value Expression.
*
* @param method
* @param expressionString
* @param args
* @return
*/
protected @Nullable Object evaluateExpression(Method method, String expressionString, Object... args) {
ValueExpression expression = this.expressions.get(expressionString);
ValueEvaluationContextProvider contextProvider = this.contextProviders.get(method);
return expression.evaluate(contextProvider.getEvaluationContext(args, expression.getExpressionDependencies()));
}
private record DefaultQueryRewriteInformation(Sort sort,
ReturnedType returnedType) implements QueryEnhancer.QueryRewriteInformation {
@Override
public Sort getSort() {
return sort();
}
@Override
public ReturnedType getReturnedType() {
return returnedType();
}
}
}

View File

@@ -1,106 +0,0 @@
/*
* 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.generated;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Limit;
import org.springframework.data.jpa.repository.query.ParameterBinding;
import org.springframework.data.jpa.repository.query.ParameterBindingParser;
import org.springframework.data.jpa.repository.query.ParameterBindingParser.Metadata;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 2025/01
*/
class AotStringQuery {
private final String raw;
private final String sanitized;
private @Nullable String countQuery;
private final List<ParameterBinding> parameterBindings;
private final Metadata parameterMetadata;
private Limit limit;
private boolean nativeQuery;
public AotStringQuery(String raw, String sanitized, List<ParameterBinding> parameterBindings,
Metadata parameterMetadata) {
this.raw = raw;
this.sanitized = sanitized;
this.parameterBindings = parameterBindings;
this.parameterMetadata = parameterMetadata;
}
static AotStringQuery of(String raw) {
List<ParameterBinding> bindings = new ArrayList<>();
Metadata metadata = new Metadata();
String targetQuery = ParameterBindingParser.INSTANCE
.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(raw, bindings, metadata);
return new AotStringQuery(raw, targetQuery, bindings, metadata);
}
static AotStringQuery nativeQuery(String raw) {
AotStringQuery q = of(raw);
q.nativeQuery = true;
return q;
}
static AotStringQuery bindable(String query, List<ParameterBinding> bindings) {
return new AotStringQuery(query, query, bindings, new Metadata());
}
public String getQueryString() {
return sanitized;
}
public String getCountQuery(@Nullable String projection) {
if (StringUtils.hasText(countQuery)) {
return countQuery;
}
return QueryUtils.createCountQueryFor(sanitized, StringUtils.hasText(projection) ? projection : null, nativeQuery);
}
public List<ParameterBinding> parameterBindings() {
return this.parameterBindings;
}
boolean isLimited() {
return limit != null && limit.isLimited();
}
Limit getLimit() {
return limit;
}
public void setLimit(Limit limit) {
this.limit = limit;
}
public boolean isNativeQuery() {
return nativeQuery;
}
public void setCountQuery(@Nullable String countQuery) {
this.countQuery = StringUtils.hasText(countQuery) ? countQuery : null;
}
}

View File

@@ -24,15 +24,9 @@ import java.util.function.LongSupplier;
import java.util.regex.Pattern;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.query.DeclaredQuery;
import org.springframework.data.jpa.repository.query.ParameterBinding;
import org.springframework.data.jpa.repository.query.QueryEnhancer;
import org.springframework.data.jpa.repository.query.QueryEnhancer.QueryRewriteInformation;
import org.springframework.data.jpa.repository.query.QueryEnhancerFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
@@ -60,7 +54,7 @@ public class JpaCodeBlocks {
static class QueryExecutionBlockBuilder {
AotRepositoryMethodGenerationContext context;
private String queryVariableName;
private String queryVariableName = "query";
public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) {
this.context = context;
@@ -130,11 +124,14 @@ public class JpaCodeBlocks {
}
}
/**
* Builder for the actual query code block.
*/
static class QueryBlockBuilder {
private final AotRepositoryMethodGenerationContext context;
private String queryVariableName;
private AotStringQuery query;
private String queryVariableName = "query";
private AotQueries queries;
public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) {
this.context = context;
@@ -146,106 +143,59 @@ public class JpaCodeBlocks {
return this;
}
QueryBlockBuilder filter(String queryString) {
return filter(AotStringQuery.of(queryString));
}
QueryBlockBuilder filter(AotStringQuery query) {
this.query = query;
QueryBlockBuilder filter(AotQueries query) {
this.queries = query;
return this;
}
CodeBlock build() {
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
context.getActualReturnType());
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
context.getActualReturnType());
Object actualReturnType = isProjecting ? context.getActualReturnType()
: context.getRepositoryInformation().getDomainType();
: context.getRepositoryInformation().getDomainType();
CodeBlock.Builder builder = CodeBlock.builder();
builder.add("\n");
String queryStringNameVariableName = "%sString".formatted(queryVariableName);
StringAotQuery query = (StringAotQuery) queries.result();
builder.addStatement("$T $L = $S", String.class, queryStringNameVariableName, query.getQueryString());
String countQueryStringNameVariableName = null;
String countQuyerVariableName = null;
if (context.returnsPage()) {
countQueryStringNameVariableName = "count%sString".formatted(StringUtils.capitalize(queryVariableName));
countQuyerVariableName = "count%s".formatted(StringUtils.capitalize(queryVariableName));
String projection = context.annotationValue(org.springframework.data.jpa.repository.Query.class,
"countProjection");
StringAotQuery countQuery = (StringAotQuery) queries.count();
builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName,
query.getCountQuery(projection));
countQuery.getQueryString());
}
// sorting
// TODO: refactor into sort builder
{
String sortParameterName = context.getSortParameterName();
if (sortParameterName == null && context.getPageableParameterName() != null) {
sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName());
}
if (StringUtils.hasText(sortParameterName)) {
builder.beginControlFlow("if($L.isSorted())", sortParameterName);
if(query.isNativeQuery()) {
builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class,
queryStringNameVariableName);
} else {
builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class,
queryStringNameVariableName);
}
String enhancerVarName = "%sEnhancer".formatted(queryStringNameVariableName);
builder.addStatement("$T $L = $T.forQuery(declaredQuery).create(declaredQuery)", QueryEnhancer.class, enhancerVarName, QueryEnhancerFactory.class);
builder.addStatement("$L = $L.rewrite(new $T() { public $T getSort() { return $L; } public $T getReturnedType() { return $T.of($T.class, $T.class, new $T());} })", queryStringNameVariableName, enhancerVarName, QueryRewriteInformation.class,
Sort.class, sortParameterName, ReturnedType.class, ReturnedType.class,
context.getRepositoryInformation().getDomainType(), actualReturnType, SpelAwareProxyProjectionFactory.class);
builder.endControlFlow();
}
String sortParameterName = context.getSortParameterName();
if (sortParameterName == null && context.getPageableParameterName() != null) {
sortParameterName = "%s.getSort()".formatted(context.getPageableParameterName());
}
addQueryBlock(builder, queryVariableName, queryStringNameVariableName, query.isNativeQuery());
if (context.isExistsMethod()) {
builder.addStatement("$L.setMaxResults(1)", queryVariableName);
} else {
{
String limitParameterName = context.getLimitParameterName();
if (StringUtils.hasText(limitParameterName)) {
builder.beginControlFlow("if($L.isLimited())", limitParameterName);
builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limitParameterName);
builder.endControlFlow();
} else if (query.isLimited()) {
builder.addStatement("$L.setMaxResults($L)", queryVariableName, query.getLimit().max());
}
}
{
String pageableParamterName = context.getPageableParameterName();
if (StringUtils.hasText(pageableParamterName)) {
builder.beginControlFlow("if($L.isPaged())", pageableParamterName);
builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName,
pageableParamterName);
if (context.returnsSlice() && !context.returnsPage()) {
builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageableParamterName);
} else {
builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageableParamterName);
}
builder.endControlFlow();
}
}
if (StringUtils.hasText(sortParameterName)) {
applySorting(builder, sortParameterName, queryStringNameVariableName, actualReturnType);
}
addQueryBlock(builder, queryVariableName, queryStringNameVariableName, queries.result());
applyLimits(builder);
if (StringUtils.hasText(countQueryStringNameVariableName)) {
builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll");
addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, query.isNativeQuery());
addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, queries.count());
builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName);
// end control flow does not work well with lambdas
@@ -256,16 +206,67 @@ public class JpaCodeBlocks {
return builder.build();
}
private void applySorting(Builder builder, String sort, String queryString, Object actualReturnType) {
builder.beginControlFlow("if ($L.isSorted())", sort);
if (queries.isNative()) {
builder.addStatement("$T declaredQuery = $T.nativeQuery($L)", DeclaredQuery.class, DeclaredQuery.class,
queryString);
} else {
builder.addStatement("$T declaredQuery = $T.jpqlQuery($L)", DeclaredQuery.class, DeclaredQuery.class,
queryString);
}
builder.addStatement("$L = rewriteQuery(declaredQuery, $L, $T.class)", queryString, sort, actualReturnType);
builder.endControlFlow();
}
private void applyLimits(Builder builder) {
if (context.isExistsMethod()) {
builder.addStatement("$L.setMaxResults(1)", queryVariableName);
return;
}
String limit = context.getLimitParameterName();
if (StringUtils.hasText(limit)) {
builder.beginControlFlow("if ($L.isLimited())", limit);
builder.addStatement("$L.setMaxResults($L.max())", queryVariableName, limit);
builder.endControlFlow();
} else if (queries.result().isLimited()) {
builder.addStatement("$L.setMaxResults($L)", queryVariableName, queries.result().getLimit().max());
}
String pageable = context.getPageableParameterName();
if (StringUtils.hasText(pageable)) {
builder.beginControlFlow("if ($L.isPaged())", pageable);
builder.addStatement("$L.setFirstResult(Long.valueOf($L.getOffset()).intValue())", queryVariableName, pageable);
if (context.returnsSlice() && !context.returnsPage()) {
builder.addStatement("$L.setMaxResults($L.getPageSize() + 1)", queryVariableName, pageable);
} else {
builder.addStatement("$L.setMaxResults($L.getPageSize())", queryVariableName, pageable);
}
builder.endControlFlow();
}
}
private void addQueryBlock(Builder builder, String queryVariableName, String queryStringNameVariableName,
boolean nativeQuery) {
AotQuery query) {
builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName,
context.fieldNameOf(EntityManager.class), nativeQuery ? "createNativeQuery" : "createQuery",
context.fieldNameOf(EntityManager.class), query.isNative() ? "createNativeQuery" : "createQuery",
queryStringNameVariableName);
for (ParameterBinding binding : query.parameterBindings()) {
for (ParameterBinding binding : query.getParameterBindings()) {
Object prepare = binding.prepare("s");
if (prepare instanceof String prepared && !prepared.equals("s")) {
String format = prepared.replaceAll("%", "%%").replace("s", "%s");
if (binding.getIdentifier().hasPosition()) {

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot.generated;
import jakarta.persistence.EntityManager;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
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.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.ParameterMetadataProvider;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.jpa.repository.query.QueryEnhancerSelector;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryImplementationMetadata;
import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.ParametersSource;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.javapoet.TypeName;
import org.springframework.javapoet.TypeSpec;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
public class JpaRepositoryContributor extends RepositoryContributor {
private final CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory();
private final AotQueryCreator queryCreator;
private final AotMetaModel metaModel;
public JpaRepositoryContributor(AotRepositoryContext repositoryContext) {
super(repositoryContext);
this.metaModel = new AotMetaModel(repositoryContext.getResolvedTypes());
this.queryCreator = new AotQueryCreator(metaModel);
}
@Override
protected void customizeFile(RepositoryInformation information, AotRepositoryImplementationMetadata metadata,
TypeSpec.Builder builder) {
builder.superclass(TypeName.get(AotRepositoryFragmentSupport.class));
}
@Override
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
constructorBuilder.addParameter("entityManager", EntityManager.class);
constructorBuilder.addParameter("context", RepositoryFactoryBeanSupport.FragmentCreationContext.class);
// TODO: Pick up the configured QueryEnhancerSelector
constructorBuilder.customize((repositoryInformation, builder) -> {
builder.addStatement("super($T.DEFAULT_SELECTOR, context)", QueryEnhancerSelector.class);
});
}
@Override
protected AotRepositoryMethodBuilder contributeRepositoryMethod(
AotRepositoryMethodGenerationContext generationContext) {
QueryEnhancerSelector selector = QueryEnhancerSelector.DEFAULT_SELECTOR;
// no stored procedures for now.
if (AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Procedure.class) != null) {
return null;
}
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class);
if (queryAnnotation != null) {
if (StringUtils.hasText(queryAnnotation.value())
&& Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) {
return null;
}
}
// TODO: Named query via EntityManager, NamedQuery via properties, also for count queries.
return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> {
MergedAnnotations annotations = MergedAnnotations.from(context.getMethod());
MergedAnnotation<Query> query = annotations.get(Query.class);
MergedAnnotation<NativeQuery> nativeQuery = annotations.get(NativeQuery.class);
MergedAnnotation<QueryHints> queryHints = annotations.get(QueryHints.class);
body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
AotQueries aotQueries;
if (query.isPresent() && StringUtils.hasText(query.getString("value"))) {
aotQueries = buildStringQuery(selector, query);
} else {
aotQueries = buildPartTreeQuery(context, query);
}
body.addCode(JpaCodeBlocks.queryBlockBuilder(context).filter(aotQueries).build());
body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).build());
});
}
private AotQueries buildStringQuery(QueryEnhancerSelector selector, MergedAnnotation<Query> query) {
Function<String, StringAotQuery> queryFunction = query.getBoolean("nativeQuery") ? StringAotQuery::nativeQuery
: StringAotQuery::jpqlQuery;
StringAotQuery aotStringQuery = queryFunction.apply(query.getString("value"));
String countQuery = query.getString("countQuery");
if (StringUtils.hasText(countQuery)) {
return AotQueries.from(aotStringQuery, queryFunction.apply(countQuery));
}
String countProjection = query.getString("countProjection");
return AotQueries.from(aotStringQuery, countProjection, selector);
}
private AotQueries buildPartTreeQuery(AotRepositoryMethodGenerationContext context, MergedAnnotation<Query> query) {
PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType());
// TODO make configurable
JpqlQueryTemplates templates = JpqlQueryTemplates.UPPER;
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
context.getActualReturnType());
Class<?> actualReturnType;
try {
actualReturnType = isProjecting
? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader())
: context.getRepositoryInformation().getDomainType();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
ReturnedType returnedType = ReturnedType.of(actualReturnType, context.getRepositoryInformation().getDomainType(),
projectionFactory);
ParametersSource parametersSource = ParametersSource.of(context.getRepositoryInformation(), context.getMethod());
JpaParameters parameters = new JpaParameters(parametersSource);
AotQuery partTreeQuery = createQuery(partTree, returnedType, parameters, templates);
if (query.isPresent() && StringUtils.hasText(query.getString("countQuery"))) {
return AotQueries.from(partTreeQuery, StringAotQuery.jpqlQuery(query.getString("countQuery")));
}
AotQuery partTreeCountQuery = createCountQuery(partTree, returnedType, parameters, templates);
return AotQueries.from(partTreeQuery, 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());
}
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);
}
}

View File

@@ -1,115 +0,0 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot.generated;
import jakarta.persistence.EntityManager;
import java.util.regex.Pattern;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder;
import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.javapoet.TypeName;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
*/
public class JpaRepsoitoryContributor extends RepositoryContributor {
AotQueryCreator queryCreator;
AotMetaModel metaModel;
public JpaRepsoitoryContributor(AotRepositoryContext repositoryContext) {
super(repositoryContext);
metaModel = new AotMetaModel(repositoryContext.getResolvedTypes());
this.queryCreator = new AotQueryCreator(metaModel);
}
@Override
protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) {
constructorBuilder.addParameter("entityManager", TypeName.get(EntityManager.class));
}
@Override
protected AotRepositoryMethodBuilder contributeRepositoryMethod(
AotRepositoryMethodGenerationContext generationContext) {
{
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class);
if (queryAnnotation != null) {
if (StringUtils.hasText(queryAnnotation.value())
&& Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) {
return null;
}
}
}
return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> {
Query query = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class);
if (query != null && StringUtils.hasText(query.value())) {
AotStringQuery aotStringQuery = query.nativeQuery() ? AotStringQuery.nativeQuery(query.value())
: AotStringQuery.of(query.value());
aotStringQuery.setCountQuery(query.countQuery());
body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
body.addCode(
JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(aotStringQuery).build());
} else {
PartTree partTree = new PartTree(context.getMethod().getName(),
context.getRepositoryInformation().getDomainType());
CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory();
boolean isProjecting = context.getActualReturnType() != null
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()),
context.getActualReturnType());
Class<?> actualReturnType = context.getRepositoryInformation().getDomainType();
try {
actualReturnType = isProjecting
? ClassUtils.forName(context.getActualReturnType().toString(), context.getClass().getClassLoader())
: context.getRepositoryInformation().getDomainType();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
ReturnedType returnedType = ReturnedType.of(actualReturnType,
context.getRepositoryInformation().getDomainType(), projectionFactory);
AotStringQuery stringQuery = queryCreator.createQuery(partTree, returnedType, context);
body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName())));
body.addCode(
JpaCodeBlocks.queryBlockBuilder(context).usingQueryVariableName("query").filter(stringQuery).build());
}
body.addCode(JpaCodeBlocks.queryExecutionBlockBuilder(context).referencing("query").build());
});
}
}

View File

@@ -0,0 +1,131 @@
/*
* 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.generated;
import java.util.List;
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;
/**
* An AOT query represented by a string.
*
* @author Mark Paluch
* @since 4.0
*/
abstract class StringAotQuery extends AotQuery {
private StringAotQuery(List<ParameterBinding> parameterBindings) {
super(parameterBindings);
}
static StringAotQuery of(DeclaredQuery query) {
if (query instanceof PreprocessedQuery pq) {
return new DeclaredAotQuery(pq);
}
return new DeclaredAotQuery(PreprocessedQuery.parse(query));
}
static StringAotQuery jpqlQuery(String queryString) {
return of(DeclaredQuery.jpqlQuery(queryString));
}
public static StringAotQuery jpqlQuery(String queryString, List<ParameterBinding> bindings, Limit resultLimit) {
return new LimitedAotQuery(queryString, bindings, resultLimit);
}
static StringAotQuery nativeQuery(String queryString) {
return of(DeclaredQuery.nativeQuery(queryString));
}
public abstract DeclaredQuery getQuery();
public abstract String getQueryString();
@Override
public String toString() {
return getQueryString();
}
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
static class DeclaredAotQuery extends StringAotQuery {
private final PreprocessedQuery query;
DeclaredAotQuery(PreprocessedQuery query) {
super(query.getBindings());
this.query = query;
}
@Override
public String getQueryString() {
return query.getQueryString();
}
@Override
public boolean isNative() {
return query.isNative();
}
public PreprocessedQuery getQuery() {
return query;
}
}
/**
* @author Mark Paluch
*/
static class LimitedAotQuery extends StringAotQuery {
private final String queryString;
private final Limit limit;
LimitedAotQuery(String queryString, List<ParameterBinding> parameterBindings, Limit limit) {
super(parameterBindings);
this.queryString = queryString;
this.limit = limit;
}
@Override
public DeclaredQuery getQuery() {
return DeclaredQuery.jpqlQuery(queryString);
}
@Override
public String getQueryString() {
return queryString;
}
@Override
public boolean isNative() {
return false;
}
@Override
public Limit getLimit() {
return limit;
}
}
}

View File

@@ -15,9 +15,7 @@
*/
package org.springframework.data.jpa.repository.config;
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.EM_BEAN_DEFINITION_REGISTRAR_POST_PROCESSOR_BEAN_NAME;
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_CONTEXT_BEAN_NAME;
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.JPA_MAPPING_CONTEXT_BEAN_NAME;
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*;
import jakarta.persistence.Entity;
import jakarta.persistence.MappedSuperclass;
@@ -54,7 +52,7 @@ import org.springframework.dao.DataAccessException;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.aot.AotContext;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.aot.generated.JpaRepsoitoryContributor;
import org.springframework.data.jpa.repository.aot.generated.JpaRepositoryContributor;
import org.springframework.data.jpa.repository.support.DefaultJpaContext;
import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor;
import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension;
@@ -335,7 +333,7 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
return null;
}
return new JpaRepsoitoryContributor(repositoryContext);
return new JpaRepositoryContributor(repositoryContext);
}
@Nullable

View File

@@ -16,6 +16,7 @@
package org.springframework.data.jpa.repository.query;
import jakarta.persistence.EntityManager;
import jakarta.persistence.metamodel.Metamodel;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
@@ -53,6 +54,24 @@ public class JpaCountQueryCreator extends JpaQueryCreator {
this.returnedType = returnedType;
}
/**
* Creates a new {@link JpaCountQueryCreator}
*
* @param tree
* @param returnedType
* @param provider
* @param templates
* @param metamodel
*/
public JpaCountQueryCreator(PartTree tree, ReturnedType returnedType, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, Metamodel metamodel) {
super(tree, returnedType, provider, templates, metamodel);
this.distinct = tree.isDistinct();
this.returnedType = returnedType;
}
@Override
protected JpqlQueryBuilder.Select buildQuery(Sort sort) {
JpqlQueryBuilder.SelectStep selectStep = JpqlQueryBuilder.selectFrom(returnedType.getDomainType());

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.*;
import java.util.ArrayList;
import java.util.Collection;
@@ -60,7 +60,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @since 4.0
*/
final class PreprocessedQuery implements DeclaredQuery {
public final class PreprocessedQuery implements DeclaredQuery {
private final DeclaredQuery source;
private final List<ParameterBinding> bindings;
@@ -127,7 +127,7 @@ final class PreprocessedQuery implements DeclaredQuery {
return usesJdbcStyleParameters;
}
List<ParameterBinding> getBindings() {
public List<ParameterBinding> getBindings() {
return Collections.unmodifiableList(bindings);
}

View File

@@ -0,0 +1,140 @@
/*
* 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.generated;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
import org.springframework.data.repository.query.ValueExpressionDelegate;
import org.springframework.orm.jpa.SharedEntityManagerCreator;
import org.springframework.util.ReflectionUtils;
/**
* Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface.
* <p>
* This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT
* fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method
* invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy.
*
* @author Mark Paluch
*/
@ImportResource("classpath:/infrastructure.xml")
class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor {
private final Class<?> repositoryInterface;
private final TestJpaAotRepositoryContext<?> repositoryContext;
public AotFragmentTestConfigurationSupport(Class<?> repositoryInterface) {
this.repositoryInterface = repositoryInterface;
this.repositoryContext = new TestJpaAotRepositoryContext<>(UserRepository.class, null);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class);
new JpaRepositoryContributor(repositoryContext).contribute(generationContext);
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
.genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot")
.addConstructorArgReference("jpaSharedEM_entityManagerFactory")
.addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition();
TestCompiler.forSystem().with(generationContext).compile(compiled -> {
beanFactory.setBeanClassLoader(compiled.getClassLoader());
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository);
});
BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> {
Object fragment = beanFactory.getBean("fragment");
Object proxy = getFragmentFacadeProxy(fragment);
return repositoryInterface.cast(proxy);
}).getBeanDefinition();
((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
}
private Object getFragmentFacadeProxy(Object fragment) {
return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class<?>[] { repositoryInterface },
(p, method, args) -> {
Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes());
if (target == null) {
throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target));
}
try {
return target.invoke(fragment, args);
} catch (ReflectiveOperationException e) {
ReflectionUtils.handleReflectionException(e);
}
return null;
});
}
@Bean("jpaSharedEM_entityManagerFactory")
EntityManager sharedEntityManagerCreator(EntityManagerFactory emf) {
return SharedEntityManagerCreator.createSharedEntityManager(emf);
}
private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext(
TestJpaAotRepositoryContext<?> repositoryContext) {
RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() {
@Override
public RepositoryMetadata getRepositoryMetadata() {
return repositoryContext.getRepositoryInformation();
}
@Override
public ValueExpressionDelegate getValueExpressionDelegate() {
return ValueExpressionDelegate.create();
}
@Override
public ProjectionFactory getProjectionFactory() {
return new SpelAwareProxyProjectionFactory();
}
};
return creationContext;
}
}

View File

@@ -0,0 +1,356 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot.generated;
import static org.assertj.core.api.Assertions.*;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
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.User;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;
/**
* Integration tests for the {@link UserRepository} AOT fragment.
*
* @author Christoph Strobl
* @author Mark Paluch
*/
@SpringJUnitConfig(classes = JpaRepositoryContributorIntegrationTests.JpaRepositoryContributorConfiguration.class)
@Transactional
class JpaRepositoryContributorIntegrationTests {
@Autowired UserRepository fragment;
@Autowired EntityManager em;
@Configuration
static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
public JpaRepositoryContributorConfiguration() {
super(UserRepository.class);
}
}
@BeforeEach
void beforeEach() {
em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate();
User luke = new User("Luke", "Skywalker", "luke@jedi.org");
em.persist(luke);
User leia = new User("Leia", "Organa", "leia@resistance.gov");
em.persist(leia);
User han = new User("Han", "Solo", "han@smuggler.net");
em.persist(han);
User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net");
em.persist(chewbacca);
User yoda = new User("Yoda", "n/a", "yoda@jedi.org");
em.persist(yoda);
User vader = new User("Anakin", "Skywalker", "vader@empire.com");
em.persist(vader);
User kylo = new User("Ben", "Solo", "kylo@new-empire.com");
em.persist(kylo);
}
@Test
void testFindDerivedFinderSingleEntity() {
User user = fragment.findByEmailAddress("luke@jedi.org");
assertThat(user.getLastname()).isEqualTo("Skywalker");
}
@Test
void testFindDerivedFinderOptionalEntity() {
Optional<User> user = fragment.findOptionalOneByEmailAddress("yoda@jedi.org");
assertThat(user).isNotNull().containsInstanceOf(User.class)
.hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda"));
}
@Test
void testDerivedCount() {
Long value = fragment.countUsersByLastname("Skywalker");
assertThat(value).isEqualTo(2L);
}
@Test
void testDerivedExists() {
Boolean exists = fragment.existsUserByLastname("Skywalker");
assertThat(exists).isTrue();
}
@Test
void testDerivedFinderWithoutArguments() {
List<User> users = fragment.findUserNoArgumentsBy();
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
}
@Test
void testDerivedFinderReturningList() {
List<User> users = fragment.findByLastnameStartingWith("S");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com",
"kylo@new-empire.com", "han@smuggler.net");
}
@Test
void testLimitedDerivedFinder() {
List<User> users = fragment.findTop2ByLastnameStartingWith("S");
assertThat(users).hasSize(2);
}
@Test
void testSortedDerivedFinder() {
List<User> users = fragment.findByLastnameStartingWithOrderByEmailAddress("S");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
"luke@jedi.org", "vader@empire.com");
}
@Test
void testDerivedFinderWithLimitArgument() {
List<User> users = fragment.findByLastnameStartingWith("S", Limit.of(2));
assertThat(users).hasSize(2);
}
@Test
void testDerivedFinderWithSort() {
List<User> users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress"));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
"luke@jedi.org", "vader@empire.com");
}
@Test
void testDerivedFinderWithSortAndLimit() {
List<User> users = fragment.findByLastnameStartingWith("S", Sort.by("emailAddress"), Limit.of(2));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
@Test
void testDerivedFinderReturningListWithPageable() {
List<User> users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
@Test
void testDerivedFinderReturningPage() {
Page<User> page = fragment.findPageOfUsersByLastnameStartingWith("S",
PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
}
@Test
void testDerivedFinderReturningSlice() {
Slice<User> slice = fragment.findSliceOfUserByLastnameStartingWith("S",
PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
}
@Test
void testAnnotatedFinderReturningSingleValueWithQuery() {
User user = fragment.findAnnotatedQueryByEmailAddress("yoda@jedi.org");
assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda");
}
@Test
void testAnnotatedFinderReturningListWithQuery() {
List<User> users = fragment.findAnnotatedQueryByLastname("S");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
@Test
void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() {
List<User> users = fragment.findAnnotatedQueryByLastnameParameter("S");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
@Test
void testAnnotatedMultilineFinderWithQuery() {
List<User> users = fragment.findAnnotatedMultilineQueryByLastname("S");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
@Test
void testAnnotatedFinderWithQueryAndLimit() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2));
assertThat(users).hasSize(2);
}
@Test
void testAnnotatedFinderWithQueryAndSort() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", Sort.by("emailAddress"));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
"luke@jedi.org", "vader@empire.com");
}
@Test
void testAnnotatedFinderWithQueryLimitAndSort() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("emailAddress"));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
@Test
void testAnnotatedFinderReturningListWithPageable() {
List<User> users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
}
@Test
void testAnnotatedFinderReturningPage() {
Page<User> page = fragment.findAnnotatedQueryPageOfUsersByLastname("S",
PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
}
@Test
void testPagingAnnotatedQueryWithSort() {
Page<User> page = fragment.findAnnotatedQueryPageWithStaticSort("S", PageRequest.of(0, 2, Sort.unsorted()));
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("luke@jedi.org",
"vader@empire.com");
}
@Test
void testAnnotatedFinderReturningSlice() {
Slice<User> slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S",
PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
}
@Test
void testDerivedFinderReturningListOfProjections() {
List<UserDtoProjection> users = fragment.findUserProjectionByLastnameStartingWith("S");
assertThat(users).extracting(UserDtoProjection::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
@Test
void testDerivedFinderReturningPageOfProjections() {
// TODO: query.setParameter(1, "%s%%".formatted(lastname));
Page<UserDtoProjection> page = fragment.findUserProjectionByLastnameStartingWith("S",
PageRequest.of(0, 2, Sort.by("emailAddress")));
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
}
// modifying
@Test
void testDerivedDeleteSingle() {
User result = fragment.deleteByEmailAddress("yoda@jedi.org");
assertThat(result).isNotNull().extracting(User::getEmailAddress).isEqualTo("yoda@jedi.org");
Object yodaShouldBeGone = em
.createQuery("SELECT u FROM %s u WHERE u.emailAddress = 'yoda@jedi.org'".formatted(User.class.getName()))
.getSingleResultOrNull();
assertThat(yodaShouldBeGone).isNull();
}
// native queries
@Test
void nativeQuery() {
Page<String> page = fragment.findByNativeQueryWithPageable(PageRequest.of(0, 2));
assertThat(page.getTotalElements()).isEqualTo(7);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).containsExactly("Anakin", "Ben");
}
// old stuff below
// TODO:
void todo() {
// interface projections
// named queries
// query hints
// entity graphs
// native queries
// delete
// @Modifying
// flush / clear
}
}

View File

@@ -1,614 +0,0 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot.generated;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.persistence.EntityManager;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Page;
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.User;
import org.springframework.data.util.Lazy;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import com.example.UserDtoProjection;
import com.example.UserRepository;
/**
* @author Christoph Strobl
*/
class JpaRepositoryContributorUnitTests {
private static Verifyer generated;
@BeforeAll
static void beforeAll() {
TestJpaAotRepsitoryContext aotContext = new TestJpaAotRepsitoryContext(UserRepository.class, null);
TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class);
new JpaRepsoitoryContributor(aotContext).contribute(generationContext);
AbstractBeanDefinition emBeanDefinition = BeanDefinitionBuilder
.rootBeanDefinition("org.springframework.orm.jpa.SharedEntityManagerCreator")
.setFactoryMethod("createSharedEntityManager").addConstructorArgReference("entityManagerFactory")
.setLazyInit(true).getBeanDefinition();
AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
.genericBeanDefinition("com.example.UserRepositoryImpl__Aot")
.addConstructorArgReference("jpaSharedEM_entityManagerFactory").getBeanDefinition();
/*
alter the RepositoryFactory so we can write generated calsses into a supplier and then write some custom code for instantiation
on JpaRepositoryFactoryBean
beanDefinition.getPropertyValues().addPropertyValue("aotImplementation", new Function<BeanFactory, Instance>() {
public Instance apply(BeanFactory beanFactor) {
EntityManager em = beanFactory.getBean(EntityManger.class);
return new com.example.UserRepositoryImpl__Aot(em);
}
});
*/
// register a dedicated factory that can read stuff
// don't write to spring.factories or uas another name for it
// maybe write the code directly to a repo fragment
// repo does not have to be a bean, but can be a method called by some component
// pass list to entiy manager to have stuff in memory have to list written out directly when creating the bean
generated = generateContext(generationContext) //
.registerBeansFrom(new ClassPathResource("infrastructure.xml"))
.register("jpaSharedEM_entityManagerFactory", emBeanDefinition)
.register("aotUserRepository", aotGeneratedRepository);
}
@BeforeEach
public void beforeEach() {
generated.doWithBean(EntityManager.class, em -> {
em.createQuery("DELETE FROM %s".formatted(User.class.getName())).executeUpdate();
User luke = new User("Luke", "Skywalker", "luke@jedi.org");
em.persist(luke);
User leia = new User("Leia", "Organa", "leia@resistance.gov");
em.persist(leia);
User han = new User("Han", "Solo", "han@smuggler.net");
em.persist(han);
User chewbacca = new User("Chewbacca", "n/a", "chewie@smuggler.net");
em.persist(chewbacca);
User yoda = new User("Yoda", "n/a", "yoda@jedi.org");
em.persist(yoda);
User vader = new User("Anakin", "Skywalker", "vader@empire.com");
em.persist(vader);
User kylo = new User("Ben", "Solo", "kylo@new-empire.com");
em.persist(kylo);
});
}
@Test
void testFindDerivedFinderSingleEntity() {
generated.verify(methodInvoker -> {
User user = methodInvoker.invoke("findByEmailAddress", "luke@jedi.org").onBean("aotUserRepository");
assertThat(user.getLastname()).isEqualTo("Skywalker");
});
}
@Test
void testFindDerivedFinderOptionalEntity() {
generated.verify(methodInvoker -> {
Optional<User> user = methodInvoker.invoke("findOptionalOneByEmailAddress", "yoda@jedi.org")
.onBean("aotUserRepository");
assertThat(user).isNotNull().containsInstanceOf(User.class)
.hasValueSatisfying(it -> assertThat(it).extracting(User::getFirstname).isEqualTo("Yoda"));
});
}
@Test
void testDerivedCount() {
generated.verify(methodInvoker -> {
Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository");
assertThat(value).isEqualTo(2L);
});
}
@Test
void testDerivedExists() {
generated.verify(methodInvoker -> {
Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository");
assertThat(exists).isTrue();
});
}
@Test
void testDerivedFinderWithoutArguments() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository");
assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class);
});
}
@Test
void testDerivedFinderReturningList() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("luke@jedi.org", "vader@empire.com",
"kylo@new-empire.com", "han@smuggler.net");
});
}
@Test
void testLimitedDerivedFinder() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testSortedDerivedFinder() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWithOrderByEmailAddress", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
"luke@jedi.org", "vader@empire.com");
});
}
@Test
void testDerivedFinderWithLimitArgument() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testDerivedFinderWithSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress"))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
"luke@jedi.org", "vader@empire.com");
});
}
@Test
void testDerivedFinderWithSortAndLimit() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("emailAddress"), Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
});
}
@Test
void testDerivedFinderReturningListWithPageable() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker
.invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
});
}
@Test
void testDerivedFinderReturningPage() {
generated.verify(methodInvoker -> {
Page<User> page = methodInvoker
.invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
});
}
@Test
void testDerivedFinderReturningSlice() {
generated.verify(methodInvoker -> {
Slice<User> slice = methodInvoker
.invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
});
}
@Test
void testAnnotatedFinderReturningSingleValueWithQuery() {
generated.verify(methodInvoker -> {
User user = methodInvoker.invoke("findAnnotatedQueryByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository");
assertThat(user).isNotNull().extracting(User::getFirstname).isEqualTo("Yoda");
});
}
@Test
void testAnnotatedFinderReturningListWithQuery() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
});
}
@Test
void testAnnotatedFinderUsingNamedParameterPlaceholderReturningListWithQuery() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastnameParamter", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
});
}
@Test
void testAnnotatedMultilineFinderWithQuery() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
});
}
@Test
void testAnnotatedFinderWithQueryAndLimit() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2))
.onBean("aotUserRepository");
assertThat(users).hasSize(2);
});
}
@Test
void testAnnotatedFinderWithQueryAndSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("emailAddress"))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com",
"luke@jedi.org", "vader@empire.com");
});
}
@Test
void testAnnotatedFinderWithQueryLimitAndSort() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("emailAddress"))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
});
}
@Test
void testAnnotatedFinderReturningListWithPageable() {
generated.verify(methodInvoker -> {
List<User> users = methodInvoker
.invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(users).extracting(User::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com");
});
}
@Test
void testAnnotatedFinderReturningPage() {
generated.verify(methodInvoker -> {
Page<User> page = methodInvoker
.invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
});
}
@Test
void testAnnotatedFinderReturningSlice() {
generated.verify(methodInvoker -> {
Slice<User> slice = methodInvoker
.invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(slice.hasNext()).isTrue();
assertThat(slice.getSize()).isEqualTo(2);
assertThat(slice.getContent()).extracting(User::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
});
}
@Test
void testDerivedFinderReturningListOfProjections() {
generated.verify(methodInvoker -> {
List<UserDtoProjection> users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S")
.onBean("aotUserRepository");
assertThat(users).extracting(UserDtoProjection::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net",
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
});
}
@Test
void testDerivedFinderReturningPageOfProjections() {
generated.verify(methodInvoker -> {
Page<UserDtoProjection> page = methodInvoker
.invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("emailAddress")))
.onBean("aotUserRepository");
assertThat(page.getTotalElements()).isEqualTo(4);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net",
"kylo@new-empire.com");
});
}
// modifying
@Test
void testDerivedDeleteSingle() {
generated.verifyInTx(methodInvoker -> {
User result = methodInvoker.invoke("deleteByEmailAddress", "yoda@jedi.org").onBean("aotUserRepository");
assertThat(result).isNotNull().extracting(User::getEmailAddress).isEqualTo("yoda@jedi.org");
}).doWithBean(EntityManager.class, em -> {
Object yodaShouldBeGone = em
.createQuery("SELECT u FROM %s u WHERE u.emailAddress = 'yoda@jedi.org'".formatted(User.class.getName()))
.getSingleResultOrNull();
assertThat(yodaShouldBeGone).isNull();
});
}
// native queries
@Test
void nativeQuery() {
generated.verify(methodInvoker -> {
Page<String> page = methodInvoker
.invoke("findByNativeQueryWithPageable", PageRequest.of(0, 2))
.onBean("aotUserRepository");
assertThat(page.getTotalElements()).isEqualTo(7);
assertThat(page.getSize()).isEqualTo(2);
assertThat(page.getContent()).containsExactly("Anakin", "Ben");
});
}
// old stuff below
// TODO:
void todo() {
// Query q;
// q.setMaxResults()
// q.setFirstResult()
// 1 build some more stuff from below
// 2 set up boot sample project in data samples
// query hints
// first and max result for pagination
// entity graphs
// native queries
// delete
// @Modifying
// flush / clear
}
static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) {
return new GeneratedContextBuilder(generationContext);
}
static class GeneratedContextBuilder implements Verifyer {
TestGenerationContext generationContext;
Map<String, BeanDefinition> beanDefinitions = new LinkedHashMap<>();
Resource xmlBeanDefinitions;
Lazy<DefaultListableBeanFactory> lazyFactory;
public GeneratedContextBuilder(TestGenerationContext generationContext) {
this.generationContext = generationContext;
this.lazyFactory = Lazy.of(() -> {
DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory();
TestCompiler.forSystem().with(generationContext).compile(compiled -> {
freshBeanFactory.setBeanClassLoader(compiled.getClassLoader());
if (xmlBeanDefinitions != null) {
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(freshBeanFactory);
beanDefinitionReader.loadBeanDefinitions(xmlBeanDefinitions);
}
for (Entry<String, BeanDefinition> entry : beanDefinitions.entrySet()) {
freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue());
}
});
return freshBeanFactory;
});
}
GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) {
this.beanDefinitions.put(name, beanDefinition);
return this;
}
GeneratedContextBuilder registerBeansFrom(Resource xmlBeanDefinitions) {
this.xmlBeanDefinitions = xmlBeanDefinitions;
return this;
}
public Verifyer verify(Consumer<GeneratedContext> methodInvoker) {
methodInvoker.accept(new GeneratedContext(lazyFactory));
return this;
}
}
interface Verifyer {
Verifyer verify(Consumer<GeneratedContext> methodInvoker);
default Verifyer verifyInTx(Consumer<GeneratedContext> methodInvoker) {
verify(ctx -> {
PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class);
new TransactionTemplate(txMgr).execute(action -> {
verify(methodInvoker);
return "ok";
});
});
return this;
}
default <T> void doWithBean(Class<T> type, Consumer<T> runit) {
verify(ctx -> {
boolean isEntityManager = type == EntityManager.class;
T bean = ctx.delegate.get().getBean(type);
if (!isEntityManager) {
runit.accept(bean);
} else {
PlatformTransactionManager txMgr = ctx.delegate.get().getBean(PlatformTransactionManager.class);
new TransactionTemplate(txMgr).execute(action -> {
runit.accept(bean);
return "ok";
});
}
});
}
}
static class GeneratedContext {
private Supplier<DefaultListableBeanFactory> delegate;
public GeneratedContext(Supplier<DefaultListableBeanFactory> defaultListableBeanFactory) {
this.delegate = defaultListableBeanFactory;
}
InvocationBuilder invoke(String method, Object... arguments) {
return new InvocationBuilder() {
@Override
public <T> T onBean(String beanName) {
DefaultListableBeanFactory defaultListableBeanFactory = delegate.get();
Object bean = defaultListableBeanFactory.getBean(beanName);
return ReflectionTestUtils.invokeMethod(bean, method, arguments);
}
};
}
interface InvocationBuilder {
<T> T onBean(String beanName);
}
}
}

View File

@@ -18,6 +18,8 @@ package org.springframework.data.jpa.repository.aot.generated;
import java.lang.reflect.Method;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryInformation;
@@ -27,7 +29,6 @@ import org.springframework.data.repository.core.support.RepositoryComposition;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl

View File

@@ -37,14 +37,20 @@ import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
class TestJpaAotRepsitoryContext implements AotRepositoryContext {
class TestJpaAotRepositoryContext<T> implements AotRepositoryContext {
private final StubRepositoryInformation repositoryInformation;
private final Class<T> repositoryInterface;
public TestJpaAotRepsitoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) {
public TestJpaAotRepositoryContext(Class<T> repositoryInterface, @Nullable RepositoryComposition composition) {
this.repositoryInterface = repositoryInterface;
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition);
}
public Class<T> getRepositoryInterface() {
return repositoryInterface;
}
@Override
public ConfigurableListableBeanFactory getBeanFactory() {
return null;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example;
package org.springframework.data.jpa.repository.aot.generated;
/**
* @author Christoph Strobl

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2024 the original author or authors.
* 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.
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example;
package org.springframework.data.jpa.repository.aot.generated;
import java.util.List;
import java.util.Optional;
@@ -27,7 +27,6 @@ import org.springframework.data.jpa.domain.sample.User;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
/**
* @author Christoph Strobl
@@ -42,7 +41,7 @@ public interface UserRepository extends CrudRepository<User, Integer> {
Long countUsersByLastname(String lastname);
Boolean existsUserByLastname(String lastname);
boolean existsUserByLastname(String lastname);
List<User> findByLastnameStartingWith(String lastname);
@@ -71,7 +70,7 @@ public interface UserRepository extends CrudRepository<User, Integer> {
List<User> findAnnotatedQueryByLastname(String lastname);
@Query("select u from User u where u.lastname like :lastname%")
List<User> findAnnotatedQueryByLastnameParamter(String lastname);
List<User> findAnnotatedQueryByLastnameParameter(String lastname);
@Query("""
select u
@@ -94,6 +93,9 @@ public interface UserRepository extends CrudRepository<User, Integer> {
@Query("select u from User u where u.lastname like ?1%")
Page<User> findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable);
@Query("select u from User u where u.lastname like ?1% ORDER BY u.lastname")
Page<User> findAnnotatedQueryPageWithStaticSort(String lastname, Pageable pageable);
@Query("select u from User u where u.lastname like ?1%")
Slice<User> findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable);
@@ -115,8 +117,6 @@ public interface UserRepository extends CrudRepository<User, Integer> {
// projections
List<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname);
Page<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page);
@@ -137,4 +137,5 @@ public interface UserRepository extends CrudRepository<User, Integer> {
List<User> findByLastnameOrderByFirstname(String lastname);
User findByEmailAddress(String emailAddress);
}