diff --git a/pom.xml b/pom.xml
index dd759f849..b63358380 100755
--- a/pom.xml
+++ b/pom.xml
@@ -145,6 +145,11 @@
${spring}
provided
+
+ org.jboss.logging
+ jboss-logging
+ 3.6.1.Final
+
diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml
index 0bdf2c8e7..43c08369f 100755
--- a/spring-data-envers/pom.xml
+++ b/spring-data-envers/pom.xml
@@ -60,6 +60,12 @@
${project.version}
+
+ org.jboss.logging
+ jboss-logging
+ 3.6.1.Final
+
+
org.hibernate.orm
diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml
index b6470bdc8..12a089e3e 100644
--- a/spring-data-jpa/pom.xml
+++ b/spring-data-jpa/pom.xml
@@ -88,12 +88,16 @@
true
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
- org.junit.platform
- junit-platform-launcher
+ org.springframework
+ spring-core-test
test
-
org.hsqldb
hsqldb
@@ -239,6 +243,12 @@
true
+
+ org.jboss.logging
+ jboss-logging
+ 3.6.1.Final
+
+
@@ -370,6 +380,11 @@
jakarta.persistence-api
${jakarta-persistence-api}
+
+ org.jboss.logging
+ jboss-logging
+ 3.6.1.Final
+
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java
new file mode 100644
index 000000000..98929eead
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotMetaModel.java
@@ -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> managedTypes;
+ private final Lazy entityManagerFactory = Lazy.of(this::init);
+ private final Lazy metamodel = Lazy.of(() -> entityManagerFactory.get().getMetamodel());
+ private final Lazy entityManager = Lazy.of(() -> entityManagerFactory.get().createEntityManager());
+
+ public AotMetaModel(Set> managedTypes) {
+ this("dynamic-tests", managedTypes);
+ }
+
+ private AotMetaModel(String persistenceUnit, Set> 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 EntityType entity(Class cls) {
+ return metamodel.get().entity(cls);
+ }
+
+ @Override
+ public EntityType> entity(String s) {
+ return metamodel.get().entity(s);
+ }
+
+ public ManagedType managedType(Class cls) {
+ return metamodel.get().managedType(cls);
+ }
+
+ public EmbeddableType embeddable(Class cls) {
+ return metamodel.get().embeddable(cls);
+ }
+
+ public Set> getManagedTypes() {
+ return metamodel.get().getManagedTypes();
+ }
+
+ public Set> getEntities() {
+ return metamodel.get().getEntities();
+ }
+
+ public Set> 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 getManagedClassNames() {
+ return persistenceUnitInfo.getManagedClassNames();
+ }
+ }, Map.of("hibernate.dialect", "org.hibernate.dialect.H2Dialect")).build();
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java
new file mode 100644
index 000000000..f6c22ec8f
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java
@@ -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;
+ }
+
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java
new file mode 100644
index 000000000..147eb0a37
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotStringQuery.java
@@ -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 parameterBindings;
+ private final Metadata parameterMetadata;
+ private Limit limit;
+ private boolean nativeQuery;
+
+ public AotStringQuery(String raw, String sanitized, List parameterBindings,
+ Metadata parameterMetadata) {
+ this.raw = raw;
+ this.sanitized = sanitized;
+ this.parameterBindings = parameterBindings;
+ this.parameterMetadata = parameterMetadata;
+ }
+
+ static AotStringQuery of(String raw) {
+
+ List 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 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 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;
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java
new file mode 100644
index 000000000..cf1489a78
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaCodeBlocks.java
@@ -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());
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java
new file mode 100644
index 000000000..57660bccc
--- /dev/null
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepsoitoryContributor.java
@@ -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());
+ });
+ }
+}
diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
index 7abdd4758..44127c452 100644
--- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
+++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java
@@ -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;
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
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
builder.addConstructorArgValue(value);
return builder.getBeanDefinition();
-
}, registry, JpaEvaluationContextExtension.class.getName(), source);
}
@@ -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
+ */
+ static class Meh implements RepositoryConfiguration {
+
+ 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 getBasePackages() {
+ return configuration.getBasePackages();
+ }
+
+ @Override
+ public Streamable getImplementationBasePackages() {
+ return configuration.getImplementationBasePackages();
+ }
+
+ @Override
+ public String getRepositoryInterface() {
+ return configuration.getRepositoryInterface();
+ }
+
+ @Override
+ public Optional