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
Originial pull request #1863
  • Loading branch information
schauder committed Aug 28, 2024
1 parent fd6d4e6 commit 21a5f74
Show file tree
Hide file tree
Showing 13 changed files with 504 additions and 18 deletions.
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);
}

/**
* 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 queryPreprocessor must not be {@literal null}.
* @since 3.4
*/
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
RowMapperFactory rowMapperFactory, JdbcConverter converter,
QueryMethodEvaluationContextProvider evaluationContextProvider, QueryPreprocessor queryPreprocessor) {

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,7 +164,8 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
.of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat)
.withEvaluationContextProvider(evaluationContextProvider);

this.query = queryMethod.getRequiredQuery();

this.query = queryPreprocessor.transform(queryMethod.getRequiredQuery());
this.spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters());
this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(query);
}
Expand All @@ -160,7 +185,7 @@ public Object execute(Object[] objects) {
private String processSpelExpressions(Object[] objects, MapSqlParameterSource parameterMap) {

if (containsSpelExpressions) {

// TODO: Make code changes here
spelEvaluator.evaluate(objects).forEach(parameterMap::addValue);
return spelEvaluator.getQueryString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@
import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
import org.springframework.data.jdbc.repository.query.PartTreeJdbcQuery;
import org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery;
import org.springframework.data.relational.repository.query.QueryPreprocessor;
import org.springframework.data.relational.repository.query.TableNameQueryPreprocessor;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.dialect.Dialect;
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.core.sql.SqlIdentifier;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
Expand Down Expand Up @@ -156,15 +159,24 @@ 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));
}

QueryPreprocessor queryPreprocessor = prepareQueryPreprocessor(repositoryMetadata);
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper,
getConverter(), evaluationContextProvider);
getConverter(), evaluationContextProvider,
queryPreprocessor);
query.setBeanFactory(getBeanFactory());
return query;
}

throw new IllegalStateException(
String.format("Did neither find a NamedQuery nor an annotated query for method %s", method));
}

private QueryPreprocessor prepareQueryPreprocessor(RepositoryMetadata repositoryMetadata) {

SqlIdentifier tableName = getContext().getPersistentEntity(repositoryMetadata.getDomainType()).getTableName();
SqlIdentifier qualifiedTableName = getContext().getPersistentEntity(repositoryMetadata.getDomainType()).getQualifiedTableName();
return new TableNameQueryPreprocessor(tableName, qualifiedTableName, getDialect());
}
}

/**
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
void plainSql() {

repository(DummyEntityRepository.class).plainQuery();

assertThat(query()).isEqualTo("select * from someTable");
}

@Test
void tableNameQuery() {

repository(DummyEntityRepository.class).tableNameQuery();

assertThat(query()).isEqualTo("select * from \"DUMMY_ENTITY\"");
}

@Test
void renamedTableNameQuery() {

repository(RenamedEntityRepository.class).tableNameQuery();

assertThat(query()).isEqualTo("select * from \"ReNamed\"");
}

@Test
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 @@ -28,9 +28,13 @@
import org.springframework.data.r2dbc.repository.query.PartTreeR2dbcQuery;
import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod;
import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery;
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.relational.repository.query.RelationalEntityInformation;
import org.springframework.data.relational.repository.query.TableNameQueryPreprocessor;
import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryInformation;
Expand All @@ -51,6 +55,7 @@
* Factory to create {@link R2dbcRepository} instances.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {

Expand Down Expand Up @@ -162,19 +167,29 @@ private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {

MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext = this.converter.getMappingContext();
Dialect dialect = dataAccessStrategy.getDialect();

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,
Class<?> domainType = metadata.getDomainType();
RelationalPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainType);
SqlIdentifier tableName = entity.getTableName();
SqlIdentifier qualifiedTableName = entity.getQualifiedTableName();

if (namedQueries.hasQuery(namedQueryName) || queryMethod.hasAnnotatedQuery()) {

QueryPreprocessor queryPreprocessor = new TableNameQueryPreprocessor(tableName, qualifiedTableName, dialect);

String query = namedQueries.hasQuery(namedQueryName) ? namedQueries.getQuery(namedQueryName) : queryMethod.getRequiredAnnotatedQuery();
query = queryPreprocessor.transform(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 21a5f74

Please sign in to comment.