Skip to content

Commit f2541b3

Browse files
committed
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
1 parent 547c756 commit f2541b3

File tree

6 files changed

+191
-10
lines changed

6 files changed

+191
-10
lines changed

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

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.jdbc.repository.aot;
1717

1818
import java.io.IOException;
19+
import java.sql.SQLType;
1920
import java.util.ArrayList;
2021
import java.util.List;
2122
import java.util.Objects;
@@ -24,13 +25,17 @@
2425
import org.jspecify.annotations.Nullable;
2526

2627
import org.springframework.core.annotation.MergedAnnotation;
28+
import org.springframework.core.convert.ConversionService;
2729
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
30+
import org.springframework.data.core.TypeInformation;
2831
import org.springframework.data.domain.Sort;
32+
import org.springframework.data.jdbc.core.convert.Identifier;
2933
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3034
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
3135
import org.springframework.data.jdbc.core.convert.JdbcTypeFactory;
3236
import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
3337
import org.springframework.data.jdbc.core.dialect.JdbcDialect;
38+
import org.springframework.data.jdbc.core.mapping.JdbcValue;
3439
import org.springframework.data.jdbc.repository.config.JdbcRepositoryConfigExtension;
3540
import org.springframework.data.jdbc.repository.query.JdbcCountQueryCreator;
3641
import org.springframework.data.jdbc.repository.query.JdbcParameters;
@@ -39,7 +44,13 @@
3944
import org.springframework.data.jdbc.repository.query.ParameterBinding;
4045
import org.springframework.data.jdbc.repository.query.ParametrizedQuery;
4146
import org.springframework.data.jdbc.repository.query.Query;
47+
import org.springframework.data.mapping.PersistentEntity;
48+
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
49+
import org.springframework.data.mapping.model.EntityInstantiators;
50+
import org.springframework.data.projection.EntityProjection;
4251
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
52+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
53+
import org.springframework.data.relational.domain.RowDocument;
4354
import org.springframework.data.relational.repository.query.ParameterMetadataProvider;
4455
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
4556
import org.springframework.data.relational.repository.query.RelationalParameters;
@@ -161,7 +172,8 @@ private AotQueries buildPartTreeQuery(RepositoryInformation repositoryInformatio
161172
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
162173
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod);
163174

164-
JdbcQueryCreator queryCreator = new JdbcQueryCreator(partTree, converter, dialect, queryMethod, accessor,
175+
JdbcQueryCreator queryCreator = new JdbcQueryCreator(partTree, new AotPassThruJdbcConverter(converter), dialect,
176+
queryMethod, accessor,
165177
returnedType) {
166178

167179
@Override
@@ -226,4 +238,95 @@ private List<ParameterBinding> getBindings(ValueExpressionQueryRewriter.ParsedQu
226238
return bindings;
227239
}
228240

241+
/**
242+
* Pass-thru implementation for {@link JdbcValue} objects to allow capturing parameter placeholders without applying
243+
* conversion.
244+
*
245+
* @param delegate
246+
*/
247+
record AotPassThruJdbcConverter(JdbcConverter delegate) implements JdbcConverter {
248+
249+
@Override
250+
public Class<?> getColumnType(RelationalPersistentProperty property) {
251+
return delegate.getColumnType(property);
252+
}
253+
254+
@Override
255+
public SQLType getTargetSqlType(RelationalPersistentProperty property) {
256+
return delegate.getTargetSqlType(property);
257+
}
258+
259+
@Override
260+
public RelationalMappingContext getMappingContext() {
261+
return delegate.getMappingContext();
262+
}
263+
264+
@Override
265+
public ConversionService getConversionService() {
266+
return delegate.getConversionService();
267+
}
268+
269+
@Override
270+
public EntityInstantiators getEntityInstantiators() {
271+
return delegate.getEntityInstantiators();
272+
}
273+
274+
@Override
275+
public <T> PersistentPropertyPathAccessor<T> getPropertyAccessor(PersistentEntity<T, ?> persistentEntity,
276+
T instance) {
277+
return delegate.getPropertyAccessor(persistentEntity, instance);
278+
}
279+
280+
@Override
281+
public JdbcValue writeJdbcValue(@Nullable Object value, Class<?> type, SQLType sqlType) {
282+
return value instanceof JdbcValue jdbcValue ? jdbcValue : delegate.writeJdbcValue(value, type, sqlType);
283+
}
284+
285+
@Override
286+
public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation<?> type, SQLType sqlType) {
287+
return value instanceof JdbcValue jdbcValue ? jdbcValue : delegate.writeJdbcValue(value, type, sqlType);
288+
}
289+
290+
@Override
291+
public @Nullable Object writeValue(@Nullable Object value, TypeInformation<?> type) {
292+
return value;
293+
}
294+
295+
@Override
296+
public <R> R readAndResolve(Class<R> type, RowDocument source) {
297+
throw new UnsupportedOperationException();
298+
}
299+
300+
@Override
301+
public <R> R readAndResolve(Class<R> type, RowDocument source, Identifier identifier) {
302+
throw new UnsupportedOperationException();
303+
}
304+
305+
@Override
306+
public <R> R readAndResolve(TypeInformation<R> type, RowDocument source, Identifier identifier) {
307+
throw new UnsupportedOperationException();
308+
}
309+
310+
@Override
311+
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> entityType) {
312+
throw new UnsupportedOperationException();
313+
}
314+
315+
@Override
316+
public <R> R project(EntityProjection<R, ?> descriptor, RowDocument document) {
317+
throw new UnsupportedOperationException();
318+
}
319+
320+
@Override
321+
public <R> R read(Class<R> type, RowDocument source) {
322+
throw new UnsupportedOperationException();
323+
}
324+
325+
@Override
326+
public @Nullable Object readValue(@Nullable Object value, TypeInformation<?> type) {
327+
throw new UnsupportedOperationException();
328+
}
329+
330+
}
331+
229332
}

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import java.time.Instant;
2021
import java.util.List;
2122
import java.util.Optional;
2223

