Polishing.

Fix Like with starts/ends, use proper parameter origins instead of assuming binding name matches parameter names. Simplify binding block.

See #3830
This commit is contained in:
Mark Paluch
2025-03-26 09:13:38 +01:00
parent 802a8db1d1
commit daae010e21
7 changed files with 88 additions and 57 deletions

View File

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

View File

@@ -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> queryHints) {
Builder hintsBuilder = CodeBlock.builder();

View File

@@ -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) -> {

View File

@@ -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<ParameterBinding> 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 {

View File

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

View File

@@ -218,6 +218,20 @@ class JpaRepositoryContributorIntegrationTests {
"kylo@new-empire.com", "luke@jedi.org", "vader@empire.com");
}
@Test
void shouldApplyAnnotatedLikeStartsEnds() {
// start with case
List<User> 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<UserDtoProjection> 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<UserDtoProjection> 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

View File

@@ -75,6 +75,9 @@ public interface UserRepository extends CrudRepository<User, Integer> {
@Query("select u from User u where u.lastname like :lastname%")
List<User> findAnnotatedQueryByLastnameParameter(String lastname);
@Query("select u from User u where u.lastname like :lastname% or u.lastname like %:lastname")
List<User> findAnnotatedLikeStartsEnds(String lastname);
@Query("""
select u
from User u