Skip to content

Commit 29c21c7

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 29c21c7

File tree

6 files changed

+194
-10
lines changed

6 files changed

+194
-10
lines changed

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

Lines changed: 107 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;
@@ -158,10 +169,14 @@ private AotQueries buildNamedQuery(String queryName, JdbcQueryMethod queryMethod
158169
private AotQueries buildPartTreeQuery(RepositoryInformation repositoryInformation, ReturnedType returnedType,
159170
JdbcQueryMethod queryMethod) {
160171

172+
if (queryMethod.getName().equals("findByCreatedBetween")) {
173+
System.out.println();
174+
}
161175
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
162176
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod);
163177

164-
JdbcQueryCreator queryCreator = new JdbcQueryCreator(partTree, converter, dialect, queryMethod, accessor,
178+
JdbcQueryCreator queryCreator = new JdbcQueryCreator(partTree, new AotPassThruJdbcConverter(converter), dialect,
179+
queryMethod, accessor,
165180
returnedType) {
166181

167182
@Override
@@ -226,4 +241,95 @@ private List<ParameterBinding> getBindings(ValueExpressionQueryRewriter.ParsedQu
226241
return bindings;
227242
}
228243

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

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)