@@ -38,6 +39,7 @@
3839
import org.springframework.data.jdbc.core.JdbcAggregateOperations;
3940
import org.springframework.data.jdbc.core.convert.QueryMappingConfiguration;
4041
import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect;
42+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
4143
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
4244
import org.springframework.data.jdbc.repository.support.BeanFactoryAwareRowMapperFactory;
4345
import org.springframework.data.jdbc.testing.DatabaseType;
@@ -102,8 +104,12 @@ void beforeEach() {
102104
operations.insert(new User("Walter", 52));
103105
operations.insert(new User("Skyler", 40));
104106
operations.insert(new User("Flynn", 16));
105-
operations.insert(new User("Mike", 62));
106-
operations.insert(new User("Gustavo", 51));
107+
User mike = operations.insert(new User("Mike", 62));
108+
User gus = operations.insert(new User("Gustavo", 51));
109+
110+
mike.setFriend(AggregateReference.to(gus.getId()));
111+
operations.save(mike);
112+
107113
operations.insert(new User("Hector", 83));
108114
}
109115

@@ -158,6 +164,22 @@ void shouldFindByFirstnameEndingWith() {
158164
assertThat(walter).isNull(); // % is escaped
159165
}
160166

167+
@Test // GH-2174
168+
void shouldSupportDerivedQueryWithConverter() {
169+
170+
List<User> users = fragment.findByCreatedBefore(Instant.now().plusSeconds(180));
171+
172+
assertThat(users).hasSize(6);
173+
}
174+
175+
@Test // GH-2174
176+
void shouldSupportDerivedQueryBetweenWithConverter() {
177+
178+
List<User> users = fragment.findByCreatedBetween(Instant.now().minusSeconds(180), Instant.now().plusSeconds(180));
179+
180+
assertThat(users).hasSize(6);
181+
}
182+
161183
@Test // GH-2121
162184
void shouldFindBetween() {
163185

@@ -283,6 +305,23 @@ void shouldFindUsingResultSetExtractorRef() {
283305
assertThat(result).isOne();
284306
}
285307

308+
@Test // GH-2174
309+
void shouldSupportDeclaredQueryWithConverter() {
310+
311+
List<User> users = fragment.findCreatedBefore(Instant.now().plusSeconds(180));
312+
313+
assertThat(users).hasSize(6);
314+
}
315+
316+
@Test // GH-2174
317+
void shouldSupportDeclaredQueryWithAggregateReference() {
318+
319+
User gus = fragment.findByFirstname("Gustavo");
320+
List<User> users = fragment.findByFriend(AggregateReference.to(gus.getId()));
321+
322+
assertThat(users).hasSize(1);
323+
}
324+
286325
@Test // GH-2121
287326
void shouldProjectOneToDto() {
288327

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ void shouldDocumentDerivedQuery() throws IOException {
123123
String json = resource.getContentAsString(StandardCharsets.UTF_8);
124124

125125
assertThatJson(json).inPath("$.methods[?(@.name == 'findByFirstname')].query").isArray().first().isObject()
126-
.containsEntry("query",
127-
"SELECT \"MY_USER\".\"ID\" AS \"ID\", \"MY_USER\".\"AGE\" AS \"AGE\", \"MY_USER\".\"FIRSTNAME\" AS \"FIRSTNAME\" FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" = :firstname");
126+
.hasEntrySatisfying("query", value -> assertThat(value).asString().contains("SELECT \"MY_USER\".\"ID\"",
127+
"FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" = :firstname"));
128128
}
129129

130130
@Test // GH-2121
@@ -139,8 +139,9 @@ void shouldDocumentDerivedQueryWithParam() throws IOException {
139139

140140
assertThatJson(json)
141141
.inPath("$.methods[?(@.name == 'findWithParameterNameByFirstnameStartingWithOrFirstnameEndingWith')].query")
142-
.isArray().first().isObject().containsEntry("query",
143-
"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)");
142+
.isArray().first().isObject()
143+
.hasEntrySatisfying("query", value -> assertThat(value).asString().contains("SELECT \"MY_USER\".\"ID\"",
144+
"FROM \"MY_USER\" WHERE \"MY_USER\".\"FIRSTNAME\" LIKE :firstname OR (\"MY_USER\".\"FIRSTNAME\" LIKE :firstname1)"));
144145
}
145146

146147
@Test // GH-2121
@@ -155,8 +156,9 @@ void shouldDocumentPagedQuery() throws IOException {
155156

156157
assertThatJson(json).inPath("$.methods[?(@.name == 'findPageByAgeGreaterThan')].query").isArray().element(0)
157158
.isObject()
158-
.containsEntry("query",
159-
"SELECT \"MY_USER\".\"ID\" AS \"ID\", \"MY_USER\".\"AGE\" AS \"AGE\", \"MY_USER\".\"FIRSTNAME\" AS \"FIRSTNAME\" FROM \"MY_USER\" WHERE \"MY_USER\".\"AGE\" > :age")
159+
.hasEntrySatisfying("query",
160+
value -> assertThat(value).asString().contains("SELECT \"MY_USER\".\"ID\"",
161+
"FROM \"MY_USER\" WHERE \"MY_USER\".\"AGE\" > :age"))
160162
.containsEntry("count-query", "SELECT COUNT(*) FROM \"MY_USER\" WHERE \"MY_USER\".\"AGE\" > :age");
161163
}
162164

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.aot;
1717

18+
import java.time.Instant;
19+
20+
import org.jspecify.annotations.Nullable;
21+
1822
import org.springframework.data.annotation.Id;
23+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
1924
import org.springframework.data.relational.core.mapping.Table;
2025

2126
/**
@@ -27,6 +32,8 @@ public class User {
2732
private @Id long id;
2833
private String firstname;
2934
private int age;
35+
private Instant created = Instant.now();
36+
private @Nullable AggregateReference<User, Long> friend;
3037

3138
public User(String firstname, int age) {
3239
this.firstname = firstname;
@@ -56,4 +63,21 @@ public int getAge() {
5663
public void setAge(int age) {
5764
this.age = age;
5865
}
66+
67+
public Instant getCreated() {
68+
return created;
69+
}
70+
71+
public void setCreated(Instant created) {
72+
this.created = created;
73+
}
74+
75+
public @Nullable AggregateReference<User, Long> getFriend() {
76+
return friend;
77+
}
78+
79+
public void setFriend(AggregateReference<User, Long> friend) {
80+
this.friend = friend;
81+
}
82+
5983
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.aot;
1717

18+
import java.time.Instant;
1819
import java.util.List;
1920
import java.util.Optional;
2021
import java.util.stream.Stream;
2122

2223
import org.springframework.data.domain.Page;
2324
import org.springframework.data.domain.Pageable;
2425
import org.springframework.data.domain.Slice;
26+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
2527
import org.springframework.data.jdbc.repository.query.Modifying;
2628
import org.springframework.data.jdbc.repository.query.Query;
2729
import org.springframework.data.repository.CrudRepository;
@@ -41,6 +43,12 @@ public interface UserRepository extends CrudRepository<User, Integer> {
4143

4244
User findByFirstnameEndingWith(String name);
4345

46+
List<User> findByCreatedBefore(Instant instant);
47+
48+
List<User> findByCreatedBetween(Instant from, Instant to);
49+
50+
List<User> findByFriend(AggregateReference<User, Long> friend);
51+
4452
List<User> findAllByAgeBetween(int start, int end);
4553

4654
Optional<User> findOptionalByFirstname(String name);
@@ -86,6 +94,9 @@ public interface UserRepository extends CrudRepository<User, Integer> {
8694
@Query(value = "SELECT * FROM MY_USER WHERE firstname = :name", resultSetExtractorRef = "simpleResultSetExtractor")
8795
int findUsingAndResultSetExtractorRef(String name);
8896

97+
@Query(value = "SELECT * FROM MY_USER WHERE created < :instant")
98+
List<User> findCreatedBefore(Instant instant);
99+
89100
// -------------------------------------------------------------------------
90101
// Parameter naming
91102
// -------------------------------------------------------------------------

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ CREATE TABLE MY_USER
22
(
33
id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ),
44
firstname VARCHAR(255),
5-
age INT
5+
age INT,
6+
created TIMESTAMP,
7+
friend BIGINT NULL
68
);

0 commit comments

Comments
 (0)