Browse Source

Add support for AOT repositories.

Closes #3830
pull/3901/head
Christoph Strobl 1 year ago committed by Mark Paluch
parent
commit
831d04dd2c
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 5
      pom.xml
  2. 6
      spring-data-envers/pom.xml
  3. 21
      spring-data-jpa/pom.xml
  4. 124
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java
  5. 62
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java
  6. 106
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java
  7. 291
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java
  8. 115
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java
  9. 142
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
  10. 12
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java
  11. 4
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java
  12. 427
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java
  13. 5
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java
  14. 2
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java
  15. 5
      spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java
  16. 39
      spring-data-jpa/src/test/java/com/example/UserDtoProjection.java
  17. 140
      spring-data-jpa/src/test/java/com/example/UserRepository.java
  18. 614
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java
  19. 126
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java
  20. 108
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java
  21. 1
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java
  22. 2
      spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/NameOnlyDto.java
  23. 2
      spring-data-jpa/src/test/resources/logback.xml

5
pom.xml

@ -145,6 +145,11 @@ @@ -145,6 +145,11 @@
<version>${spring}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.6.1.Final</version>
</dependency>
</dependencies>
<build>

6
spring-data-envers/pom.xml

@ -60,6 +60,12 @@ @@ -60,6 +60,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.6.1.Final</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate.orm</groupId>

21
spring-data-jpa/pom.xml

@ -88,12 +88,16 @@ @@ -88,12 +88,16 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<groupId>org.springframework</groupId>
<artifactId>spring-core-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
@ -239,6 +243,12 @@ @@ -239,6 +243,12 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.6.1.Final</version>
</dependency>
</dependencies>
<build>
@ -370,6 +380,11 @@ @@ -370,6 +380,11 @@
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta-persistence-api}</version>
</path>
<path>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.6.1.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

124
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java

@ -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();
}
}

62
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java

@ -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;
}
}

106
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java

@ -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;
}
}

291
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java

@ -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());
}
}
}
}
}
}

115
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java

@ -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());
});
}
}

142
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java

