Skip to content

Commit

Permalink
Support for table names in SpEL expressions.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
schauder authored and mp911de committed Sep 4, 2024
1 parent 4221840 commit f937738
Show file tree
Hide file tree
Showing 16 changed files with 574 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ String getDeclaredQuery() {
return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery();
}

String getRequiredQuery() {
public String getRequiredQuery() {

String query = getDeclaredQuery();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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;
Expand Down Expand Up @@ -103,11 +104,33 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
* @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);

Expand All @@ -116,6 +139,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
this.converter = converter;
this.rowMapperFactory = rowMapperFactory;


if (queryMethod.isSliceQuery()) {
throw new UnsupportedOperationException(
"Slice queries are not supported using string-based queries; Offending method: " + queryMethod);
Expand All @@ -140,9 +164,9 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
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;
Expand All @@ -60,7 +61,7 @@
* @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);

Expand All @@ -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");
Expand Down Expand Up @@ -156,8 +159,10 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository
"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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
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;
Expand Down Expand Up @@ -310,6 +311,14 @@ public String renderForGeneratedValues(SqlIdentifier identifier) {
return dialect.renderForGeneratedValues(identifier);
}

/**
* @since 3.4
*/
@Override
public Dialect getDialect() {
return dialect;
}

private RelationalPersistentEntity<?> getRequiredPersistentEntity(Class<?> typeToRead) {
return this.mappingContext.getRequiredPersistentEntity(typeToRead);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

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;
Expand Down Expand Up @@ -154,6 +156,14 @@ default String renderForGeneratedValues(SqlIdentifier identifier) {
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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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;
Expand All @@ -51,6 +52,7 @@
* Factory to create {@link R2dbcRepository} instances.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {

Expand Down Expand Up @@ -139,8 +141,9 @@ private <T, ID> RelationalEntityInformation<T, ID> getEntityInformation(Class<T>
* {@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;
Expand All @@ -151,30 +154,34 @@ private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
}
Loading

0 comments on commit f937738

Please sign in to comment.