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:
Jens Schauder
2024-08-20 11:08:08 +02:00
committed by Mark Paluch
parent 4221840538
commit f937738038
16 changed files with 574 additions and 24 deletions

View File

@@ -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);
}

View File

@@ -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.
*/

View File

@@ -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);
}

View File

@@ -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[]
}

View File

@@ -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);

View File

@@ -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;
}
}