mirror of
https://github.com/spring-projects/spring-data-relational.git
synced 2026-05-03 11:51:36 +01:00
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
+1
-1
@@ -128,7 +128,7 @@ public class JdbcQueryMethod extends QueryMethod {
|
||||
return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery();
|
||||
}
|
||||
|
||||
String getRequiredQuery() {
|
||||
public String getRequiredQuery() {
|
||||
|
||||
String query = getDeclaredQuery();
|
||||
|
||||
|
||||
+27
-3
@@ -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
|
||||
|
||||
+8
-3
@@ -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;
|
||||
}
|
||||
|
||||
+142
@@ -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();
|
||||
}
|
||||
}
|
||||
+9
@@ -41,6 +41,7 @@ import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
|
||||
import org.springframework.data.r2dbc.query.UpdateMapper;
|
||||
import org.springframework.data.r2dbc.support.ArrayUtils;
|
||||
import org.springframework.data.relational.core.dialect.ArrayColumns;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.dialect.RenderContextFactory;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
@@ -310,6 +311,14 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
|
||||
return dialect.renderForGeneratedValues(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.4
|
||||
*/
|
||||
@Override
|
||||
public Dialect getDialect() {
|
||||
return dialect;
|
||||
}
|
||||
|
||||
private RelationalPersistentEntity<?> getRequiredPersistentEntity(Class<?> typeToRead) {
|
||||
return this.mappingContext.getRequiredPersistentEntity(typeToRead);
|
||||
}
|
||||
|
||||
+10
@@ -25,6 +25,8 @@ import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.data.r2dbc.convert.R2dbcConverter;
|
||||
import org.springframework.data.r2dbc.mapping.OutboundRow;
|
||||
import org.springframework.data.relational.core.dialect.AnsiDialect;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.sql.IdentifierProcessing;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.relational.domain.RowDocument;
|
||||
@@ -154,6 +156,14 @@ public interface ReactiveDataAccessStrategy {
|
||||
return identifier.toSql(IdentifierProcessing.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link Dialect} used by this strategy.
|
||||
* @since 3.4
|
||||
*/
|
||||
default Dialect getDialect() {
|
||||
return AnsiDialect.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to retrieve parameters for named parameter processing.
|
||||
*/
|
||||
|
||||
+17
-10
@@ -32,6 +32,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
|
||||
import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation;
|
||||
import org.springframework.data.relational.repository.support.RelationalQueryLookupStrategy;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryInformation;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
@@ -51,6 +52,7 @@ import org.springframework.util.Assert;
|
||||
* Factory to create {@link R2dbcRepository} instances.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
|
||||
|
||||
@@ -139,8 +141,9 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
|
||||
* {@link QueryLookupStrategy} to create R2DBC queries..
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
|
||||
private static class R2dbcQueryLookupStrategy extends RelationalQueryLookupStrategy {
|
||||
|
||||
private final R2dbcEntityOperations entityOperations;
|
||||
private final ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider;
|
||||
@@ -151,30 +154,34 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
|
||||
R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations,
|
||||
ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider, R2dbcConverter converter,
|
||||
ReactiveDataAccessStrategy dataAccessStrategy) {
|
||||
|
||||
super(converter.getMappingContext(), dataAccessStrategy.getDialect());
|
||||
|
||||
this.entityOperations = entityOperations;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.converter = converter;
|
||||
this.dataAccessStrategy = dataAccessStrategy;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
|
||||
NamedQueries namedQueries) {
|
||||
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext = this.converter.getMappingContext();
|
||||
|
||||
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory,
|
||||
this.converter.getMappingContext());
|
||||
mappingContext);
|
||||
String namedQueryName = queryMethod.getNamedQueryName();
|
||||
|
||||
if (namedQueries.hasQuery(namedQueryName)) {
|
||||
String namedQuery = namedQueries.getQuery(namedQueryName);
|
||||
return new StringBasedR2dbcQuery(namedQuery, queryMethod, this.entityOperations, this.converter,
|
||||
if (namedQueries.hasQuery(namedQueryName) || queryMethod.hasAnnotatedQuery()) {
|
||||
|
||||
String query = namedQueries.hasQuery(namedQueryName) ? namedQueries.getQuery(namedQueryName) : queryMethod.getRequiredAnnotatedQuery();
|
||||
query = evaluateTableExpressions(metadata, query);
|
||||
|
||||
return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter,
|
||||
this.dataAccessStrategy,
|
||||
parser, this.evaluationContextProvider);
|
||||
} else if (queryMethod.hasAnnotatedQuery()) {
|
||||
return new StringBasedR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy,
|
||||
this.parser,
|
||||
this.evaluationContextProvider);
|
||||
|
||||
} else {
|
||||
return new PartTreeR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy);
|
||||
}
|
||||
|
||||
+6
-1
@@ -34,6 +34,11 @@ public interface PersonRepository extends ReactiveCrudRepository<Person, String>
|
||||
|
||||
// tag::spel[]
|
||||
@Query("SELECT * FROM person WHERE lastname = :#{[0]}")
|
||||
Flux<Person> findByQueryWithExpression(String lastname);
|
||||
Flux<Person> findByQueryWithParameterExpression(String lastname);
|
||||
// end::spel[]
|
||||
|
||||
// tag::spel2[]
|
||||
@Query("SELECT * FROM #{tableName} WHERE lastname = :lastname")
|
||||
Flux<Person> findByQueryWithExpression(String lastname);
|
||||
// end::spel2[]
|
||||
}
|
||||
|
||||
+5
@@ -29,6 +29,7 @@ import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
|
||||
import org.springframework.data.r2dbc.convert.R2dbcConverter;
|
||||
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
|
||||
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
|
||||
import org.springframework.data.relational.core.dialect.AnsiDialect;
|
||||
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
|
||||
import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation;
|
||||
import org.springframework.data.repository.Repository;
|
||||
@@ -38,6 +39,7 @@ import org.springframework.r2dbc.core.DatabaseClient;
|
||||
* Unit test for {@link R2dbcRepositoryFactory}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class R2dbcRepositoryFactoryUnitTests {
|
||||
@@ -50,6 +52,7 @@ public class R2dbcRepositoryFactoryUnitTests {
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void before() {
|
||||
|
||||
when(dataAccessStrategy.getConverter()).thenReturn(r2dbcConverter);
|
||||
}
|
||||
|
||||
@@ -65,6 +68,8 @@ public class R2dbcRepositoryFactoryUnitTests {
|
||||
@Test
|
||||
public void createsRepositoryWithIdTypeLong() {
|
||||
|
||||
when(dataAccessStrategy.getDialect()).thenReturn(AnsiDialect.INSTANCE);
|
||||
|
||||
R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, dataAccessStrategy);
|
||||
MyPersonRepository repository = factory.getRepository(MyPersonRepository.class);
|
||||
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2018-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.r2dbc.repository.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
|
||||
import org.springframework.data.r2dbc.convert.R2dbcConverter;
|
||||
import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
|
||||
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
|
||||
import org.springframework.data.r2dbc.dialect.H2Dialect;
|
||||
import org.springframework.data.r2dbc.dialect.PostgresDialect;
|
||||
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.testing.StatementRecorder;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.r2dbc.core.DatabaseClient;
|
||||
|
||||
/**
|
||||
* Test extracting the SQL from a repository method call and performing assertions on it.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class SqlInspectingR2dbcRepositoryUnitTests {
|
||||
|
||||
R2dbcConverter r2dbcConverter = new MappingR2dbcConverter(new R2dbcMappingContext());
|
||||
|
||||
DatabaseClient databaseClient;
|
||||
StatementRecorder recorder = StatementRecorder.newInstance();
|
||||
ReactiveDataAccessStrategy dataAccessStrategy = new DefaultReactiveDataAccessStrategy(H2Dialect.INSTANCE);
|
||||
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
public void before() {
|
||||
|
||||
databaseClient = DatabaseClient.builder().connectionFactory(recorder)
|
||||
.bindMarkers(H2Dialect.INSTANCE.getBindMarkersFactory()).build();
|
||||
|
||||
}
|
||||
|
||||
@Test // GH-1856
|
||||
public void replacesSpelExpressionInQuery() {
|
||||
|
||||
recorder.addStubbing(SqlInspectingR2dbcRepositoryUnitTests::isSelect, List.of());
|
||||
|
||||
R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, dataAccessStrategy);
|
||||
MyPersonRepository repository = factory.getRepository(MyPersonRepository.class);
|
||||
|
||||
assertThat(repository).isNotNull();
|
||||
|
||||
repository.findBySpel().block(Duration.ofMillis(100));
|
||||
|
||||
StatementRecorder.RecordedStatement statement = recorder.getCreatedStatement(SqlInspectingR2dbcRepositoryUnitTests::isSelect);
|
||||
|
||||
assertThat(statement.getSql()).isEqualTo("select * from PERSONx");
|
||||
}
|
||||
|
||||
private static boolean isSelect(String sql) {
|
||||
return sql.toLowerCase().startsWith("select");
|
||||
}
|
||||
|
||||
interface MyPersonRepository extends Repository<Person, Long> {
|
||||
@Query("select * from #{#tableName +'x'}")
|
||||
Mono<Person> findBySpel();
|
||||
}
|
||||
|
||||
static class Person {
|
||||
@Id long id;
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.relational.repository.query;
|
||||
|
||||
public interface QueryPreprocessor {
|
||||
|
||||
QueryPreprocessor NOOP = new QueryPreprocessor() {
|
||||
|
||||
@Override
|
||||
public String transform(String query) {
|
||||
return query;
|
||||
}
|
||||
};
|
||||
String transform(String query);
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.relational.repository.support;
|
||||
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.relational.repository.query.QueryPreprocessor;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Base class for R2DBC and JDBC {@link QueryLookupStrategy} implementations.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
* @since 3.4
|
||||
*/
|
||||
public abstract class RelationalQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context;
|
||||
private final Dialect dialect;
|
||||
|
||||
protected RelationalQueryLookupStrategy(
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
|
||||
Dialect dialect) {
|
||||
|
||||
Assert.notNull(context, "RelationalMappingContext must not be null");
|
||||
Assert.notNull(dialect, "Dialect must not be null");
|
||||
|
||||
this.context = context;
|
||||
this.dialect = dialect;
|
||||
}
|
||||
|
||||
protected String evaluateTableExpressions(RepositoryMetadata repositoryMetadata, String queryString) {
|
||||
|
||||
return prepareQueryPreprocessor(repositoryMetadata).transform(queryString);
|
||||
}
|
||||
|
||||
private QueryPreprocessor prepareQueryPreprocessor(RepositoryMetadata repositoryMetadata) {
|
||||
|
||||
SqlIdentifier tableName = context.getPersistentEntity(repositoryMetadata.getDomainType()).getTableName();
|
||||
SqlIdentifier qualifiedTableName = context.getPersistentEntity(repositoryMetadata.getDomainType())
|
||||
.getQualifiedTableName();
|
||||
return new TableNameQueryPreprocessor(tableName, qualifiedTableName, dialect);
|
||||
}
|
||||
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.relational.repository.support;
|
||||
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.relational.repository.query.QueryPreprocessor;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Replaces SpEL expressions based on table names in query strings.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class TableNameQueryPreprocessor implements QueryPreprocessor {
|
||||
|
||||
private static final String EXPRESSION_PARAMETER = "$1#{";
|
||||
private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{";
|
||||
|
||||
private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile("([:?])#\\{");
|
||||
private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile("([:?])__HASH__\\{");
|
||||
|
||||
private final SqlIdentifier tableName;
|
||||
private final SqlIdentifier qualifiedTableName;
|
||||
private final Dialect dialect;
|
||||
|
||||
public TableNameQueryPreprocessor(SqlIdentifier tableName, SqlIdentifier qualifiedTableName, Dialect dialect) {
|
||||
|
||||
Assert.notNull(tableName, "TableName must not be null");
|
||||
Assert.notNull(qualifiedTableName, "QualifiedTableName must not be null");
|
||||
Assert.notNull(dialect, "Dialect must not be null");
|
||||
|
||||
this.tableName = tableName;
|
||||
this.qualifiedTableName = qualifiedTableName;
|
||||
this.dialect = dialect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String transform(String query) {
|
||||
|
||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
|
||||
evaluationContext.setVariable("tableName", tableName.toSql(dialect.getIdentifierProcessing()));
|
||||
evaluationContext.setVariable("qualifiedTableName", qualifiedTableName.toSql(dialect.getIdentifierProcessing()));
|
||||
|
||||
SpelExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
query = quoteExpressionsParameter(query);
|
||||
Expression expression = parser.parseExpression(query, ParserContext.TEMPLATE_EXPRESSION);
|
||||
|
||||
return unquoteParameterExpressions(expression.getValue(evaluationContext, String.class));
|
||||
}
|
||||
|
||||
private static String unquoteParameterExpressions(String result) {
|
||||
return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER);
|
||||
}
|
||||
|
||||
private static String quoteExpressionsParameter(String query) {
|
||||
return EXPRESSION_PARAMETER_QUOTING.matcher(query).replaceAll(QUOTED_EXPRESSION_PARAMETER);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.relational.repository.support;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.relational.core.dialect.AnsiDialect;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
|
||||
/**
|
||||
* Tests for {@link TableNameQueryPreprocessor}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class TableNameQueryPreprocessorUnitTests {
|
||||
|
||||
@Test // GH-1856
|
||||
void transform() {
|
||||
|
||||
TableNameQueryPreprocessor preprocessor = new TableNameQueryPreprocessor(SqlIdentifier.quoted("some_table_name"), SqlIdentifier.quoted("qualified_table_name"), AnsiDialect.INSTANCE);
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
softly.assertThat(preprocessor.transform("someString")).isEqualTo("someString");
|
||||
softly.assertThat(preprocessor.transform("someString#{#tableName}restOfString"))
|
||||
.isEqualTo("someString\"some_table_name\"restOfString");
|
||||
softly.assertThat(preprocessor.transform("select from #{#tableName} where x = :#{#some other spel}"))
|
||||
.isEqualTo("select from \"some_table_name\" where x = :#{#some other spel}");
|
||||
softly.assertThat(preprocessor.transform("select from #{#qualifiedTableName}"))
|
||||
.isEqualTo("select from \"qualified_table_name\"");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,10 @@ For example if the `User` from the example above has an `address` with the prope
|
||||
WARNING: Note that String-based queries do not support pagination nor accept `Sort`, `PageRequest`, and `Limit` as a query parameter as for these queries the query would be required to be rewritten.
|
||||
If you want to apply limiting, please express this intent using SQL and bind the appropriate parameters to the query yourself.
|
||||
|
||||
Queries may contain SpEL expressions where bind variables are allowed.
|
||||
Queries may contain SpEL expressions.
|
||||
There are two variants that are evaluated differently.
|
||||
|
||||
In the first variant a SpEL expression is prefixed with `:` and used like a bind variable.
|
||||
Such a SpEL expression will get replaced with a bind variable and the variable gets bound to the result of the SpEL expression.
|
||||
|
||||
.Use a SpEL in a query
|
||||
@@ -189,6 +192,18 @@ Person findWithSpEL(PersonRef person);
|
||||
This can be used to access members of a parameter, as demonstrated in the example above.
|
||||
For more involved use cases an `EvaluationContextExtension` can be made available in the application context, which in turn can make any object available in to the SpEL.
|
||||
|
||||
The other variant can be used anywhere in the query and the result of evaluating the query will replace the expression in the query string.
|
||||
|
||||
.Use a SpEL in a query
|
||||
[source,java]
|
||||
----
|
||||
@Query("SELECT * FROM #{tableName} WHERE id = :id")
|
||||
Person findWithSpEL(PersonRef person);
|
||||
----
|
||||
|
||||
It is evaluated once before the first execution and uses a `StandardEvaluationContext` with the two variables `tableName` and `qualifiedTableName` added.
|
||||
This use is most useful when table names are dynamic themselves, because they use SpEL expressions as well.
|
||||
|
||||
NOTE: Spring fully supports Java 8’s parameter name discovery based on the `-parameters` compiler flag.
|
||||
By using this flag in your build as an alternative to debug information, you can omit the `@Param` annotation for named parameters.
|
||||
|
||||
|
||||
@@ -207,6 +207,8 @@ By using this flag in your build as an alternative to debug information, you can
|
||||
=== Queries with SpEL Expressions
|
||||
|
||||
Query string definitions can be used together with SpEL expressions to create dynamic queries at runtime.
|
||||
SpEL expressions can be used in two ways.
|
||||
|
||||
SpEL expressions can provide predicate values which are evaluated right before running the query.
|
||||
|
||||
Expressions expose method arguments through an array that contains all the arguments.
|
||||
@@ -218,12 +220,24 @@ to declare the predicate value for `lastname` (which is equivalent to the `:last
|
||||
include::example$r2dbc/PersonRepository.java[tags=spel]
|
||||
----
|
||||
|
||||
SpEL in query strings can be a powerful way to enhance queries.
|
||||
However, they can also accept a broad range of unwanted arguments.
|
||||
You should make sure to sanitize strings before passing them to the query to avoid unwanted changes to your query.
|
||||
|
||||
Expression support is extensible through the Query SPI: `org.springframework.data.spel.spi.EvaluationContextExtension`.
|
||||
This Expression support is extensible through the Query SPI: `org.springframework.data.spel.spi.EvaluationContextExtension`.
|
||||
The Query SPI can contribute properties and functions and can customize the root object.
|
||||
Extensions are retrieved from the application context at the time of SpEL evaluation when the query is built.
|
||||
|
||||
TIP: When using SpEL expressions in combination with plain parameters, use named parameter notation instead of native bind markers to ensure a proper binding order.
|
||||
|
||||
The other way to use Expression is in the middle of query, independent of parameters.
|
||||
The result of evaluating the query will replace the expression in the query string.
|
||||
|
||||
.Use a SpEL in a query
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::example$r2dbc/PersonRepository.java[tags=spel2]
|
||||
----
|
||||
|
||||
It is evaluated once before the first execution and uses a `StandardEvaluationContext` with the two variables `tableName` and `qualifiedTableName` added.
|
||||
This use is most useful when table names are dynamic themselves, because they use SpEL expressions as well.
|
||||
|
||||
SpEL in query strings can be a powerful way to enhance queries.
|
||||
However, they can also accept a broad range of unwanted arguments.
|
||||
You should make sure to sanitize strings before passing them to the query to avoid unwanted changes to your query.
|
||||
|
||||
Reference in New Issue
Block a user