Skip to content

Commit 65c3f3b

Browse files
committed
Add Ahead of Time Repository support.
We now provide AOT support to generate repository implementations during build-time for JDBC repository query methods. Supported Features Derived query methods, @query and named query methods * @Modifying methods returning void, int, and long * Pagination, Slice, Stream, and Optional return types * DTO and Interface Projections * Value Expressions Excluded methods * CrudRepository, Querydsl, Query by Example, and other base interface methods as their implementation is provided by the base class respective fragments * Methods whose implementation would be overly complex * Methods accepting ScrollPosition (e.g. Keyset pagination)
1 parent 0b2bcfb commit 65c3f3b

File tree

58 files changed

+5058
-518
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+5058
-518
lines changed

spring-data-jdbc/pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@
167167

168168
<!-- Test dependencies -->
169169

170+
<dependency>
171+
<groupId>org.springframework</groupId>
172+
<artifactId>spring-test</artifactId>
173+
<scope>test</scope>
174+
</dependency>
175+
176+
<dependency>
177+
<groupId>org.springframework</groupId>
178+
<artifactId>spring-core-test</artifactId>
179+
<scope>test</scope>
180+
</dependency>
181+
170182
<dependency>
171183
<groupId>org.awaitility</groupId>
172184
<artifactId>awaitility</artifactId>
@@ -194,6 +206,19 @@
194206
<scope>test</scope>
195207
</dependency>
196208

209+
<dependency>
210+
<groupId>net.javacrumbs.json-unit</groupId>
211+
<artifactId>json-unit-assertj</artifactId>
212+
<version>4.1.0</version>
213+
<scope>test</scope>
214+
</dependency>
215+
216+
<dependency>
217+
<groupId>com.fasterxml.jackson.core</groupId>
218+
<artifactId>jackson-databind</artifactId>
219+
<scope>test</scope>
220+
</dependency>
221+
197222
<dependency>
198223
<groupId>com.tngtech.archunit</groupId>
199224
<artifactId>archunit</artifactId>

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
2929
import org.springframework.data.jdbc.core.convert.JdbcConverter;
3030
import org.springframework.data.relational.core.query.Query;
31+
import org.springframework.jdbc.core.RowMapper;
3132

3233
/**
3334
* Specifies operations one can perform on a database, based on an <em>Domain Type</em>.
@@ -342,4 +343,16 @@ public interface JdbcAggregateOperations {
342343
*/
343344
DataAccessStrategy getDataAccessStrategy();
344345

346+
/**
347+
* Return a {@link RowMapper} that can map rows of a {@link java.sql.ResultSet} to instances of the specified
348+
* {@link Class type}. The row mapper supports entity callbacks and lifecycle events if enabled and configured on the
349+
* underlying template instance.
350+
*
351+
* @param type type of the entity to map.
352+
* @return a row mapper for the given type.
353+
* @param <T>
354+
* @since 4.0
355+
*/
356+
<T> RowMapper<? extends T> getRowMapper(Class<T> type);
357+
345358
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.jdbc.core;
1717

18+
import java.sql.ResultSet;
19+
import java.sql.SQLException;
1820
import java.util.ArrayList;
1921
import java.util.Collections;
2022
import java.util.HashMap;
@@ -37,7 +39,9 @@
3739
import org.springframework.data.domain.Pageable;
3840
import org.springframework.data.domain.Sort;
3941
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
42+
import org.springframework.data.jdbc.core.convert.EntityRowMapper;
4043
import org.springframework.data.jdbc.core.convert.JdbcConverter;
44+
import org.springframework.data.jdbc.core.convert.QueryMappingConfiguration;
4145
import org.springframework.data.mapping.IdentifierAccessor;
4246
import org.springframework.data.mapping.callback.EntityCallbacks;
4347
import org.springframework.data.relational.core.EntityLifecycleEventDelegate;
@@ -56,6 +60,7 @@
5660
import org.springframework.data.relational.core.mapping.event.*;
5761
import org.springframework.data.relational.core.query.Query;
5862
import org.springframework.data.support.PageableExecutionUtils;
63+
import org.springframework.jdbc.core.RowMapper;
5964
import org.springframework.util.Assert;
6065
import org.springframework.util.ClassUtils;
6166

