23 changed files with 2341 additions and 18 deletions
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
/* |
||||
* Copyright 2024-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 jakarta.persistence.metamodel.EmbeddableType; |
||||
import jakarta.persistence.metamodel.EntityType; |
||||
import jakarta.persistence.metamodel.ManagedType; |
||||
import jakarta.persistence.metamodel.Metamodel; |
||||
import jakarta.persistence.spi.ClassTransformer; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import org.hibernate.jpa.HibernatePersistenceProvider; |
||||
import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; |
||||
import org.hibernate.jpa.boot.internal.PersistenceUnitInfoDescriptor; |
||||
import org.springframework.data.util.Lazy; |
||||
import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; |
||||
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
public class AotMetaModel implements Metamodel { |
||||
|
||||
private final String persistenceUnit; |
||||
private final Set<Class<?>> managedTypes; |
||||
private final Lazy<EntityManagerFactory> entityManagerFactory = Lazy.of(this::init); |
||||
private final Lazy<Metamodel> metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel()); |
||||
private final Lazy<EntityManager> entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager()); |
||||
|
||||
public AotMetaModel(Set<Class<?>> managedTypes) { |
||||
this("dynamic-tests", managedTypes); |
||||
} |
||||
|
||||
private AotMetaModel(String persistenceUnit, Set<Class<?>> managedTypes) { |
||||
this.persistenceUnit = persistenceUnit; |
||||
this.managedTypes = managedTypes; |
||||
} |
||||
|
||||
public static AotMetaModel hibernateModel(Class<?>... types) { |
||||
return new AotMetaModel(Set.of(types)); |
||||
} |
||||
|
||||
public static AotMetaModel hibernateModel(String persistenceUnit, Class<?>... types) { |
||||
return new AotMetaModel(persistenceUnit, Set.of(types)); |
||||
} |
||||
|
||||
public <X> EntityType<X> entity(Class<X> cls) { |
||||
return metamodel.get().entity(cls); |
||||
} |
||||
|
||||
@Override |
||||
public EntityType<?> entity(String s) { |
||||
return metamodel.get().entity(s); |
||||
} |
||||
|
||||
public <X> ManagedType<X> managedType(Class<X> cls) { |
||||
return metamodel.get().managedType(cls); |
||||
} |
||||
|
||||
public <X> EmbeddableType<X> embeddable(Class<X> cls) { |
||||
return metamodel.get().embeddable(cls); |
||||
} |
||||
|
||||
public Set<ManagedType<?>> getManagedTypes() { |
||||
return metamodel.get().getManagedTypes(); |
||||
} |
||||
|
||||
public Set<EntityType<?>> getEntities() { |
||||
return metamodel.get().getEntities(); |
||||
} |
||||
|
||||
public Set<EmbeddableType<?>> getEmbeddables() { |
||||
return metamodel.get().getEmbeddables(); |
||||
} |
||||
|
||||
public EntityManager entityManager() { |
||||
return entityManager.get(); |
||||
} |
||||
|
||||
EntityManagerFactory init() { |
||||
|
||||
MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo() { |
||||
@Override |
||||
public ClassLoader getNewTempClassLoader() { |
||||
return new SimpleThrowawayClassLoader(this.getClass().getClassLoader()); |
||||
} |
||||
|
||||
@Override |
||||
public void addTransformer(ClassTransformer classTransformer) { |
||||
// just ingnore it
|
||||
} |
||||
}; |
||||
|
||||
persistenceUnitInfo.setPersistenceUnitName(persistenceUnit); |
||||
this.managedTypes.stream().map(Class::getName).forEach(persistenceUnitInfo::addManagedClassName); |
||||
|
||||
persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName()); |
||||
|
||||
return new EntityManagerFactoryBuilderImpl(new PersistenceUnitInfoDescriptor(persistenceUnitInfo) { |
||||
@Override |
||||
public List<String> getManagedClassNames() { |
||||
return persistenceUnitInfo.getManagedClassNames(); |
||||
} |
||||
}, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build(); |
||||
} |
||||
} |
||||
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
/* |
||||
* 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 |
||||
* |
||||
* http://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.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 { |
||||
|
||||
Metamodel metamodel; |
||||
|
||||
public AotQueryCreator(Metamodel metamodel) { |
||||
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,106 @@
@@ -0,0 +1,106 @@
|
||||
/* |
||||
* 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; |
||||
} |
||||
} |
||||
@ -0,0 +1,291 @@
@@ -0,0 +1,291 @@
|
||||
/* |
||||
* 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.Query; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
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; |
||||
import org.springframework.javapoet.TypeName; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
* @since 2025/01 |
||||
*/ |
||||
public class JpaCodeBlocks { |
||||
|
||||
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); |
||||
|
||||
static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { |
||||
return new QueryBlockBuilder(context); |
||||
} |
||||
|
||||
static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { |
||||
return new QueryExecutionBlockBuilder(context); |
||||
} |
||||
|
||||
static class QueryExecutionBlockBuilder { |
||||
|
||||
AotRepositoryMethodGenerationContext context; |
||||
private String queryVariableName; |
||||
|
||||
public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { |
||||
this.context = context; |
||||
} |
||||
|
||||
QueryExecutionBlockBuilder referencing(String queryVariableName) { |
||||
|
||||
this.queryVariableName = queryVariableName; |
||||
return this; |
||||
} |
||||
|
||||
CodeBlock build() { |
||||
|
||||
Builder builder = CodeBlock.builder(); |
||||
|
||||
boolean isProjecting = context.getActualReturnType() != null |
||||
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), |
||||
context.getActualReturnType()); |
||||
Object actualReturnType = isProjecting ? context.getActualReturnType() |
||||
: context.getRepositoryInformation().getDomainType(); |
||||
|
||||
builder.add("\n"); |
||||
|
||||
if (context.isDeleteMethod()) { |
||||
|
||||
builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, queryVariableName); |
||||
builder.addStatement("resultList.forEach($L::remove)", context.fieldNameOf(EntityManager.class)); |
||||
if (context.returnsSingleValue()) { |
||||
if (ClassUtils.isAssignable(Number.class, context.getMethod().getReturnType())) { |
||||
builder.addStatement("return $T.valueOf(resultList.size())", context.getMethod().getReturnType()); |
||||
} else { |
||||
builder.addStatement("return resultList.isEmpty() ? null : resultList.iterator().next()"); |
||||
} |
||||
} else { |
||||
builder.addStatement("return resultList"); |
||||
} |
||||
} else if (context.isExistsMethod()) { |
||||
builder.addStatement("return !$L.getResultList().isEmpty()", queryVariableName); |
||||
} else { |
||||
|
||||
if (context.returnsSingleValue()) { |
||||
if (context.returnsOptionalValue()) { |
||||
builder.addStatement("return $T.ofNullable(($T) $L.getSingleResultOrNull())", Optional.class, |
||||
actualReturnType, queryVariableName); |
||||
} else { |
||||
builder.addStatement("return ($T) $L.getSingleResultOrNull()", context.getReturnType(), queryVariableName); |
||||
} |
||||
} else if (context.returnsPage()) { |
||||
builder.addStatement("return $T.getPage(($T<$T>) $L.getResultList(), $L, countAll)", |
||||
PageableExecutionUtils.class, List.class, actualReturnType, queryVariableName, |
||||
context.getPageableParameterName()); |
||||
} else if (context.returnsSlice()) { |
||||
builder.addStatement("$T<$T> resultList = $L.getResultList()", List.class, actualReturnType, |
||||
queryVariableName); |
||||
builder.addStatement("boolean hasNext = $L.isPaged() && resultList.size() > $L.getPageSize()", |
||||
context.getPageableParameterName(), context.getPageableParameterName()); |
||||
builder.addStatement( |
||||
"return new $T<>(hasNext ? resultList.subList(0, $L.getPageSize()) : resultList, $L, hasNext)", |
||||
SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); |
||||
} else { |
||||
builder.addStatement("return ($T) query.getResultList()", context.getReturnType()); |
||||
} |
||||
} |
||||
|
||||
return builder.build(); |
||||
|
||||
} |
||||
} |
||||
|
||||
static class QueryBlockBuilder { |
||||
|
||||
private final AotRepositoryMethodGenerationContext context; |
||||
private String queryVariableName; |
||||
private AotStringQuery query; |
||||
|
||||
public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { |
||||
this.context = context; |
||||
} |
||||
|
||||
QueryBlockBuilder usingQueryVariableName(String queryVariableName) { |
||||
|
||||
this.queryVariableName = queryVariableName; |
||||
return this; |
||||
} |
||||
|
||||
QueryBlockBuilder filter(String queryString) { |
||||
return filter(AotStringQuery.of(queryString)); |
||||
} |
||||
|
||||
QueryBlockBuilder filter(AotStringQuery query) { |
||||
this.query = query; |
||||
return this; |
||||
} |
||||
|
||||
CodeBlock build() { |
||||
|
||||
boolean isProjecting = context.getActualReturnType() != null |
||||
&& !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), |
||||
context.getActualReturnType()); |
||||
Object actualReturnType = isProjecting ? context.getActualReturnType() |
||||
: context.getRepositoryInformation().getDomainType(); |
||||
|
||||
CodeBlock.Builder builder = CodeBlock.builder(); |
||||
builder.add("\n"); |
||||
String queryStringNameVariableName = "%sString".formatted(queryVariableName); |
||||
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"); |
||||
builder.addStatement("$T $L = $S", String.class, countQueryStringNameVariableName, |
||||
query.getCountQuery(projection)); |
||||
} |
||||
|
||||
// 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(); |
||||
} |
||||
} |
||||
|
||||
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(countQueryStringNameVariableName)) { |
||||
builder.beginControlFlow("$T $L = () ->", LongSupplier.class, "countAll"); |
||||
addQueryBlock(builder, countQuyerVariableName, countQueryStringNameVariableName, query.isNativeQuery()); |
||||
builder.addStatement("return ($T) $L.getSingleResult()", Long.class, countQuyerVariableName); |
||||
|
||||
// end control flow does not work well with lambdas
|
||||
builder.unindent(); |
||||
builder.add("};\n"); |
||||
} |
||||
|
||||
return builder.build(); |
||||
} |
||||
|
||||
private void addQueryBlock(Builder builder, String queryVariableName, String queryStringNameVariableName, |
||||
boolean nativeQuery) { |
||||
|
||||
builder.addStatement("$T $L = this.$L.$L($L)", Query.class, queryVariableName, |
||||
context.fieldNameOf(EntityManager.class), nativeQuery ? "createNativeQuery" : "createQuery", |
||||
queryStringNameVariableName); |
||||
|
||||
for (ParameterBinding binding : query.parameterBindings()) { |
||||
|
||||
Object prepare = binding.prepare("s"); |
||||
if (prepare instanceof String prepared && !prepared.equals("s")) { |
||||
String format = prepared.replaceAll("%", "%%").replace("s", "%s"); |
||||
if (binding.getIdentifier().hasPosition()) { |
||||
builder.addStatement("$L.setParameter($L, $S.formatted($L))", queryVariableName, |
||||
binding.getIdentifier().getPosition(), format, |
||||
context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1)); |
||||
} else { |
||||
builder.addStatement("$L.setParameter($S, $S.formatted($L))", queryVariableName, |
||||
binding.getIdentifier().getName(), format, binding.getIdentifier().getName()); |
||||
} |
||||
} else { |
||||
if (binding.getIdentifier().hasPosition()) { |
||||
builder.addStatement("$L.setParameter($L, $L)", queryVariableName, binding.getIdentifier().getPosition(), |
||||
context.getParameterNameOfPosition(binding.getIdentifier().getPosition() - 1)); |
||||
} else { |
||||
builder.addStatement("$L.setParameter($S, $L)", queryVariableName, binding.getIdentifier().getName(), |
||||
binding.getIdentifier().getName()); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
/* |
||||
* 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,427 @@
@@ -0,0 +1,427 @@
|
||||
/* |
||||
* 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.query; |
||||
|
||||
import static java.util.regex.Pattern.CASE_INSENSITIVE; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
import java.util.function.BiFunction; |
||||
import java.util.function.Consumer; |
||||
import java.util.function.Function; |
||||
import java.util.regex.Matcher; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import org.springframework.data.expression.ValueExpression; |
||||
import org.springframework.data.expression.ValueExpressionParser; |
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier; |
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.InParameterBinding; |
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.LikeParameterBinding; |
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument; |
||||
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterOrigin; |
||||
import org.springframework.data.repository.query.ValueExpressionQueryRewriter; |
||||
import org.springframework.data.repository.query.parser.Part.Type; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* A parser that extracts the parameter bindings from a given query string. |
||||
* |
||||
* @author Thomas Darimont |
||||
*/ |
||||
public enum ParameterBindingParser { |
||||
|
||||
INSTANCE; |
||||
|
||||
private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; |
||||
public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; |
||||
// .....................................................................^ not followed by a hash or a letter.
|
||||
// .................................................................^ zero or more digits.
|
||||
// .............................................................^ start with a question mark.
|
||||
private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); |
||||
private static final Pattern PARAMETER_BINDING_PATTERN; |
||||
private static final Pattern JDBC_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?(?!\\d)"); // no \ and [no digit]
|
||||
private static final Pattern NUMBERED_STYLE_PARAM = Pattern.compile("(?!\\\\)\\?\\d"); // no \ and [digit]
|
||||
private static final Pattern NAMED_STYLE_PARAM = Pattern.compile("(?!\\\\):\\w+"); // no \ and :[text]
|
||||
|
||||
private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type; " |
||||
+ "Already have: %s, found %s; If you bind a parameter multiple times make sure they use the same binding"; |
||||
private static final int INDEXED_PARAMETER_GROUP = 4; |
||||
private static final int NAMED_PARAMETER_GROUP = 6; |
||||
private static final int COMPARISION_TYPE_GROUP = 1; |
||||
|
||||
public static class Metadata { |
||||
private boolean usesJdbcStyleParameters = false; |
||||
|
||||
public boolean usesJdbcStyleParameters() { |
||||
return usesJdbcStyleParameters; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Utility to create unique parameter bindings for LIKE that refer to the same underlying method parameter but are |
||||
* bound to potentially unique query parameters for {@link LikeParameterBinding#prepare(Object) LIKE rewrite}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 3.1.2 |
||||
*/ |
||||
static class ParameterBindings { |
||||
|
||||
private final MultiValueMap<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>(); |
||||
|
||||
private final Consumer<ParameterBinding> registration; |
||||
private int syntheticParameterIndex; |
||||
|
||||
public ParameterBindings(List<ParameterBinding> bindings, Consumer<ParameterBinding> registration, |
||||
int syntheticParameterIndex) { |
||||
|
||||
for (ParameterBinding binding : bindings) { |
||||
this.methodArgumentToLikeBindings.put(binding.getIdentifier(), new ArrayList<>(List.of(binding))); |
||||
} |
||||
|
||||
this.registration = registration; |
||||
this.syntheticParameterIndex = syntheticParameterIndex; |
||||
} |
||||
|
||||
/** |
||||
* Return whether the identifier is already bound. |
||||
* |
||||
* @param identifier |
||||
* @return |
||||
*/ |
||||
public boolean isBound(BindingIdentifier identifier) { |
||||
return !getBindings(identifier).isEmpty(); |
||||
} |
||||
|
||||
BindingIdentifier register(BindingIdentifier identifier, ParameterOrigin origin, |
||||
Function<BindingIdentifier, ParameterBinding> bindingFactory) { |
||||
|
||||
Assert.isInstanceOf(MethodInvocationArgument.class, origin); |
||||
|
||||
BindingIdentifier methodArgument = ((MethodInvocationArgument) origin).identifier(); |
||||
List<ParameterBinding> bindingsForOrigin = getBindings(methodArgument); |
||||
|
||||
if (!isBound(identifier)) { |
||||
|
||||
ParameterBinding binding = bindingFactory.apply(identifier); |
||||
registration.accept(binding); |
||||
bindingsForOrigin.add(binding); |
||||
return binding.getIdentifier(); |
||||
} |
||||
|
||||
ParameterBinding binding = bindingFactory.apply(identifier); |
||||
|
||||
for (ParameterBinding existing : bindingsForOrigin) { |
||||
|
||||
if (existing.isCompatibleWith(binding)) { |
||||
return existing.getIdentifier(); |
||||
} |
||||
} |
||||
|
||||
BindingIdentifier syntheticIdentifier; |
||||
if (identifier.hasName() && methodArgument.hasName()) { |
||||
|
||||
int index = 0; |
||||
String newName = methodArgument.getName(); |
||||
while (existsBoundParameter(newName)) { |
||||
index++; |
||||
newName = methodArgument.getName() + "_" + index; |
||||
} |
||||
syntheticIdentifier = BindingIdentifier.of(newName); |
||||
} else { |
||||
syntheticIdentifier = BindingIdentifier.of(++syntheticParameterIndex); |
||||
} |
||||
|
||||
ParameterBinding newBinding = bindingFactory.apply(syntheticIdentifier); |
||||
registration.accept(newBinding); |
||||
bindingsForOrigin.add(newBinding); |
||||
return newBinding.getIdentifier(); |
||||
} |
||||
|
||||
private boolean existsBoundParameter(String key) { |
||||
return methodArgumentToLikeBindings.values().stream().flatMap(Collection::stream) |
||||
.anyMatch(it -> key.equals(it.getName())); |
||||
} |
||||
|
||||
private List<ParameterBinding> getBindings(BindingIdentifier identifier) { |
||||
return methodArgumentToLikeBindings.computeIfAbsent(identifier, s -> new ArrayList<>()); |
||||
} |
||||
|
||||
public void register(ParameterBinding parameterBinding) { |
||||
registration.accept(parameterBinding); |
||||
} |
||||
} |
||||
|
||||
static { |
||||
|
||||
List<String> keywords = new ArrayList<>(); |
||||
|
||||
for (ParameterBindingType type : ParameterBindingType.values()) { |
||||
if (type.getKeyword() != null) { |
||||
keywords.add(type.getKeyword()); |
||||
} |
||||
} |
||||
|
||||
StringBuilder builder = new StringBuilder(); |
||||
builder.append("("); |
||||
builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
|
||||
builder.append(")?"); |
||||
builder.append("(?: )?"); // some whitespace
|
||||
builder.append("\\(?"); // optional braces around parameters
|
||||
builder.append("("); |
||||
builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
|
||||
builder.append("|"); // or
|
||||
|
||||
// named parameter and the parameter name
|
||||
builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?"); |
||||
|
||||
builder.append(")"); |
||||
builder.append("\\)?"); // optional braces around parameters
|
||||
|
||||
PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); |
||||
} |
||||
|
||||
/** |
||||
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns |
||||
* the cleaned up query. |
||||
*/ |
||||
public String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List<ParameterBinding> bindings, |
||||
Metadata queryMeta) { |
||||
|
||||
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query); |
||||
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1; |
||||
|
||||
/* |
||||
* Prefer indexed access over named parameters if only SpEL Expression parameters are present. |
||||
*/ |
||||
if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { |
||||
parametersShouldBeAccessedByIndex = true; |
||||
greatestParameterIndex = 0; |
||||
} |
||||
|
||||
ValueExpressionQueryRewriter.ParsedQuery parsedQuery = createSpelExtractor(query, |
||||
parametersShouldBeAccessedByIndex, |
||||
greatestParameterIndex); |
||||
|
||||
String resultingQuery = parsedQuery.getQueryString(); |
||||
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); |
||||
|
||||
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; |
||||
int syntheticParameterIndex = expressionParameterIndex + parsedQuery.size(); |
||||
|
||||
ParameterBindings parameterBindings = new ParameterBindings(bindings, it -> checkAndRegister(it, bindings), |
||||
syntheticParameterIndex); |
||||
int currentIndex = 0; |
||||
|
||||
boolean usesJpaStyleParameters = false; |
||||
|
||||
while (matcher.find()) { |
||||
|
||||
if (parsedQuery.isQuoted(matcher.start())) { |
||||
continue; |
||||
} |
||||
|
||||
String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP); |
||||
String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP); |
||||
Integer parameterIndex = getParameterIndex(parameterIndexString); |
||||
|
||||
String match = matcher.group(0); |
||||
if (JDBC_STYLE_PARAM.matcher(match).find()) { |
||||
queryMeta.usesJdbcStyleParameters = true; |
||||
} |
||||
|
||||
if (NUMBERED_STYLE_PARAM.matcher(match).find() || NAMED_STYLE_PARAM.matcher(match).find()) { |
||||
usesJpaStyleParameters = true; |
||||
} |
||||
|
||||
if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) { |
||||
throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported"); |
||||
} |
||||
|
||||
String typeSource = matcher.group(COMPARISION_TYPE_GROUP); |
||||
Assert.isTrue(parameterIndexString != null || parameterName != null, |
||||
() -> String.format("We need either a name or an index; Offending query string: %s", query)); |
||||
ValueExpression expression = parsedQuery |
||||
.getParameter(parameterName == null ? parameterIndexString : parameterName); |
||||
String replacement = null; |
||||
|
||||
expressionParameterIndex++; |
||||
if ("".equals(parameterIndexString)) { |
||||
parameterIndex = expressionParameterIndex; |
||||
} |
||||
|
||||
BindingIdentifier queryParameter; |
||||
if (parameterIndex != null) { |
||||
queryParameter = BindingIdentifier.of(parameterIndex); |
||||
} else { |
||||
queryParameter = BindingIdentifier.of(parameterName); |
||||
} |
||||
ParameterOrigin origin = ObjectUtils.isEmpty(expression) |
||||
? ParameterOrigin.ofParameter(parameterName, parameterIndex) |
||||
: ParameterOrigin.ofExpression(expression); |
||||
|
||||
BindingIdentifier targetBinding = queryParameter; |
||||
Function<BindingIdentifier, ParameterBinding> bindingFactory = switch (ParameterBindingType.of(typeSource)) { |
||||
case LIKE -> { |
||||
|
||||
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); |
||||
yield (identifier) -> new LikeParameterBinding(identifier, origin, likeType); |
||||
} |
||||
case IN -> (identifier) -> new InParameterBinding(identifier, origin); // fall-through we don't need a special parameter queryParameter for the given parameter.
|
||||
default -> (identifier) -> new ParameterBinding(identifier, origin); |
||||
}; |
||||
|
||||
if (origin.isExpression()) { |
||||
parameterBindings.register(bindingFactory.apply(queryParameter)); |
||||
} else { |
||||
targetBinding = parameterBindings.register(queryParameter, origin, bindingFactory); |
||||
} |
||||
|
||||
replacement = targetBinding.hasName() ? ":" + targetBinding.getName() |
||||
: ((!usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) ? "?" |
||||
: "?" + targetBinding.getPosition()); |
||||
String result; |
||||
String substring = matcher.group(2); |
||||
|
||||
int index = resultingQuery.indexOf(substring, currentIndex); |
||||
if (index < 0) { |
||||
result = resultingQuery; |
||||
} else { |
||||
currentIndex = index + replacement.length(); |
||||
result = resultingQuery.substring(0, index) + replacement |
||||
+ resultingQuery.substring(index + substring.length()); |
||||
} |
||||
|
||||
resultingQuery = result; |
||||
} |
||||
|
||||
return resultingQuery; |
||||
} |
||||
|
||||
private static ValueExpressionQueryRewriter.ParsedQuery createSpelExtractor(String queryWithSpel, |
||||
boolean parametersShouldBeAccessedByIndex, |
||||
int greatestParameterIndex) { |
||||
|
||||
/* |
||||
* If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to |
||||
* not mix-up with the actual parameter indices. |
||||
*/ |
||||
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; |
||||
|
||||
BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex |
||||
? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1) |
||||
: (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1); |
||||
|
||||
String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; |
||||
|
||||
BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; |
||||
ValueExpressionQueryRewriter rewriter = ValueExpressionQueryRewriter.of(ValueExpressionParser.create(), |
||||
indexToParameterName, parameterNameToReplacement); |
||||
|
||||
return rewriter.parse(queryWithSpel); |
||||
} |
||||
|
||||
@Nullable |
||||
private static Integer getParameterIndex(@Nullable String parameterIndexString) { |
||||
|
||||
if (parameterIndexString == null || parameterIndexString.isEmpty()) { |
||||
return null; |
||||
} |
||||
return Integer.valueOf(parameterIndexString); |
||||
} |
||||
|
||||
private static int tryFindGreatestParameterIndexIn(String query) { |
||||
|
||||
Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); |
||||
|
||||
int greatestParameterIndex = -1; |
||||
while (parameterIndexMatcher.find()) { |
||||
|
||||
String parameterIndexString = parameterIndexMatcher.group(1); |
||||
Integer parameterIndex = getParameterIndex(parameterIndexString); |
||||
if (parameterIndex != null) { |
||||
greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex); |
||||
} |
||||
} |
||||
|
||||
return greatestParameterIndex; |
||||
} |
||||
|
||||
private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) { |
||||
|
||||
bindings.stream() //
|
||||
.filter(it -> it.bindsTo(binding)) //
|
||||
.forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); |
||||
|
||||
if (!bindings.contains(binding)) { |
||||
bindings.add(binding); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* An enum for the different types of bindings. |
||||
* |
||||
* @author Thomas Darimont |
||||
* @author Oliver Gierke |
||||
*/ |
||||
private enum ParameterBindingType { |
||||
|
||||
// Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
|
||||
// character, while = does not.
|
||||
LIKE("like "), IN("in "), AS_IS(null); |
||||
|
||||
private final @Nullable String keyword; |
||||
|
||||
ParameterBindingType(@Nullable String keyword) { |
||||
this.keyword = keyword; |
||||
} |
||||
|
||||
/** |
||||
* Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a |
||||
* keyword. |
||||
* |
||||
* @return the keyword |
||||
*/ |
||||
@Nullable |
||||
public String getKeyword() { |
||||
return keyword; |
||||
} |
||||
|
||||
/** |
||||
* Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in |
||||
* case no other {@link ParameterBindingType} could be found. |
||||
*/ |
||||
static ParameterBindingType of(String typeSource) { |
||||
|
||||
if (!StringUtils.hasText(typeSource)) { |
||||
return AS_IS; |
||||
} |
||||
|
||||
for (ParameterBindingType type : values()) { |
||||
if (type.name().equalsIgnoreCase(typeSource.trim())) { |
||||
return type; |
||||
} |
||||
} |
||||
|
||||
throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s", typeSource)); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
/* |
||||
* 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 com.example; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
* @since 2025/01 |
||||
*/ |
||||
public class UserDtoProjection { |
||||
|
||||
private final String firstname; |
||||
private final String emailAddress; |
||||
|
||||
public UserDtoProjection(String firstname, String emailAddress) { |
||||
this.firstname = firstname; |
||||
this.emailAddress = emailAddress; |
||||
} |
||||
|
||||
public String getFirstname() { |
||||
return firstname; |
||||
} |
||||
|
||||
public String getEmailAddress() { |
||||
return emailAddress; |
||||
} |
||||
} |
||||
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
/* |
||||
* 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 com.example; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
import org.springframework.data.domain.Limit; |
||||
import org.springframework.data.domain.Page; |
||||
import org.springframework.data.domain.Pageable; |
||||
import org.springframework.data.domain.Slice; |
||||
import org.springframework.data.domain.Sort; |
||||
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 |
||||
*/ |
||||
public interface UserRepository extends CrudRepository<User, Integer> { |
||||
|
||||
List<User> findUserNoArgumentsBy(); |
||||
|
||||
User findOneByEmailAddress(String emailAddress); |
||||
|
||||
Optional<User> findOptionalOneByEmailAddress(String emailAddress); |
||||
|
||||
Long countUsersByLastname(String lastname); |
||||
|
||||
Boolean existsUserByLastname(String lastname); |
||||
|
||||
List<User> findByLastnameStartingWith(String lastname); |
||||
|
||||
List<User> findTop2ByLastnameStartingWith(String lastname); |
||||
|
||||
List<User> findByLastnameStartingWithOrderByEmailAddress(String lastname); |
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Limit limit); |
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Sort sort); |
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Sort sort, Limit limit); |
||||
|
||||
List<User> findByLastnameStartingWith(String lastname, Pageable page); |
||||
|
||||
Page<User> findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); |
||||
|
||||
Slice<User> findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); |
||||
|
||||
/* Annotated Queries */ |
||||
|
||||
@Query("select u from User u where u.emailAddress = ?1") |
||||
User findAnnotatedQueryByEmailAddress(String username); |
||||
|
||||
@Query("select u from User u where u.lastname like ?1%") |
||||
List<User> findAnnotatedQueryByLastname(String lastname); |
||||
|
||||
@Query("select u from User u where u.lastname like :lastname%") |
||||
List<User> findAnnotatedQueryByLastnameParamter(String lastname); |
||||
|
||||
@Query(""" |
||||
select u |
||||
from User u |
||||
where u.lastname LIKE ?1%""") |
||||
List<User> findAnnotatedMultilineQueryByLastname(String username); |
||||
|
||||
@Query("select u from User u where u.lastname like ?1%") |
||||
List<User> findAnnotatedQueryByLastname(String lastname, Limit limit); |
||||
|
||||
@Query("select u from User u where u.lastname like ?1%") |
||||
List<User> findAnnotatedQueryByLastname(String lastname, Sort sort); |
||||
|
||||
@Query("select u from User u where u.lastname like ?1%") |
||||
List<User> findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort); |
||||
|
||||
@Query("select u from User u where u.lastname like ?1%") |
||||
List<User> findAnnotatedQueryByLastname(String lastname, Pageable pageable); |
||||
|
||||
@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%") |
||||
Slice<User> findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); |
||||
|
||||
// modifying
|
||||
|
||||
User deleteByEmailAddress(String username); |
||||
|
||||
Long deleteReturningDeleteCountByEmailAddress(String username); |
||||
|
||||
@Modifying |
||||
@Query("delete from User u where u.emailAddress = ?1") |
||||
User deleteAnnotatedQueryByEmailAddress(String username); |
||||
|
||||
// native queries
|
||||
|
||||
@Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User", |
||||
nativeQuery = true) |
||||
Page<String> findByNativeQueryWithPageable(Pageable pageable); |
||||
|
||||
// projections
|
||||
|
||||
|
||||
|
||||
List<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname); |
||||
|
||||
Page<UserDtoProjection> findUserProjectionByLastnameStartingWith(String lastname, Pageable page); |
||||
|
||||
// old ones
|
||||
|
||||
@Query("select u from User u where u.firstname = ?1") |
||||
List<User> findAllUsingAnnotatedJpqlQuery(String firstname); |
||||
|
||||
List<User> findByLastname(String lastname); |
||||
|
||||
List<User> findByLastnameStartingWithOrderByFirstname(String lastname, Limit limit); |
||||
|
||||
List<User> findByLastname(String lastname, Sort sort); |
||||
|
||||
List<User> findByLastname(String lastname, Pageable page); |
||||
|
||||
List<User> findByLastnameOrderByFirstname(String lastname); |
||||
|
||||
User findByEmailAddress(String emailAddress); |
||||
} |
||||
@ -0,0 +1,614 @@
@@ -0,0 +1,614 @@
|
||||
/* |
||||
* 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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
/* |
||||
* 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 java.lang.reflect.Method; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.data.jpa.repository.support.SimpleJpaRepository; |
||||
import org.springframework.data.repository.core.CrudMethods; |
||||
import org.springframework.data.repository.core.RepositoryInformation; |
||||
import org.springframework.data.repository.core.RepositoryMetadata; |
||||
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; |
||||
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 |
||||
*/ |
||||
class StubRepositoryInformation implements RepositoryInformation { |
||||
|
||||
private final RepositoryMetadata metadata; |
||||
private final RepositoryComposition baseComposition; |
||||
|
||||
public StubRepositoryInformation(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { |
||||
|
||||
this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); |
||||
this.baseComposition = composition != null ? composition |
||||
: RepositoryComposition.of(RepositoryFragment.structural(SimpleJpaRepository.class)); |
||||
} |
||||
|
||||
@Override |
||||
public TypeInformation<?> getIdTypeInformation() { |
||||
return metadata.getIdTypeInformation(); |
||||
} |
||||
|
||||
@Override |
||||
public TypeInformation<?> getDomainTypeInformation() { |
||||
return metadata.getDomainTypeInformation(); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getRepositoryInterface() { |
||||
return metadata.getRepositoryInterface(); |
||||
} |
||||
|
||||
@Override |
||||
public TypeInformation<?> getReturnType(Method method) { |
||||
return metadata.getReturnType(method); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getReturnedDomainClass(Method method) { |
||||
return metadata.getReturnedDomainClass(method); |
||||
} |
||||
|
||||
@Override |
||||
public CrudMethods getCrudMethods() { |
||||
return metadata.getCrudMethods(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isPagingRepository() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public Set<Class<?>> getAlternativeDomainTypes() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isReactiveRepository() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public Set<RepositoryFragment<?>> getFragments() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isBaseClassMethod(Method method) { |
||||
return baseComposition.findMethod(method).isPresent(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isCustomMethod(Method method) { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isQueryMethod(Method method) { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public Streamable<Method> getQueryMethods() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getRepositoryBaseClass() { |
||||
return SimpleJpaRepository.class; |
||||
} |
||||
|
||||
@Override |
||||
public Method getTargetClassMethod(Method method) { |
||||
return null; |
||||
} |
||||
} |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* 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.Entity; |
||||
import jakarta.persistence.MappedSuperclass; |
||||
|
||||
import java.io.IOException; |
||||
import java.lang.annotation.Annotation; |
||||
import java.util.List; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; |
||||
import org.springframework.core.annotation.MergedAnnotation; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.core.test.tools.ClassFile; |
||||
import org.springframework.data.jpa.domain.sample.Role; |
||||
import org.springframework.data.jpa.domain.sample.User; |
||||
import org.springframework.data.repository.config.AotRepositoryContext; |
||||
import org.springframework.data.repository.core.RepositoryInformation; |
||||
import org.springframework.data.repository.core.support.RepositoryComposition; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* @author Christoph Strobl |
||||
*/ |
||||
class TestJpaAotRepsitoryContext implements AotRepositoryContext { |
||||
|
||||
private final StubRepositoryInformation repositoryInformation; |
||||
|
||||
public TestJpaAotRepsitoryContext(Class<?> repositoryInterface, @Nullable RepositoryComposition composition) { |
||||
this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); |
||||
} |
||||
|
||||
@Override |
||||
public ConfigurableListableBeanFactory getBeanFactory() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public TypeIntrospector introspectType(String typeName) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public String getBeanName() { |
||||
return "dummyRepository"; |
||||
} |
||||
|
||||
@Override |
||||
public Set<String> getBasePackages() { |
||||
return Set.of("org.springframework.data.dummy.repository.aot"); |
||||
} |
||||
|
||||
@Override |
||||
public Set<Class<? extends Annotation>> getIdentifyingAnnotations() { |
||||
return Set.of(Entity.class, MappedSuperclass.class); |
||||
} |
||||
|
||||
@Override |
||||
public RepositoryInformation getRepositoryInformation() { |
||||
return repositoryInformation; |
||||
} |
||||
|
||||
@Override |
||||
public Set<MergedAnnotation<Annotation>> getResolvedAnnotations() { |
||||
return Set.of(); |
||||
} |
||||
|
||||
@Override |
||||
public Set<Class<?>> getResolvedTypes() { |
||||
return Set.of(User.class, Role.class); |
||||
} |
||||
|
||||
public List<ClassFile> getRequiredContextFiles() { |
||||
return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); |
||||
} |
||||
|
||||
static ClassFile classFileForType(Class<?> type) { |
||||
|
||||
String name = type.getName(); |
||||
ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); |
||||
|
||||
try { |
||||
return ClassFile.of(name, cpr.getContentAsByteArray()); |
||||
} catch (IOException e) { |
||||
throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue