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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user