Skip to content

Commit f937738

Browse files
schaudermp911de
authored andcommitted
Support for table names in SpEL expressions.
SpEL expressions in queries get processed in two steps: 1. First SpEL expressions outside parameters are detected and processed. This is done with a `StandardEvaluationContext` with the variables `tableName` and `qualifiedTableName` added. This step is introduced by this commit. 2. Parameters made up by SpEL expressions are processed as usual. Closes #1856 Original pull request #1863
1 parent 4221840 commit f937738

File tree

16 files changed

+574
-24
lines changed

16 files changed

+574
-24
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ String getDeclaredQuery() {
128128
return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery();
129129
}
130130

131-
String getRequiredQuery() {
131+
public String getRequiredQuery() {
132132

133133
String query = getDeclaredQuery();
134134

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java

+27-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.data.jdbc.core.mapping.JdbcValue;
3636
import org.springframework.data.jdbc.support.JdbcUtil;
3737
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
38+
import org.springframework.data.relational.repository.query.QueryPreprocessor;
3839
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
3940
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
4041
import org.springframework.data.repository.query.Parameter;
@@ -103,11 +104,33 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
103104
* @param queryMethod must not be {@literal null}.
104105
* @param operations must not be {@literal null}.
105106
* @param rowMapperFactory must not be {@literal null}.
107+
* @param converter must not be {@literal null}.
108+
* @param evaluationContextProvider must not be {@literal null}.
106109
* @since 2.3
110+
* @deprecated use alternative constructor
107111
*/
112+
@Deprecated(since = "3.4")
108113
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
109114
RowMapperFactory rowMapperFactory, JdbcConverter converter,
110115
QueryMethodEvaluationContextProvider evaluationContextProvider) {
116+
this(queryMethod, operations, rowMapperFactory, converter, evaluationContextProvider, QueryPreprocessor.NOOP.transform(queryMethod.getRequiredQuery()));
117+
}
118+
119+
/**
120+
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
121+
* and {@link RowMapperFactory}.
122+
*
123+
* @param queryMethod must not be {@literal null}.
124+
* @param operations must not be {@literal null}.
125+
* @param rowMapperFactory must not be {@literal null}.
126+
* @param converter must not be {@literal null}.
127+
* @param evaluationContextProvider must not be {@literal null}.
128+
* @param query
129+
* @since 3.4
130+
*/
131+
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
132+
RowMapperFactory rowMapperFactory, JdbcConverter converter,
133+
QueryMethodEvaluationContextProvider evaluationContextProvider, String query) {
111134

112135
super(queryMethod, operations);
113136

@@ -116,6 +139,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
116139
this.converter = converter;
117140
this.rowMapperFactory = rowMapperFactory;
118141

142+
119143
if (queryMethod.isSliceQuery()) {
120144
throw new UnsupportedOperationException(
121145
"Slice queries are not supported using string-based queries; Offending method: " + queryMethod);
@@ -140,9 +164,9 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
140164
.of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat)
141165
.withEvaluationContextProvider(evaluationContextProvider);
142166

143-
this.query = queryMethod.getRequiredQuery();
144-
this.spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters());
145-
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(query);
167+
this.query = query;
168+
this.spelEvaluator = queryContext.parse(this.query, getQueryMethod().getParameters());
169+
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(this.query);
146170
}
147171

148172
@Override

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3737
import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
3838
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
39+
import org.springframework.data.relational.repository.support.RelationalQueryLookupStrategy;
3940
import org.springframework.data.repository.core.NamedQueries;
4041
import org.springframework.data.repository.core.RepositoryMetadata;
4142
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -60,7 +61,7 @@
6061
* @author Diego Krupitza
6162
* @author Christopher Klein
6263
*/
63-
abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy {
64+
abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
6465

6566
private static final Log LOG = LogFactory.getLog(JdbcQueryLookupStrategy.class);
6667

@@ -79,8 +80,10 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy {
7980
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
8081
@Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) {
8182

83+
super(context, dialect);
84+
8285
Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
83-
Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
86+
Assert.notNull(context, "RelationalMappingContext must not be null");
8487
Assert.notNull(converter, "JdbcConverter must not be null");
8588
Assert.notNull(dialect, "Dialect must not be null");
8689
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
@@ -156,8 +159,10 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository
156159
"Query method %s is annotated with both, a query and a query name; Using the declared query", method));
157160
}
158161

