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:
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user