Browse Source

Skip conversion of placeholders during AOT processing of derived queries.

We now bypass the converter used for AOT query mapping in derived queries to avoid conversion of placeholders in the AOT query.

Closes #2174
issue/year-as-integer
Mark Paluch 1 month ago
parent
commit
f2541b376f
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 105
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/aot/QueriesFactory.java
  2. 43
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryContributorIntegrationTests.java
  3. 14
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryMetadataIntegrationTests.java
  4. 24
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/User.java
  5. 11
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/UserRepository.java
  6. 4
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository.aot/JdbcRepositoryContributorIntegrationTests-h2.sql

105
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/aot/QueriesFactory.java

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package org.springframework.data.jdbc.repository.aot;
import java.io.IOException;
import java.sql.SQLType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@ -24,13 +25,17 @@ import java.util.Properties; @@ -24,13 +25,17 @@ import java.util.Properties;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.data.core.TypeInformation;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.Identifier;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.core.convert.JdbcTypeFactory;
import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
import org.springframework.data.jdbc.repository.config.JdbcRepositoryConfigExtension;
import org.springframework.data.jdbc.repository.query.JdbcCountQueryCreator;
import org.springframework.data.jdbc.repository.query.JdbcParameters;
@ -39,7 +44,13 @@ import org.springframework.data.jdbc.repository.query.JdbcQueryMethod; @@ -39,7 +44,13 @@ import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
import org.springframework.data.jdbc.repository.query.ParameterBinding;
import org.springframework.data.jdbc.repository.query.ParametrizedQuery;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.data.relational.repository.query.ParameterMetadataProvider;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalParameters;
@ -161,7 +172,8 @@ class QueriesFactory { @@ -161,7 +172,8 @@ class QueriesFactory {
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod);
JdbcQueryCreator queryCreator = new JdbcQueryCreator(partTree, converter, dialect, queryMethod, accessor,
JdbcQueryCreator queryCreator = new JdbcQueryCreator(partTree, new AotPassThruJdbcConverter(converter), dialect,
queryMethod, accessor,
returnedType) {
@Override
@ -226,4 +238,95 @@ class QueriesFactory { @@ -226,4 +238,95 @@ class QueriesFactory {
return bindings;
}
/**
* Pass-thru implementation for {@link JdbcValue} objects to allow capturing parameter placeholders without applying
* conversion.
*
* @param delegate
*/
record AotPassThruJdbcConverter(JdbcConverter delegate) implements JdbcConverter {
@Override
public Class<?> getColumnType(RelationalPersistentProperty property) {
return delegate.getColumnType(property);
}
@Override
public SQLType getTargetSqlType(RelationalPersistentProperty property) {
return delegate.getTargetSqlType(property);
}
@Override
public RelationalMappingContext getMappingContext() {
return delegate.getMappingContext();
}
@Override
public ConversionService getConversionService() {
return delegate.getConversionService();
}
@Override
public EntityInstantiators getEntityInstantiators() {
return delegate.getEntityInstantiators();
}
@Override
public <T> PersistentPropertyPathAccessor<T> getPropertyAccessor(PersistentEntity<T, ?> persistentEntity,
T instance) {
return delegate.getPropertyAccessor(persistentEntity, instance);
}
@Override
public JdbcValue writeJdbcValue(@Nullable Object value, Class<?> type, SQLType sqlType) {
return value instanceof JdbcValue jdbcValue ? jdbcValue : delegate.writeJdbcValue(value, type, sqlType);
}
@Override
public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation<?> type, SQLType sqlType) {
return value instanceof JdbcValue jdbcValue ? jdbcValue : delegate.writeJdbcValue(value, type, sqlType);
}
@Override
public @Nullable Object writeValue(@Nullable Object value, TypeInformation<?> type) {
return value;
}
@Override
public <R> R readAndResolve(Class<R> type, RowDocument source) {
throw new UnsupportedOperationException();
}
@Override
public <R> R readAndResolve(Class<R> type, RowDocument source, Identifier identifier) {
throw new UnsupportedOperationException();
}
@Override
public <R> R readAndResolve(TypeInformation<R> type, RowDocument source, Identifier identifier) {
throw new UnsupportedOperationException();
}
@Override
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> entityType) {
throw new UnsupportedOperationException();
}
@Override
public <R> R project(EntityProjection<R, ?> descriptor, RowDocument document) {
throw new UnsupportedOperationException();
}
@Override
public <R> R read(Class<R> type, RowDocument source) {
throw new UnsupportedOperationException();
}
@Override
public @Nullable Object readValue(@Nullable Object value, TypeInformation<?> type) {
throw new UnsupportedOperationException();
}
}
}

43
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryContributorIntegrationTests.java

@ -17,6 +17,7 @@ package org.springframework.data.jdbc.repository.aot; @@ -17,6 +17,7 @@ package org.springframework.data.jdbc.repository.aot;
import static org.assertj.core.api.Assertions.*;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
@ -38,6 +39,7 @@ import org.springframework.data.domain.Sort; @@ -38,6 +39,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
import org.springframework.data.jdbc.core.convert.QueryMappingConfiguration;
import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.repository.support.BeanFactoryAwareRowMapperFactory;
import org.springframework.data.jdbc.testing.DatabaseType;
@ -102,8 +104,12 @@ class JdbcRepositoryContributorIntegrationTests { @@ -102,8 +104,12 @@ class JdbcRepositoryContributorIntegrationTests {
operations.insert(new User("Walter", 52));
operations.insert(new User("Skyler", 40));
operations.insert(new User("Flynn", 16));
operations.insert(new User("Mike", 62));
operations.insert(new User("Gustavo", 51));
User mike = operations.insert(new User("Mike", 62));
User gus = operations.insert(new User("Gustavo", 51));
mike.setFriend(AggregateReference.to(gus.getId()));
operations.save(mike);
operations.insert(new User("Hector", 83));
}
@ -158,6 +164,22 @@ class JdbcRepositoryContributorIntegrationTests { @@ -158,6 +164,22 @@ class JdbcRepositoryContributorIntegrationTests {
assertThat(walter).isNull(); // % is escaped
}
@Test // GH-2174
void shouldSupportDerivedQueryWithConverter() {
List<User> users = fragment.findByCreatedBefore(Instant.now().plusSeconds(180));
assertThat(users).hasSize(6);
}
@Test // GH-2174
void shouldSupportDerivedQueryBetweenWithConverter() {
List<User> users = fragment.findByCreatedBetween(Instant.now().minusSeconds(180), Instant.now().plusSeconds(180));
assertThat(users).hasSize(6);
}
@Test // GH-2121
void shouldFindBetween() {
@ -283,6 +305,23 @@ class JdbcRepositoryContributorIntegrationTests { @@ -283,6 +305,23 @@ class JdbcRepositoryContributorIntegrationTests {
assertThat(result).isOne();
}
@Test // GH-2174
void shouldSupportDeclaredQueryWithConverter() {
List<User> users = fragment.findCreatedBefore(Instant.now().plusSeconds(180));
assertThat(users).hasSize(6);
}
@Test // GH-2174
void shouldSupportDeclaredQueryWithAggregateReference() {
User gus = fragment.findByFirstname("Gustavo");
List<User> users = fragment.findByFriend(AggregateReference.to(gus.getId()));
assertThat(users).hasSize(1);
}
@Test // GH-2121
void shouldProjectOneToDto() {

14
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryMetadataIntegrationTests.java

@ -123,8 +123,8 @@ class JdbcRepositoryMetadataIntegrationTests { @@ -123,8 +123,8 @@ class JdbcRepositoryMetadataIntegrationTests {
String json = resource.getContentAsString(StandardCharsets.UTF_8);
assertThatJson(json).inPath("$.methods[?(@.name == 'findByFirstname')].query").isArray().first().isObject()
.containsEntry("query",
"SELECT \"MY_USER\".\"ID\" AS \"ID\", \"MY_USER\".\"AGE\" AS \"AGE\", \"MY_USER\".\"FIRSTNAME\" AS \"FIRSTNAME\" FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" = :firstname");
.hasEntrySatisfying("query", value -> assertThat(value).asString().contains("SELECT \"MY_USER\".\"ID\"",
"FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" = :firstname"));
}
@Test // GH-2121
@ -139,8 +139,9 @@ class JdbcRepositoryMetadataIntegrationTests { @@ -139,8 +139,9 @@ class JdbcRepositoryMetadataIntegrationTests {
assertThatJson(json)
.inPath("$.methods[?(@.name == 'findWithParameterNameByFirstnameStartingWithOrFirstnameEndingWith')].query")
.isArray().first().isObject().containsEntry("query",
"SELECT \"MY_USER\".\"ID\" AS \"ID\", \"MY_USER\".\"AGE\" AS \"AGE\", \"MY_USER\".\"FIRSTNAME\" AS \"FIRSTNAME\" FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" LIKE :firstname OR (\"MY_USER\".\"FIRSTNAME\" LIKE :firstname1)");
.isArray().first().isObject()
.hasEntrySatisfying("query", value -> assertThat(value).asString().contains("SELECT \"MY_USER\".\"ID\"",
"FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" LIKE :firstname OR (\"MY_USER\".\"FIRSTNAME\" LIKE :firstname1)"));
}
@Test // GH-2121
@ -155,8 +156,9 @@ class JdbcRepositoryMetadataIntegrationTests { @@ -155,8 +156,9 @@ class JdbcRepositoryMetadataIntegrationTests {
assertThatJson(json).inPath("$.methods[?(@.name == 'findPageByAgeGreaterThan')].query").isArray().element(0)
.isObject()
.containsEntry("query",
"SELECT \"MY_USER\".\"ID\" AS \"ID\", \"MY_USER\".\"AGE\" AS \"AGE\", \"MY_USER\".\"FIRSTNAME\" AS \"FIRSTNAME\" FROM \"MY_USER\" WHERE \"MY_USER\".\"AGE\" > :age")
.hasEntrySatisfying("query",
value -> assertThat(value).asString().contains("SELECT \"MY_USER\".\"ID\"",
"FROM \"MY_USER\" WHERE \"MY_USER\".\"AGE\" > :age"))
.containsEntry("count-query", "SELECT COUNT(*) FROM \"MY_USER\" WHERE \"MY_USER\".\"AGE\" > :age");
}

24
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/User.java

@ -15,7 +15,12 @@ @@ -15,7 +15,12 @@
*/
package org.springframework.data.jdbc.repository.aot;
import java.time.Instant;
import org.jspecify.annotations.Nullable;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Table;
/**
@ -27,6 +32,8 @@ public class User { @@ -27,6 +32,8 @@ public class User {
private @Id long id;
private String firstname;
private int age;
private Instant created = Instant.now();
private @Nullable AggregateReference<User, Long> friend;
public User(String firstname, int age) {
this.firstname = firstname;
@ -56,4 +63,21 @@ public class User { @@ -56,4 +63,21 @@ public class User {
public void setAge(int age) {
this.age = age;
}
public Instant getCreated() {
return created;
}
public void setCreated(Instant created) {
this.created = created;
}
public @Nullable AggregateReference<User, Long> getFriend() {
return friend;
}
public void setFriend(AggregateReference<User, Long> friend) {
this.friend = friend;
}
}

11
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/UserRepository.java

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
*/
package org.springframework.data.jdbc.repository.aot;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@ -22,6 +23,7 @@ import java.util.stream.Stream; @@ -22,6 +23,7 @@ import java.util.stream.Stream;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
@ -41,6 +43,12 @@ public interface UserRepository extends CrudRepository<User, Integer> { @@ -41,6 +43,12 @@ public interface UserRepository extends CrudRepository<User, Integer> {
User findByFirstnameEndingWith(String name);
List<User> findByCreatedBefore(Instant instant);
List<User> findByCreatedBetween(Instant from, Instant to);
List<User> findByFriend(AggregateReference<User, Long> friend);
List<User> findAllByAgeBetween(int start, int end);
Optional<User> findOptionalByFirstname(String name);
@ -86,6 +94,9 @@ public interface UserRepository extends CrudRepository<User, Integer> { @@ -86,6 +94,9 @@ public interface UserRepository extends CrudRepository<User, Integer> {
@Query(value = "SELECT * FROM MY_USER WHERE firstname = :name", resultSetExtractorRef = "simpleResultSetExtractor")
int findUsingAndResultSetExtractorRef(String name);
@Query(value = "SELECT * FROM MY_USER WHERE created < :instant")
List<User> findCreatedBefore(Instant instant);
// -------------------------------------------------------------------------
// Parameter naming
// -------------------------------------------------------------------------

4
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository.aot/JdbcRepositoryContributorIntegrationTests-h2.sql

@ -2,5 +2,7 @@ CREATE TABLE MY_USER @@ -2,5 +2,7 @@ CREATE TABLE MY_USER
(
id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ),
firstname VARCHAR(255),
age INT
age INT,
created TIMESTAMP,
friend BIGINT NULL
);

Loading…
Cancel
Save