162+
String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery());
163+
159164
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper,
160-
getConverter(), evaluationContextProvider);
165+
getConverter(), evaluationContextProvider, queryString);
161166
query.setBeanFactory(getBeanFactory());
162167
return query;
163168
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2024 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+
17+
package org.springframework.data.jdbc.repository;
18+
19+
import static org.assertj.core.api.Assertions.*;
20+
import static org.mockito.Mockito.*;
21+
22+
import org.jetbrains.annotations.NotNull;
23+
import org.junit.jupiter.api.Test;
24+
import org.mockito.ArgumentCaptor;
25+
import org.springframework.context.ApplicationEventPublisher;
26+
import org.springframework.data.annotation.Id;
27+
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
28+
import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory;
29+
import org.springframework.data.jdbc.core.convert.DelegatingDataAccessStrategy;
30+
import org.springframework.data.jdbc.core.convert.JdbcConverter;
31+
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
32+
import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
33+
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
34+
import org.springframework.data.jdbc.repository.query.Query;
35+
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
36+
import org.springframework.data.relational.core.dialect.Dialect;
37+
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
38+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
39+
import org.springframework.data.relational.core.mapping.Table;
40+
import org.springframework.data.repository.CrudRepository;
41+
import org.springframework.jdbc.core.RowMapper;
42+
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
43+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
44+
import org.springframework.lang.Nullable;
45+
46+
/**
47+
* Extracts the SQL statement that results from declared queries of a repository and perform assertions on it.
48+
*
49+
* @author Jens Schauder
50+
*/
51+
public class DeclaredQueryRepositoryUnitTests {
52+
53+
private NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class, RETURNS_DEEP_STUBS);
54+
55+
@Test // GH-1856
56+
void plainSql() {
57+
58+
repository(DummyEntityRepository.class).plainQuery();
59+
60+
assertThat(query()).isEqualTo("select * from someTable");
61+
}
62+
63+
@Test // GH-1856
64+
void tableNameQuery() {
65+
66+
repository(DummyEntityRepository.class).tableNameQuery();
67+
68+
assertThat(query()).isEqualTo("select * from \"DUMMY_ENTITY\"");
69+
}
70+
71+
@Test // GH-1856
72+
void renamedTableNameQuery() {
73+
74+
repository(RenamedEntityRepository.class).tableNameQuery();
75+
76+
assertThat(query()).isEqualTo("select * from \"ReNamed\"");
77+
}
78+
79+
@Test // GH-1856
80+
void fullyQualifiedTableNameQuery() {
81+
82+
repository(RenamedEntityRepository.class).qualifiedTableNameQuery();
83+
84+
assertThat(query()).isEqualTo("select * from \"someSchema\".\"ReNamed\"");
85+
}
86+
87+
private String query() {
88+
89+
ArgumentCaptor<String> queryCaptor = ArgumentCaptor.forClass(String.class);
90+
verify(operations).queryForObject(queryCaptor.capture(), any(SqlParameterSource.class), any(RowMapper.class));
91+
return queryCaptor.getValue();
92+
}
93+
94+
private @NotNull <T extends CrudRepository> T repository(Class<T> repositoryInterface) {
95+
96+
Dialect dialect = HsqlDbDialect.INSTANCE;
97+
98+
RelationalMappingContext context = new JdbcMappingContext();
99+
100+
DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy();
101+
JdbcConverter converter = new MappingJdbcConverter(context, delegatingDataAccessStrategy,
102+
new JdbcCustomConversions(), new DefaultJdbcTypeFactory(operations.getJdbcOperations()));
103+
104+
DataAccessStrategy dataAccessStrategy = mock(DataAccessStrategy.class);
105+
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
106+
107+
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, dialect,
108+
publisher, operations);
109+
110+
return factory.getRepository(repositoryInterface);
111+
}
112+
113+
@Table
114+
record DummyEntity(@Id Long id, String name) {
115+
}
116+
117+
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
118+
119+
@Nullable
120+
@Query("select * from someTable")
121+
DummyEntity plainQuery();
122+
123+
@Nullable
124+
@Query("select * from #{#tableName}")
125+
DummyEntity tableNameQuery();
126+
}
127+
128+
@Table(name = "ReNamed", schema = "someSchema")
129+
record RenamedEntity(@Id Long id, String name) {
130+
}
131+
132+
interface RenamedEntityRepository extends CrudRepository<RenamedEntity, Long> {
133+
134+
@Nullable
135+
@Query("select * from #{#tableName}")
136+
DummyEntity tableNameQuery();
137+
138+
@Nullable
139+
@Query("select * from #{#qualifiedTableName}")
140+
DummyEntity qualifiedTableNameQuery();
141+
}
142+
}

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java

+9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.data.r2dbc.query.UpdateMapper;
4242
import org.springframework.data.r2dbc.support.ArrayUtils;
4343
import org.springframework.data.relational.core.dialect.ArrayColumns;
44+
import org.springframework.data.relational.core.dialect.Dialect;
4445
import org.springframework.data.relational.core.dialect.RenderContextFactory;
4546
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
4647
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -310,6 +311,14 @@ public String renderForGeneratedValues(SqlIdentifier identifier) {
310311
return dialect.renderForGeneratedValues(identifier);
311312
}
312313

