Skip to content

Commit 21a5f74

Browse files
committed
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 Originial pull request #1863
1 parent fd6d4e6 commit 21a5f74

File tree

13 files changed

+504
-18
lines changed

13 files changed

+504
-18
lines changed

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

+27-2
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);
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 queryPreprocessor must not be {@literal null}.
129+
* @since 3.4
130+
*/
131+
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
132+
RowMapperFactory rowMapperFactory, JdbcConverter converter,
133+
QueryMethodEvaluationContextProvider evaluationContextProvider, QueryPreprocessor queryPreprocessor) {
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,7 +164,8 @@ 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();
167+
168+
this.query = queryPreprocessor.transform(queryMethod.getRequiredQuery());
144169
this.spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters());
145170
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(query);
146171
}
@@ -160,7 +185,7 @@ public Object execute(Object[] objects) {
160185
private String processSpelExpressions(Object[] objects, MapSqlParameterSource parameterMap) {
161186

162187
if (containsSpelExpressions) {
163-
188+
// TODO: Make code changes here
164189
spelEvaluator.evaluate(objects).forEach(parameterMap::addValue);
165190
return spelEvaluator.getQueryString();
166191
}

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@
2929
import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
3030
import org.springframework.data.jdbc.repository.query.PartTreeJdbcQuery;
3131
import org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery;
32+
import org.springframework.data.relational.repository.query.QueryPreprocessor;
33+
import org.springframework.data.relational.repository.query.TableNameQueryPreprocessor;
3234
import org.springframework.data.mapping.callback.EntityCallbacks;
3335
import org.springframework.data.projection.ProjectionFactory;
3436
import org.springframework.data.relational.core.dialect.Dialect;
3537
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3638
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3739
import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
3840
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
41+
import org.springframework.data.relational.core.sql.SqlIdentifier;
3942
import org.springframework.data.repository.core.NamedQueries;
4043
import org.springframework.data.repository.core.RepositoryMetadata;
4144
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -156,15 +159,24 @@ 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+
QueryPreprocessor queryPreprocessor = prepareQueryPreprocessor(repositoryMetadata);
159163
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper,
160-
getConverter(), evaluationContextProvider);
164+
getConverter(), evaluationContextProvider,
165+
queryPreprocessor);
161166
query.setBeanFactory(getBeanFactory());
162167
return query;
163168
}
164169

165170
throw new IllegalStateException(
166171
String.format("Did neither find a NamedQuery nor an annotated query for method %s", method));
167172
}
173+
174+
private QueryPreprocessor prepareQueryPreprocessor(RepositoryMetadata repositoryMetadata) {
175+
176+
SqlIdentifier tableName = getContext().getPersistentEntity(repositoryMetadata.getDomainType()).getTableName();
177+
SqlIdentifier qualifiedTableName = getContext().getPersistentEntity(repositoryMetadata.getDomainType()).getQualifiedTableName();
178+
return new TableNameQueryPreprocessor(tableName, qualifiedTableName, getDialect());
179+
}
168180
}
169181

170182
/**
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
56+
void plainSql() {
57+
58+
repository(DummyEntityRepository.class).plainQuery();
59+
60+
assertThat(query()).isEqualTo("select * from someTable");
61+
}
62+
63+
@Test
64+
void tableNameQuery() {
65+
66+
repository(DummyEntityRepository.class).tableNameQuery();
67+
68+
assertThat(query()).isEqualTo("select * from \"DUMMY_ENTITY\"");
69+
}
70+
71+
@Test
72+
void renamedTableNameQuery() {
73+
74+
repository(RenamedEntityRepository.class).tableNameQuery();
75+
76+
assertThat(query()).isEqualTo("select * from \"ReNamed\"");
77+
}
78+
79+
@Test
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

+23-8
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828
import org.springframework.data.r2dbc.repository.query.PartTreeR2dbcQuery;
2929
import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod;
3030
import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery;
31+
import org.springframework.data.relational.core.dialect.Dialect;
3132
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3233
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
34+
import org.springframework.data.relational.core.sql.SqlIdentifier;
35+
import org.springframework.data.relational.repository.query.QueryPreprocessor;
3336
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
37+
import org.springframework.data.relational.repository.query.TableNameQueryPreprocessor;
3438
import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation;
3539
import org.springframework.data.repository.core.NamedQueries;
3640
import org.springframework.data.repository.core.RepositoryInformation;
@@ -51,6 +55,7 @@
5155
* Factory to create {@link R2dbcRepository} instances.
5256
*
5357
* @author Mark Paluch
58+
* @author Jens Schauder
5459
*/
5560
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
5661

@@ -162,19 +167,29 @@ private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
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+
Dialect dialect = dataAccessStrategy.getDialect();
172+
165173
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory,
166-
this.converter.getMappingContext());
174+
mappingContext);
167175
String namedQueryName = queryMethod.getNamedQueryName();
168176

169-
if (namedQueries.hasQuery(namedQueryName)) {
170-
String namedQuery = namedQueries.getQuery(namedQueryName);
171-
return new StringBasedR2dbcQuery(namedQuery, queryMethod, this.entityOperations, this.converter,
177+
Class<?> domainType = metadata.getDomainType();
178+
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainType);
179+
SqlIdentifier tableName = entity.getTableName();
180+
SqlIdentifier qualifiedTableName = entity.getQualifiedTableName();
181+
182+
if (namedQueries.hasQuery(namedQueryName) || queryMethod.hasAnnotatedQuery()) {
183+
184+
QueryPreprocessor queryPreprocessor = new TableNameQueryPreprocessor(tableName, qualifiedTableName, dialect);
185+
186+
String query = namedQueries.hasQuery(namedQueryName) ? namedQueries.getQuery(namedQueryName) : queryMethod.getRequiredAnnotatedQuery();
187+
query = queryPreprocessor.transform(query);
188+
189+
return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter,
172190
this.dataAccessStrategy,
173191
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);
192+
178193
} else {
179194
return new PartTreeR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy);
180195
}

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)