@ -15,7 +15,9 @@ @@ -15,7 +15,9 @@
*/
package org.springframework.data.jpa.repository.config;
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*;
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 jakarta.persistence.Entity;
import jakarta.persistence.MappedSuperclass;
@ -41,23 +43,34 @@ import org.springframework.beans.factory.config.BeanDefinition; @@ -41,23 +43,34 @@ 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.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
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.support.DefaultJpaContext;
import org.springframework.data.jpa.repository.support.EntityManagerBeanDefinitionRegistrarPostProcessor;
import org.springframework.data.jpa.repository.support.JpaEvaluationContextExtension;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.aot.generate.RepositoryContributor;
import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
import org.springframework.data.repository.config.AotRepositoryContext;
import org.springframework.data.repository.config.ImplementationDetectionConfiguration;
import org.springframework.data.repository.config.ImplementationLookupConfiguration;
import org.springframework.data.repository.config.RepositoryConfiguration;
import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor;
import org.springframework.data.repository.config.XmlRepositoryConfigurationSource;
import org.springframework.data.util.Streamable;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@ -193,7 +206,6 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi @@ -193,7 +206,6 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
contextDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
return contextDefinition;
}, registry, JPA_CONTEXT_BEAN_NAME, source);
registerIfNotAlreadyRegistered(() -> new RootBeanDefinition(JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME), registry,
@ -211,7 +223,6 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi @@ -211,7 +223,6 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
builder.addConstructorArgValue(value);
return builder.getBeanDefinition();
}, registry, JpaEvaluationContextExtension.class.getName(), source);
}
@ -316,8 +327,131 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi @@ -316,8 +327,131 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
*/
public static class JpaRepositoryRegistrationAotProcessor extends RepositoryRegistrationAotProcessor {
protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) {
// don't register domain types nor annotations.
if (!AotContext.aotGeneratedRepositoriesEnabled()) {
return null;
}
return new JpaRepsoitoryContributor(repositoryContext);
}
@Nullable
@Override
protected RepositoryConfiguration<?> getRepositoryMetadata(RegisteredBean bean) {
RepositoryConfiguration<?> configuration = super.getRepositoryMetadata(bean);
if (!configuration.getRepositoryBaseClassName().isEmpty()) {
return configuration;
}
return new Meh<>(configuration);
}
}
/**
* I'm just a dirty hack so we can refine the {@link #getRepositoryBaseClassName()} method as we cannot instantiate
* the bean safely to extract it form the repository factory in data commons. So we either have a configurable
* {@link RepositoryConfiguration} return from
* {@link RepositoryRegistrationAotProcessor#getRepositoryMetadata(RegisteredBean)} or change the arrangement and
* maybe move the type out of the factoy.
*
* @param <T>
*/
static class Meh<T extends RepositoryConfigurationSource> implements RepositoryConfiguration<T> {
private RepositoryConfiguration<?> configuration;
public Meh(RepositoryConfiguration<?> configuration) {
this.configuration = configuration;
}
@Nullable
@Override
public Object getSource() {
return configuration.getSource();
}
@Override
public T getConfigurationSource() {
return (T) configuration.getConfigurationSource();
}
@Override
public boolean isLazyInit() {
return configuration.isLazyInit();
}
@Override
public boolean isPrimary() {
return configuration.isPrimary();
}
@Override
public Streamable<String> getBasePackages() {
return configuration.getBasePackages();
}
@Override
public Streamable<String> getImplementationBasePackages() {
return configuration.getImplementationBasePackages();
}
@Override
public String getRepositoryInterface() {
return configuration.getRepositoryInterface();
}
@Override
public Optional<Object> getQueryLookupStrategyKey() {
return Optional.ofNullable(configuration.getQueryLookupStrategyKey());
}
@Override
public Optional<String> getNamedQueriesLocation() {
return configuration.getNamedQueriesLocation();
}
@Override
public Optional<String> getRepositoryBaseClassName() {
String name = SimpleJpaRepository.class.getName();
return Optional.of(name);
}
@Override
public String getRepositoryFactoryBeanClassName() {
return configuration.getRepositoryFactoryBeanClassName();
}
@Override
public String getImplementationBeanName() {
return configuration.getImplementationBeanName();
}
@Override
public String getRepositoryBeanName() {
return configuration.getRepositoryBeanName();
}
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return configuration.getExcludeFilters();
}
@Override
public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) {
return configuration.toImplementationDetectionConfiguration(factory);
}
@Override
public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) {
return configuration.toLookupConfiguration(factory);
}
@Nullable
@Override
public String getResourceDescription() {
return configuration.getResourceDescription();
}
}
}

12
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java

