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
This commit is contained in:
committed by
Mark Paluch
parent
4221840538
commit
f937738038
@@ -128,7 +128,7 @@ public class JdbcQueryMethod extends QueryMethod {
|
||||
return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery();
|
||||
}
|
||||
|
||||
String getRequiredQuery() {
|
||||
public String getRequiredQuery() {
|
||||
|
||||
String query = getDeclaredQuery();
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcValue;
|
||||
import org.springframework.data.jdbc.support.JdbcUtil;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.repository.query.QueryPreprocessor;
|
||||
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
|
||||
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
@@ -103,11 +104,33 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
|
||||
* @param queryMethod must not be {@literal null}.
|
||||
* @param operations must not be {@literal null}.
|
||||
* @param rowMapperFactory must not be {@literal null}.
|
||||
* @param converter must not be {@literal null}.
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @since 2.3
|
||||
* @deprecated use alternative constructor
|
||||
*/
|
||||
@Deprecated(since = "3.4")
|
||||
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
|
||||
RowMapperFactory rowMapperFactory, JdbcConverter converter,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
this(queryMethod, operations, rowMapperFactory, converter, evaluationContextProvider, QueryPreprocessor.NOOP.transform(queryMethod.getRequiredQuery()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
|
||||
* and {@link RowMapperFactory}.
|
||||
*
|
||||
* @param queryMethod must not be {@literal null}.
|
||||
* @param operations must not be {@literal null}.
|
||||
* @param rowMapperFactory must not be {@literal null}.
|
||||
* @param converter must not be {@literal null}.
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @param query
|
||||
* @since 3.4
|
||||
*/
|
||||
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
|
||||
RowMapperFactory rowMapperFactory, JdbcConverter converter,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider, String query) {
|
||||
|
||||
super(queryMethod, operations);
|
||||
|
||||
@@ -116,6 +139,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
|
||||
this.converter = converter;
|
||||
this.rowMapperFactory = rowMapperFactory;
|
||||
|
||||
|
||||
if (queryMethod.isSliceQuery()) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Slice queries are not supported using string-based queries; Offending method: " + queryMethod);
|
||||
@@ -140,9 +164,9 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
|
||||
.of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat)
|
||||
.withEvaluationContextProvider(evaluationContextProvider);
|
||||
|
||||
this.query = queryMethod.getRequiredQuery();
|
||||
this.spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters());
|
||||
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(query);
|
||||
this.query = query;
|
||||
this.spelEvaluator = queryContext.parse(this.query, getQueryMethod().getParameters());
|
||||
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(this.query);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
|
||||
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
|
||||
import org.springframework.data.relational.repository.support.RelationalQueryLookupStrategy;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
@@ -60,7 +61,7 @@ import org.springframework.util.Assert;
|
||||
* @author Diego Krupitza
|
||||
* @author Christopher Klein
|
||||
*/
|
||||
abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy {
|
||||
abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(JdbcQueryLookupStrategy.class);
|
||||
|
||||
@@ -79,8 +80,10 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy {
|
||||
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
|
||||
@Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
|
||||
super(context, dialect);
|
||||
|
||||
Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
|
||||
Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
|
||||
Assert.notNull(context, "RelationalMappingContext must not be null");
|
||||
Assert.notNull(converter, "JdbcConverter must not be null");
|
||||
Assert.notNull(dialect, "Dialect must not be null");
|
||||
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
|
||||
@@ -156,8 +159,10 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy {
|
||||
"Query method %s is annotated with both, a query and a query name; Using the declared query", method));
|
||||
}
|
||||
|
||||
String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery());
|
||||
|
||||
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper,
|
||||
getConverter(), evaluationContextProvider);
|
||||
getConverter(), evaluationContextProvider, queryString);
|
||||
query.setBeanFactory(getBeanFactory());
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.jdbc.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
|
||||
import org.springframework.data.jdbc.core.convert.DefaultJdbcTypeFactory;
|
||||
import org.springframework.data.jdbc.core.convert.DelegatingDataAccessStrategy;
|
||||
import org.springframework.data.jdbc.core.convert.JdbcConverter;
|
||||
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
|
||||
import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.jdbc.repository.query.Query;
|
||||
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Extracts the SQL statement that results from declared queries of a repository and perform assertions on it.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class DeclaredQueryRepositoryUnitTests {
|
||||
|
||||
private NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class, RETURNS_DEEP_STUBS);
|
||||
|
||||
@Test // GH-1856
|
||||
void plainSql() {
|
||||
|
||||
repository(DummyEntityRepository.class).plainQuery();
|
||||
|
||||
assertThat(query()).isEqualTo("select * from someTable");
|
||||
}
|
||||
|
||||
@Test // GH-1856
|
||||
void tableNameQuery() {
|
||||
|
||||
repository(DummyEntityRepository.class).tableNameQuery();
|
||||
|
||||
assertThat(query()).isEqualTo("select * from \"DUMMY_ENTITY\"");
|
||||
}
|
||||
|
||||
@Test // GH-1856
|
||||
void renamedTableNameQuery() {
|
||||
|
||||
repository(RenamedEntityRepository.class).tableNameQuery();
|
||||
|
||||
assertThat(query()).isEqualTo("select * from \"ReNamed\"");
|
||||
}
|
||||
|
||||
@Test // GH-1856
|
||||
void fullyQualifiedTableNameQuery() {
|
||||
|
||||
repository(RenamedEntityRepository.class).qualifiedTableNameQuery();
|
||||
|
||||
assertThat(query()).isEqualTo("select * from \"someSchema\".\"ReNamed\"");
|
||||
}
|
||||
|
||||
private String query() {
|
||||
|
||||
ArgumentCaptor<String> queryCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(operations).queryForObject(queryCaptor.capture(), any(SqlParameterSource.class), any(RowMapper.class));
|
||||
return queryCaptor.getValue();
|
||||
}
|
||||
|
||||
private @NotNull <T extends CrudRepository> T repository(Class<T> repositoryInterface) {
|
||||
|
||||
Dialect dialect = HsqlDbDialect.INSTANCE;
|
||||
|
||||
RelationalMappingContext context = new JdbcMappingContext();
|
||||
|
||||
DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy();
|
||||
JdbcConverter converter = new MappingJdbcConverter(context, delegatingDataAccessStrategy,
|
||||
new JdbcCustomConversions(), new DefaultJdbcTypeFactory(operations.getJdbcOperations()));
|
||||
|
||||
DataAccessStrategy dataAccessStrategy = mock(DataAccessStrategy.class);
|
||||
ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);
|
||||
|
||||
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, dialect,
|
||||
publisher, operations);
|
||||
|
||||
return factory.getRepository(repositoryInterface);
|
||||
}
|
||||
|
||||
@Table
|
||||
record DummyEntity(@Id Long id, String name) {
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
|
||||
|
||||
@Nullable
|
||||
@Query("select * from someTable")
|
||||
DummyEntity plainQuery();
|
||||
|
||||
@Nullable
|
||||
@Query("select * from #{#tableName}")
|
||||
DummyEntity tableNameQuery();
|
||||
}
|
||||
|
||||
@Table(name = "ReNamed", schema = "someSchema")
|
||||
record RenamedEntity(@Id Long id, String name) {
|
||||
}
|
||||
|
||||
interface RenamedEntityRepository extends CrudRepository<RenamedEntity, Long> {
|
||||
|
||||
@Nullable
|
||||
@Query("select * from #{#tableName}")
|
||||
DummyEntity tableNameQuery();
|
||||
|
||||
@Nullable
|
||||
@Query("select * from #{#qualifiedTableName}")
|
||||
DummyEntity qualifiedTableNameQuery();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user