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 deleted file mode 100644 index 98254aff9..000000000 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/AotQueryCreator.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -class AotQueryCreator { - - Metamodel metamodel; - - public AotQueryCreator(Metamodel metamodel) { - this.metamodel = metamodel; - } - - - -} 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 index 3f249fdf4..edf9bfbd8 100644 --- 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 @@ -226,31 +226,49 @@ class JpaCodeBlocks { for (ParameterBinding binding : query.getParameterBindings()) { Object prepare = binding.prepare("s"); + Object parameterIdentifier = getParameterName(binding.getIdentifier()); + String valueFormat = parameterIdentifier instanceof CharSequence ? "$S" : "$L"; 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()); - } + builder.addStatement("$L.setParameter(%s, $S.formatted($L))".formatted(valueFormat), queryVariableName, + parameterIdentifier, format, getParameter(binding.getOrigin())); } 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()); - } + builder.addStatement("$L.setParameter(%s, $L)".formatted(valueFormat), queryVariableName, parameterIdentifier, + getParameter(binding.getOrigin())); } } return builder.build(); } + private Object getParameterName(ParameterBinding.BindingIdentifier identifier) { + + if (identifier.hasPosition()) { + return identifier.getPosition(); + } + + return identifier.getName(); + + } + + private Object getParameter(ParameterBinding.ParameterOrigin origin) { + + if (origin.isMethodArgument() && origin instanceof ParameterBinding.MethodInvocationArgument mia) { + + if (mia.identifier().hasPosition()) { + return context.getParameterNameOfPosition(mia.identifier().getPosition() - 1); + } + + if (mia.identifier().hasName()) { + return mia.identifier().getName(); + } + } + + throw new UnsupportedOperationException("Not supported yet"); + } + private CodeBlock applyHints(String queryVariableName, MergedAnnotation queryHints) { Builder hintsBuilder = CodeBlock.builder(); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java index c16f8a6ed..7f29a51f5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributor.java @@ -23,6 +23,8 @@ import java.util.regex.Pattern; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; @@ -59,14 +61,12 @@ import org.springframework.util.StringUtils; public class JpaRepositoryContributor extends RepositoryContributor { private final CollectionAwareProjectionFactory projectionFactory = new CollectionAwareProjectionFactory(); - private final AotQueryCreator queryCreator; private final AotMetaModel metaModel; public JpaRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); this.metaModel = new AotMetaModel(repositoryContext.getResolvedTypes()); - this.queryCreator = new AotQueryCreator(metaModel); } @Override @@ -106,6 +106,12 @@ public class JpaRepositoryContributor extends RepositoryContributor { } } + // no KeysetScrolling for now. + if (generationContext.getParameterNameOf(ScrollPosition.class) != null + || generationContext.getParameterNameOf(KeysetScrollPosition.class) != null) { + return null; + } + // TODO: Named query via EntityManager, NamedQuery via properties, also for count queries. return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java index c9a0d318f..fb41cabce 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/aot/generated/StringAotQuery.java @@ -34,6 +34,9 @@ abstract class StringAotQuery extends AotQuery { super(parameterBindings); } + /** + * Creates a new {@code StringAotQuery} from a {@link DeclaredQuery}. Parses the query into {@link PreprocessedQuery}. + */ static StringAotQuery of(DeclaredQuery query) { if (query instanceof PreprocessedQuery pq) { @@ -43,21 +46,37 @@ abstract class StringAotQuery extends AotQuery { return new DeclaredAotQuery(PreprocessedQuery.parse(query)); } + /** + * Creates a new {@code StringAotQuery} from a JPQL {@code queryString}. Parses the query into + * {@link PreprocessedQuery}. + */ static StringAotQuery jpqlQuery(String queryString) { return of(DeclaredQuery.jpqlQuery(queryString)); } + /** + * Creates a JPQL {@code StringAotQuery} using the given bindings and limit. + */ public static StringAotQuery jpqlQuery(String queryString, List bindings, Limit resultLimit) { return new LimitedAotQuery(queryString, bindings, resultLimit); } + /** + * Creates a new {@code StringAotQuery} from a native (SQL) {@code queryString}. Parses the query into + * {@link PreprocessedQuery}. + */ static StringAotQuery nativeQuery(String queryString) { return of(DeclaredQuery.nativeQuery(queryString)); } + /** + * @return the underlying declared query. + */ public abstract DeclaredQuery getQuery(); - public abstract String getQueryString(); + public String getQueryString() { + return getQuery().getQueryString(); + } @Override public String toString() { @@ -94,6 +113,8 @@ abstract class StringAotQuery extends AotQuery { } /** + * Query with a limit associated. + * * @author Mark Paluch */ static class LimitedAotQuery extends StringAotQuery { diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java index 8b40751cd..b06b0f971 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinding.java @@ -25,9 +25,9 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import org.springframework.data.expression.ValueExpression; - import org.jspecify.annotations.Nullable; + +import org.springframework.data.expression.ValueExpression; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.support.JpqlQueryTemplates; import org.springframework.data.repository.query.Parameter; @@ -608,7 +608,7 @@ public class ParameterBinding { * @author Mark Paluch * @since 3.1.2 */ - sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic { + public sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic { /** * Creates a {@link Expression} for the given {@code expression}. diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java index bfa807708..c61be724a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/JpaRepositoryContributorIntegrationTests.java @@ -218,6 +218,20 @@ class JpaRepositoryContributorIntegrationTests { "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); } + @Test + void shouldApplyAnnotatedLikeStartsEnds() { + + // start with case + List users = fragment.findAnnotatedLikeStartsEnds("S"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("han@smuggler.net", + "kylo@new-empire.com", "luke@jedi.org", "vader@empire.com"); + + // ends case + users = fragment.findAnnotatedLikeStartsEnds("a"); + assertThat(users).extracting(User::getEmailAddress).containsExactlyInAnyOrder("leia@resistance.gov", + "chewie@smuggler.net", "yoda@jedi.org"); + } + @Test void testAnnotatedMultilineFinderWithQuery() { @@ -306,7 +320,6 @@ class JpaRepositoryContributorIntegrationTests { @Test void testDerivedFinderReturningPageOfProjections() { - // TODO: query.setParameter(1, "%s%%".formatted(lastname)); Page page = fragment.findUserProjectionByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("emailAddress"))); @@ -314,6 +327,9 @@ class JpaRepositoryContributorIntegrationTests { assertThat(page.getSize()).isEqualTo(2); assertThat(page.getContent()).extracting(UserDtoProjection::getEmailAddress).containsExactly("han@smuggler.net", "kylo@new-empire.com"); + + Page noResults = fragment.findUserProjectionByLastnameStartingWith("a", + PageRequest.of(0, 2, Sort.by("emailAddress"))); } // modifying @@ -345,9 +361,10 @@ class JpaRepositoryContributorIntegrationTests { // old stuff below - // TODO: void todo() { + // expressions, templated query with #{#entityName} + // synthetic parameters (keyset scrolling! yuck!) // interface projections // named queries // dynamic projections diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java index d783c9cdf..f766a3f16 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/aot/generated/UserRepository.java @@ -75,6 +75,9 @@ public interface UserRepository extends CrudRepository { @Query("select u from User u where u.lastname like :lastname%") List findAnnotatedQueryByLastnameParameter(String lastname); + @Query("select u from User u where u.lastname like :lastname% or u.lastname like %:lastname") + List findAnnotatedLikeStartsEnds(String lastname); + @Query(""" select u from User u