@ -63,7 +63,7 @@ import org.springframework.util.Assert; @@ -63,7 +63,7 @@ import org.springframework.util.Assert;
* @author Christoph Strobl
* @author Jinmyeong Kim
*/
class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
public class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Predicate> implements JpqlQueryCreator {
private final ReturnedType returnedType;
private final ParameterMetadataProvider provider;
@ -86,15 +86,21 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred @@ -86,15 +86,21 @@ class JpaQueryCreator extends AbstractQueryCreator<String, JpqlQueryBuilder.Pred
public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, EntityManager em) {
this(tree, type, provider, templates, em.getMetamodel());
}
public JpaQueryCreator(PartTree tree, ReturnedType type, ParameterMetadataProvider provider,
JpqlQueryTemplates templates, Metamodel metamodel) {
super(tree);
this.tree = tree;
this.returnedType = type;
this.provider = provider;
this.templates = templates;
this.escape = provider.getEscape();
this.entityType = em.getMetamodel().entity(type.getDomainType());
this.entityType = metamodel.entity(type.getDomainType());
this.entity = JpqlQueryBuilder.entity(returnedType.getDomainType());
this.metamodel = em.getMetamodel();
this.metamodel = metamodel;
}
Bindable<?> getFrom() {

4
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java

@ -46,7 +46,7 @@ import org.springframework.util.StringUtils; @@ -46,7 +46,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
* @author Christoph Strobl
*/
class ParameterBinding {
public class ParameterBinding {
private final BindingIdentifier identifier;
private final ParameterOrigin origin;
@ -462,7 +462,7 @@ class ParameterBinding { @@ -462,7 +462,7 @@ class ParameterBinding {
* @author Mark Paluch
* @since 3.1.2
*/
sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed {
public sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed {
/**
* Creates an identifier for the given {@code name}.

427
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBindingParser.java

@ -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));
}
}
}

5
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PreprocessedQuery.java

@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
*/
package org.springframework.data.jpa.repository.query;
import static java.util.regex.Pattern.*;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import java.util.ArrayList;
import java.util.Collection;
@ -34,6 +34,7 @@ import org.jspecify.annotations.Nullable; @@ -34,6 +34,7 @@ import org.jspecify.annotations.Nullable;
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.repository.query.ValueExpressionQueryRewriter;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.util.Assert;
@ -463,7 +464,7 @@ final class PreprocessedQuery implements DeclaredQuery { @@ -463,7 +464,7 @@ final class PreprocessedQuery implements DeclaredQuery {
*/
private static class ParameterBindings {
private final MultiValueMap<ParameterBinding.BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
private final MultiValueMap<BindingIdentifier, ParameterBinding> methodArgumentToLikeBindings = new LinkedMultiValueMap<>();
private final Consumer<ParameterBinding> registration;

2
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java

@ -578,7 +578,7 @@ public abstract class QueryUtils { @@ -578,7 +578,7 @@ public abstract class QueryUtils {
* @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
* @since 2.7.8
*/
static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) {
public static String createCountQueryFor(String originalQuery, @Nullable String countProjection, boolean nativeQuery) {
Assert.hasText(originalQuery, "OriginalQuery must not be null or empty");

5
spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java

@ -105,11 +105,12 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID> @@ -105,11 +105,12 @@ public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
* fallback to {@link org.springframework.data.jpa.repository.query.DefaultJpaQueryMethodFactory} in case none is
* available.
*
* @param factory may be {@literal null}.
* @param resolver may be {@literal null}.
*/
@Autowired
public void setQueryMethodFactory(@Nullable JpaQueryMethodFactory factory) {
public void setQueryMethodFactory(ObjectProvider<JpaQueryMethodFactory> resolver) { // TODO: nullable insteand of ObjectProvider
JpaQueryMethodFactory factory = resolver.getIfAvailable();
if (factory != null) {
this.queryMethodFactory = factory;
}

39
spring-data-jpa/src/test/java/com/example/UserDtoProjection.java

@ -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;
}
}

140
spring-data-jpa/src/test/java/com/example/UserRepository.java

@ -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);
}

614
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorUnitTests.java

@ -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);
}
}
}

126
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/StubRepositoryInformation.java

@ -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;
}
}

108
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/TestJpaAotRepsitoryContext.java

@ -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()));
}
}
}

1
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/DefaultEntityQueryUnitTests.java

@ -28,6 +28,7 @@ import org.springframework.data.jpa.repository.query.ParameterBinding.InParamete @@ -28,6 +28,7 @@ import org.springframework.data.jpa.repository.query.ParameterBinding.InParamete
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.jpa.repository.query.ParameterBindingParser.Metadata;
import org.springframework.data.repository.query.parser.Part.Type;
/**

2
spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/NameOnlyDto.java

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
package org.springframework.data.jpa.repository.sample;
// DATAJPA-1334
class NameOnlyDto {
public class NameOnlyDto {
private String firstname;
private String lastname;

2
spring-data-jpa/src/test/resources/logback.xml

@ -19,6 +19,8 @@ @@ -19,6 +19,8 @@
<!-- <logger name="org.testcontainers" level="debug" />-->
<logger name="org.springframework.data.repository.aot.generate.RepositoryContributor" level="trace" />
<root level="error">
<appender-ref ref="console"/>
</root>

Loading…
Cancel
Save