@@ -83,6 +88,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations, Applicati
8388
private final JdbcConverter converter;
8489

8590
private @Nullable EntityCallbacks entityCallbacks;
91+
private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY;
8692

8793
/**
8894
* Creates a new {@link JdbcAggregateTemplate} given {@link RelationalMappingContext} and {@link DataAccessStrategy}.
@@ -186,6 +192,23 @@ public void setEntityLifecycleEventsEnabled(boolean enabled) {
186192
this.eventDelegate.setEventsEnabled(enabled);
187193
}
188194

195+
/**
196+
* Return a {@link RowMapper} to map results for {@link Class type}.
197+
*
198+
* @param type must not be {@literal null}.
199+
* @return a row mapper to map results for {@link Class type}.
200+
* @param <T> return type.
201+
* @since 4.0
202+
*/
203+
@Override
204+
@SuppressWarnings("unchecked")
205+
public <T> RowMapper<? extends T> getRowMapper(Class<T> type) {
206+
207+
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(type);
208+
209+
return new LifecycleEntityRowMapper<T>((RelationalPersistentEntity<T>) entity);
210+
}
211+
189212
@Override
190213
public <T> T save(T instance) {
191214

@@ -741,4 +764,29 @@ private interface AggregateChangeCreator<T> {
741764
RootAggregateChange<T> createAggregateChange(T instance);
742765
}
743766

767+
class LifecycleEntityRowMapper<T> extends EntityRowMapper<T> {
768+
769+
public LifecycleEntityRowMapper(RelationalPersistentEntity<T> entity) {
770+
super(entity, converter);
771+
}
772+
773+
@Override
774+
public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException {
775+
776+
T object = super.mapRow(resultSet, rowNumber);
777+
778+
if (object != null) {
779+
780+
eventDelegate.publishEvent(() -> new AfterConvertEvent<>(object));
781+
782+
if (entityCallbacks != null) {
783+
return entityCallbacks.callback(AfterConvertCallback.class, object);
784+
}
785+
}
786+
787+
return object;
788+
}
789+
790+
}
791+
744792
}

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,6 @@ public class EntityRowMapper<T> implements RowMapper<T> {
4141
private final JdbcConverter converter;
4242
private final Identifier identifier;
4343

44-
private EntityRowMapper(TypeInformation<T> typeInformation, JdbcConverter converter, Identifier identifier) {
45-
46-
this.typeInformation = typeInformation;
47-
this.converter = converter;
48-
this.identifier = identifier;
49-
}
50-
5144
@SuppressWarnings("unchecked")
5245
public EntityRowMapper(AggregatePath path, JdbcConverter converter, Identifier identifier) {
5346
this(((RelationalPersistentEntity<T>) path.getRequiredLeafEntity()).getTypeInformation(), converter, identifier);
@@ -57,6 +50,13 @@ public EntityRowMapper(RelationalPersistentEntity<T> entity, JdbcConverter conve
5750
this(entity.getTypeInformation(), converter, Identifier.empty());
5851
}
5952

53+
private EntityRowMapper(TypeInformation<T> typeInformation, JdbcConverter converter, Identifier identifier) {
54+
55+
this.typeInformation = typeInformation;
56+
this.converter = converter;
57+
this.identifier = identifier;
58+
}
59+
6060
@Override
6161
public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException {
6262

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMappingConfiguration.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
*/
2929
public interface QueryMappingConfiguration {
3030

31-
@Nullable
32-
default <T extends @Nullable Object> RowMapper<? extends T> getRowMapper(Class<T> type) {
33-
return null;
34-
}
31+
/**
32+
* Returns the {@link RowMapper} to be used for the given type or {@literal null} if no specific mapper is configured.
33+
*
34+
* @param type
35+
* @return
36+
* @param <T extends @Nullable Object>
37+
*/
38+
<T> @Nullable RowMapper<? extends T> getRowMapper(Class<T> type);
3539

3640
/**
3741
* An immutable empty instance that will return {@literal null} for all arguments.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository.aot;
17+
18+
import java.util.LinkedHashMap;
19+
import java.util.Map;
20+
21+
import org.jspecify.annotations.Nullable;
22+
23+
import org.springframework.data.repository.aot.generate.QueryMetadata;
24+
25+
/**
26+
* Value object capturing queries used for repository query methods.
27+
*
28+
* @author Mark Paluch
29+
* @since 4.0
30+
*/
31+
record AotQueries(AotQuery result, @Nullable AotQuery count) {
32+
33+
/**
34+
* Factory method to create an {@link AotQueries} instance with a single query.
35+
*
36+
* @param query
37+
* @return
38+
*/
39+
public static AotQueries create(AotQuery query) {
40+
return new AotQueries(query, null);
41+
}
42+
43+
/**
44+
* Factory method to create an {@link AotQueries} instance with an entity- and a count query.
45+
*
46+
* @param query
47+
* @param count
48+
* @return
49+
*/
50+
public static AotQueries create(AotQuery query, AotQuery count) {
51+
return new AotQueries(query, count);
52+
}
53+
54+
public QueryMetadata toMetadata() {
55+
return new AotQueryMetadata();
56+
}
57+
58+
/**
59+
* String and Named Query-based {@link QueryMetadata}.
60+
*/
61+
private class AotQueryMetadata implements QueryMetadata {
62+
63+
@Override
64+
public Map<String, Object> serialize() {
65+
66+
Map<String, Object> serialized = new LinkedHashMap<>();
67+
68+
if (result() instanceof StringAotQuery sq) {
69+
serialized.put("query", sq.getQueryString());
70+
}
71+
72+
if (result() instanceof StringAotQuery.NamedStringAotQuery nsq) {
73+
serialized.put("name", nsq.getQueryName());
74+
}
75+
76+
if (count() instanceof StringAotQuery sq) {
77+
serialized.put("count-query", sq.getQueryString());
78+
}
79+
80+
if (count() instanceof StringAotQuery.NamedStringAotQuery nsq) {
81+
serialized.put("count-name", nsq.getQueryName());
82+
}
83+
84+
return serialized;
85+
}
86+
87+
}
88+
89+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository.aot;
17+
18+
import java.util.List;
19+
20+
import org.springframework.data.domain.Limit;
21+
import org.springframework.data.jdbc.repository.query.ParameterBinding;
22+
23+
/**
24+
* AOT query value object along with its parameter bindings.
25+
*
26+
* @author Mark Paluch
27+
* @since 4.0
28+
*/
29+
abstract class AotQuery {
30+
31+
private final List<ParameterBinding> parameterBindings;
32+
33+
AotQuery(List<ParameterBinding> parameterBindings) {
34+
this.parameterBindings = parameterBindings;
35+
}
36+
37+
/**
38+
* @return the list of parameter bindings.
39+
*/
40+
public List<ParameterBinding> getParameterBindings() {
41+
return parameterBindings;
42+
}
43+
44+
/**
45+
* @return the preliminary query limit.
46+
*/
47+
public Limit getLimit() {
48+
return Limit.unlimited();
49+
}
50+
51+
/**
52+
* @return whether the query is limited (e.g. {@code findTop10By}).
53+
*/
54+
public boolean isLimited() {
55+
return getLimit().isLimited();
56+
}
57+
58+
/**
59+
* @return whether the query a delete query.
60+
*/
61+
public boolean isDelete() {
62+
return false;
63+
}
64+
65+
/**
66+
* @return whether the query is a count query.
67+
*/
68+
public boolean isCount() {
69+
return false;
70+
}
71+
72+
/**
73+
* @return whether the query is an exists query.
74+
*/
75+
public boolean isExists() {
76+
return false;
77+
}
78+
79+
/**
80+
* @return {@literal true} if the query uses value expressions.
81+
*/
82+
public boolean hasExpression() {
83+
84+
for (ParameterBinding parameterBinding : parameterBindings) {
85+
if (parameterBinding.getOrigin().isExpression()) {
86+
return true;
87+
}
88+
}
89+
90+
return false;
91+
}
92+
93+
}

0 commit comments

Comments
 (0)