314+
/**
315+
* @since 3.4
316+
*/
317+
@Override
318+
public Dialect getDialect() {
319+
return dialect;
320+
}
321+
313322
private RelationalPersistentEntity<?> getRequiredPersistentEntity(Class<?> typeToRead) {
314323
return this.mappingContext.getRequiredPersistentEntity(typeToRead);
315324
}

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
import org.springframework.data.r2dbc.convert.R2dbcConverter;
2727
import org.springframework.data.r2dbc.mapping.OutboundRow;
28+
import org.springframework.data.relational.core.dialect.AnsiDialect;
29+
import org.springframework.data.relational.core.dialect.Dialect;
2830
import org.springframework.data.relational.core.sql.IdentifierProcessing;
2931
import org.springframework.data.relational.core.sql.SqlIdentifier;
3032
import org.springframework.data.relational.domain.RowDocument;
@@ -154,6 +156,14 @@ default String renderForGeneratedValues(SqlIdentifier identifier) {
154156
return identifier.toSql(IdentifierProcessing.NONE);
155157
}
156158

159+
/**
160+
* @return the {@link Dialect} used by this strategy.
161+
* @since 3.4
162+
*/
163+
default Dialect getDialect() {
164+
return AnsiDialect.INSTANCE;
165+
}
166+
157167
/**
158168
* Interface to retrieve parameters for named parameter processing.
159169
*/

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java

+17-10
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
3333
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
3434
import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation;
35+
import org.springframework.data.relational.repository.support.RelationalQueryLookupStrategy;
3536
import org.springframework.data.repository.core.NamedQueries;
3637
import org.springframework.data.repository.core.RepositoryInformation;
3738
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -51,6 +52,7 @@
5152
* Factory to create {@link R2dbcRepository} instances.
5253
*
5354
* @author Mark Paluch
55+
* @author Jens Schauder
5456
*/
5557
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
5658

@@ -139,8 +141,9 @@ private <T, ID> RelationalEntityInformation<T, ID> getEntityInformation(Class<T>
139141
* {@link QueryLookupStrategy} to create R2DBC queries..
140142
*
141143
* @author Mark Paluch
144+
* @author Jens Schauder
142145
*/
143-
private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
146+
private static class R2dbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
144147

145148
private final R2dbcEntityOperations entityOperations;
146149
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
@@ -151,30 +154,34 @@ private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
151154
R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations,
152155
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, R2dbcConverter converter,
153156
ReactiveDataAccessStrategy dataAccessStrategy) {
157+
158+
super(converter.getMappingContext(), dataAccessStrategy.getDialect());
159+
154160
this.entityOperations = entityOperations;
155161
this.evaluationContextProvider = evaluationContextProvider;
156162
this.converter = converter;
157163
this.dataAccessStrategy = dataAccessStrategy;
158-
159164
}
160165

161166
@Override
162167
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
163168
NamedQueries namedQueries) {
164169

170+
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext = this.converter.getMappingContext();
171+
165172
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory,
166-
this.converter.getMappingContext());
173+
mappingContext);
167174
String namedQueryName = queryMethod.getNamedQueryName();
168175

169-
if (namedQueries.hasQuery(namedQueryName)) {
170-
String namedQuery = namedQueries.getQuery(namedQueryName);
171-
return new StringBasedR2dbcQuery(namedQuery, queryMethod, this.entityOperations, this.converter,
176+
if (namedQueries.hasQuery(namedQueryName) || queryMethod.hasAnnotatedQuery()) {
177+
178+
String query = namedQueries.hasQuery(namedQueryName) ? namedQueries.getQuery(namedQueryName) : queryMethod.getRequiredAnnotatedQuery();
179+
query = evaluateTableExpressions(metadata, query);
180+
181+
return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter,
172182
this.dataAccessStrategy,
173183
parser, this.evaluationContextProvider);
174-
} else if (queryMethod.hasAnnotatedQuery()) {
175-
return new StringBasedR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy,
176-
this.parser,
177-
this.evaluationContextProvider);
184+
178185
} else {
179186
return new PartTreeR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy);
180187
}

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/documentation/PersonRepository.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public interface PersonRepository extends ReactiveCrudRepository<Person, String>
3434

3535
// tag::spel[]
3636
@Query("SELECT * FROM person WHERE lastname = :#{[0]}")
37-
Flux<Person> findByQueryWithExpression(String lastname);
37+
Flux<Person> findByQueryWithParameterExpression(String lastname);
3838
// end::spel[]
39+
40+
// tag::spel2[]
41+
@Query("SELECT * FROM #{tableName} WHERE lastname = :lastname")
42+
Flux<Person> findByQueryWithExpression(String lastname);
43+
// end::spel2[]
3944
}

0 commit comments

Comments
 (0)