From 2c4c4c999164981f6f2c3c94b992ae59d28cdc58 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 20 Aug 2024 10:43:36 +0200 Subject: [PATCH 1/4] 1856-tablename-spel - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index f0b2a42b23..9a7069063b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 84eeb178e0..2d4fc8c172 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 266af71b96..4c7b94a9d0 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3168f9d4f6..e7d73a2458 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 0287ece743..b02529a221 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1856-tablename-spel-SNAPSHOT From 22b879555c1232a6d47e3de61212bdd1a1f19dfc Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 20 Aug 2024 10:54:52 +0200 Subject: [PATCH 2/4] Polishing. Fixing a test used for performance reasons. Formatting a test. Removing public modifier. Separating test methods from infrastructure. Original pull request #1863 See #1856 --- .../query/StringBasedJdbcQuery.java | 2 +- .../query/StringBasedJdbcQueryUnitTests.java | 68 +++++++++---------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index ab14f8c0f2..c41031c7a7 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -142,7 +142,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera this.query = queryMethod.getRequiredQuery(); this.spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters()); - this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(queryContext); + this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(query); } @Override diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index 563454646a..d5fabc8f7c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -33,7 +33,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; - import org.springframework.beans.factory.BeanFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; @@ -330,14 +329,13 @@ void appliesConverterToIterable() { @Test // GH-1323 void queryByListOfTuples() { - String[][] tuples = {new String[]{"Albert", "Einstein"}, new String[]{"Richard", "Feynman"}}; + String[][] tuples = { new String[] { "Albert", "Einstein" }, new String[] { "Richard", "Feynman" } }; SqlParameterSource parameterSource = forMethod("findByListOfTuples", List.class) // - .withArguments(Arrays.asList(tuples)) + .withArguments(Arrays.asList(tuples)) // .extractParameterSource(); - assertThat(parameterSource.getValue("tuples")) - .asInstanceOf(LIST) + assertThat(parameterSource.getValue("tuples")).asInstanceOf(LIST) // .containsExactly(tuples); assertThat(parameterSource.getSqlType("tuples")).isEqualTo(JdbcUtil.TYPE_UNKNOWN.getVendorTypeNumber()); @@ -348,12 +346,38 @@ void queryByListOfConvertableTuples() { SqlParameterSource parameterSource = forMethod("findByListOfTuples", List.class) // .withCustomConverters(DirectionToIntegerConverter.INSTANCE) // - .withArguments(Arrays.asList(new Object[]{Direction.LEFT, "Einstein"}, new Object[]{Direction.RIGHT, "Feynman"})) + .withArguments( + Arrays.asList(new Object[] { Direction.LEFT, "Einstein" }, new Object[] { Direction.RIGHT, "Feynman" })) .extractParameterSource(); - assertThat(parameterSource.getValue("tuples")) - .asInstanceOf(LIST) - .containsExactly(new Object[][]{new Object[]{-1, "Einstein"}, new Object[]{1, "Feynman"}}); + assertThat(parameterSource.getValue("tuples")).asInstanceOf(LIST) // + .containsExactly(new Object[][] { new Object[] { -1, "Einstein" }, new Object[] { 1, "Feynman" } }); + } + + @Test // GH-619 + void spelCanBeUsedInsideQueries() { + + JdbcQueryMethod queryMethod = createMethod("findBySpelExpression", Object.class); + + List list = new ArrayList<>(); + list.add(new MyEvaluationContextProvider()); + QueryMethodEvaluationContextProvider evaluationContextProviderImpl = new ExtensionAwareQueryMethodEvaluationContextProvider( + list); + + StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, + evaluationContextProviderImpl); + + ArgumentCaptor paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + + sut.execute(new Object[] { "myValue" }); + + verify(this.operations).queryForObject(query.capture(), paramSource.capture(), any(RowMapper.class)); + + assertThat(query.getValue()) + .isEqualTo("SELECT * FROM table WHERE c = :__$synthetic$__1 AND c2 = :__$synthetic$__2"); + assertThat(paramSource.getValue().getValue("__$synthetic$__1")).isEqualTo("test-value1"); + assertThat(paramSource.getValue().getValue("__$synthetic$__2")).isEqualTo("test-value2"); } QueryFixture forMethod(String name, Class... paramTypes) { @@ -486,32 +510,6 @@ interface MyRepository extends Repository { Object findByListOfTuples(@Param("tuples") List tuples); } - @Test // GH-619 - public void spelCanBeUsedInsideQueries() { - - JdbcQueryMethod queryMethod = createMethod("findBySpelExpression", Object.class); - - List list = new ArrayList<>(); - list.add(new MyEvaluationContextProvider()); - QueryMethodEvaluationContextProvider evaluationContextProviderImpl = new ExtensionAwareQueryMethodEvaluationContextProvider( - list); - - StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, - evaluationContextProviderImpl); - - ArgumentCaptor paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); - ArgumentCaptor query = ArgumentCaptor.forClass(String.class); - - sut.execute(new Object[] { "myValue" }); - - verify(this.operations).queryForObject(query.capture(), paramSource.capture(), any(RowMapper.class)); - - assertThat(query.getValue()) - .isEqualTo("SELECT * FROM table WHERE c = :__$synthetic$__1 AND c2 = :__$synthetic$__2"); - assertThat(paramSource.getValue().getValue("__$synthetic$__1")).isEqualTo("test-value1"); - assertThat(paramSource.getValue().getValue("__$synthetic$__2")).isEqualTo("test-value2"); - } - private static class CustomRowMapper implements RowMapper { @Override From f87866ce60052b9a33404ad6ec51ba78f23cc10f Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 20 Aug 2024 11:08:08 +0200 Subject: [PATCH 3/4] 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 Originial pull request #1863 --- .../query/StringBasedJdbcQuery.java | 29 +++- .../support/JdbcQueryLookupStrategy.java | 14 +- .../DeclaredQueryRepositoryUnitTests.java | 142 ++++++++++++++++++ .../DefaultReactiveDataAccessStrategy.java | 9 ++ .../core/ReactiveDataAccessStrategy.java | 10 ++ .../support/R2dbcRepositoryFactory.java | 31 +++- .../r2dbc/documentation/PersonRepository.java | 7 +- ...SqlInspectingR2dbcRepositoryUnitTests.java | 95 ++++++++++++ .../repository/query/QueryPreprocessor.java | 29 ++++ .../query/TableNameQueryPreprocessor.java | 74 +++++++++ .../TableNameQueryPreprocessorUnitTests.java | 41 +++++ .../ROOT/pages/jdbc/query-methods.adoc | 17 ++- .../ROOT/pages/r2dbc/query-methods.adoc | 24 ++- 13 files changed, 504 insertions(+), 18 deletions(-) create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java create mode 100644 spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/QueryPreprocessor.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index c41031c7a7..cf023d11f3 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -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; @@ -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); @@ -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); @@ -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); } @@ -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(); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 0a4ee14768..46e0effdea 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -29,6 +29,8 @@ 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; @@ -36,6 +38,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.core.sql.SqlIdentifier; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; @@ -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)); } + QueryPreprocessor queryPreprocessor = prepareQueryPreprocessor(repositoryMetadata); StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper, - getConverter(), evaluationContextProvider); + getConverter(), evaluationContextProvider, + queryPreprocessor); query.setBeanFactory(getBeanFactory()); return query; } @@ -165,6 +170,13 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository 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()); + } } /** diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java new file mode 100644 index 0000000000..59aafd8a70 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java @@ -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 queryCaptor = ArgumentCaptor.forClass(String.class); + verify(operations).queryForObject(queryCaptor.capture(), any(SqlParameterSource.class), any(RowMapper.class)); + return queryCaptor.getValue(); + } + + private @NotNull T repository(Class 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 { + + @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 { + + @Nullable + @Query("select * from #{#tableName}") + DummyEntity tableNameQuery(); + + @Nullable + @Query("select * from #{#qualifiedTableName}") + DummyEntity qualifiedTableNameQuery(); + } +} diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index c2be43faaf..e393af023b 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -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; @@ -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); } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java index 7520c7d11e..b36002fb90 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java @@ -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; @@ -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. */ diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 376f6054c8..32bbff3c3f 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -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; @@ -51,6 +55,7 @@ * Factory to create {@link R2dbcRepository} instances. * * @author Mark Paluch + * @author Jens Schauder */ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -162,19 +167,29 @@ private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy { public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { + MappingContext, ? 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); } diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/documentation/PersonRepository.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/documentation/PersonRepository.java index 815bf1f03f..e1879f5089 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/documentation/PersonRepository.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/documentation/PersonRepository.java @@ -34,6 +34,11 @@ public interface PersonRepository extends ReactiveCrudRepository // tag::spel[] @Query("SELECT * FROM person WHERE lastname = :#{[0]}") - Flux findByQueryWithExpression(String lastname); + Flux findByQueryWithParameterExpression(String lastname); // end::spel[] + + // tag::spel2[] + @Query("SELECT * FROM #{tableName} WHERE lastname = :lastname") + Flux findByQueryWithExpression(String lastname); + // end::spel2[] } diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java new file mode 100644 index 0000000000..c97c3c7308 --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java @@ -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 + 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 { + @Query("select * from #{#tableName +'x'}") + Mono findBySpel(); + } + + static class Person { + @Id long id; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/QueryPreprocessor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/QueryPreprocessor.java new file mode 100644 index 0000000000..4508e21d7d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/QueryPreprocessor.java @@ -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); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java new file mode 100644 index 0000000000..811edf9dbe --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java @@ -0,0 +1,74 @@ +/* + * 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; + +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.sql.SqlIdentifier; +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; + +public 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); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java new file mode 100644 index 0000000000..3e50f6ff9e --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java @@ -0,0 +1,41 @@ +/* + * 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; + +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; + +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\""); + }); + } +} diff --git a/src/main/antora/modules/ROOT/pages/jdbc/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jdbc/query-methods.adoc index a329071eba..970a36a540 100644 --- a/src/main/antora/modules/ROOT/pages/jdbc/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jdbc/query-methods.adoc @@ -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. diff --git a/src/main/antora/modules/ROOT/pages/r2dbc/query-methods.adoc b/src/main/antora/modules/ROOT/pages/r2dbc/query-methods.adoc index b0f966faad..421aea88d6 100644 --- a/src/main/antora/modules/ROOT/pages/r2dbc/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/r2dbc/query-methods.adoc @@ -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. From c563c2c97d3eafc4abe3751d93586f1e11dc059e Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 3 Sep 2024 10:46:36 +0200 Subject: [PATCH 4/4] Move query extraction into `QueryLookupStrategy`. JdbcStringBasedQuery no longer extracts the query from the `QueryMethod`. The logic of applying table name based SpEL expressions moves into the new base class `RelationalQueryLookupStrategy`. See #1856 Originial pull request #1863 --- .../repository/query/JdbcQueryMethod.java | 2 +- .../query/StringBasedJdbcQuery.java | 17 +++-- .../support/JdbcQueryLookupStrategy.java | 23 +++---- .../DeclaredQueryRepositoryUnitTests.java | 8 +-- .../support/R2dbcRepositoryFactory.java | 22 ++----- .../R2dbcRepositoryFactoryUnitTests.java | 5 ++ ...SqlInspectingR2dbcRepositoryUnitTests.java | 2 +- .../RelationalQueryLookupStrategy.java | 64 +++++++++++++++++++ .../TableNameQueryPreprocessor.java | 10 ++- .../TableNameQueryPreprocessorUnitTests.java | 7 +- 10 files changed, 112 insertions(+), 48 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/RelationalQueryLookupStrategy.java rename spring-data-relational/src/main/java/org/springframework/data/relational/repository/{query => support}/TableNameQueryPreprocessor.java (90%) rename spring-data-relational/src/test/java/org/springframework/data/relational/repository/{query => support}/TableNameQueryPreprocessorUnitTests.java (92%) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java index 0bca96a88f..6747a7d267 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java @@ -128,7 +128,7 @@ String getDeclaredQuery() { return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery(); } - String getRequiredQuery() { + public String getRequiredQuery() { String query = getDeclaredQuery(); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index cf023d11f3..6949ea3681 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -113,7 +113,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, RowMapperFactory rowMapperFactory, JdbcConverter converter, QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod, operations, rowMapperFactory, converter, evaluationContextProvider, QueryPreprocessor.NOOP); + this(queryMethod, operations, rowMapperFactory, converter, evaluationContextProvider, QueryPreprocessor.NOOP.transform(queryMethod.getRequiredQuery())); } /** @@ -125,12 +125,12 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera * @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}. + * @param query * @since 3.4 */ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider, QueryPreprocessor queryPreprocessor) { + RowMapperFactory rowMapperFactory, JdbcConverter converter, + QueryMethodEvaluationContextProvider evaluationContextProvider, String query) { super(queryMethod, operations); @@ -164,10 +164,9 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera .of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat) .withEvaluationContextProvider(evaluationContextProvider); - - this.query = queryPreprocessor.transform(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 @@ -185,7 +184,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(); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 46e0effdea..999481d57b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -29,8 +29,6 @@ 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; @@ -38,7 +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.core.sql.SqlIdentifier; +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; @@ -63,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); @@ -82,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"); @@ -159,10 +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)); } - QueryPreprocessor queryPreprocessor = prepareQueryPreprocessor(repositoryMetadata); + String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery()); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper, - getConverter(), evaluationContextProvider, - queryPreprocessor); + getConverter(), evaluationContextProvider, queryString); query.setBeanFactory(getBeanFactory()); return query; } @@ -170,13 +170,6 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository 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()); - } } /** diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java index 59aafd8a70..6df361b7aa 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/DeclaredQueryRepositoryUnitTests.java @@ -52,7 +52,7 @@ public class DeclaredQueryRepositoryUnitTests { private NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class, RETURNS_DEEP_STUBS); - @Test + @Test // GH-1856 void plainSql() { repository(DummyEntityRepository.class).plainQuery(); @@ -60,7 +60,7 @@ void plainSql() { assertThat(query()).isEqualTo("select * from someTable"); } - @Test + @Test // GH-1856 void tableNameQuery() { repository(DummyEntityRepository.class).tableNameQuery(); @@ -68,7 +68,7 @@ void tableNameQuery() { assertThat(query()).isEqualTo("select * from \"DUMMY_ENTITY\""); } - @Test + @Test // GH-1856 void renamedTableNameQuery() { repository(RenamedEntityRepository.class).tableNameQuery(); @@ -76,7 +76,7 @@ void renamedTableNameQuery() { assertThat(query()).isEqualTo("select * from \"ReNamed\""); } - @Test + @Test // GH-1856 void fullyQualifiedTableNameQuery() { repository(RenamedEntityRepository.class).qualifiedTableNameQuery(); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 32bbff3c3f..d3299a29a7 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -28,14 +28,11 @@ 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.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; @@ -144,8 +141,9 @@ private RelationalEntityInformation getEntityInformation(Class * {@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; @@ -156,11 +154,13 @@ 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 @@ -168,23 +168,15 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) { MappingContext, ? extends RelationalPersistentProperty> mappingContext = this.converter.getMappingContext(); - Dialect dialect = dataAccessStrategy.getDialect(); R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, mappingContext); String namedQueryName = queryMethod.getNamedQueryName(); - 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); + query = evaluateTableExpressions(metadata, query); return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy, diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index 990b3085bc..380b3039b8 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -29,6 +29,7 @@ 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 @@ * 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 void usesMappingRelationalEntityInformationIfMappingContextSet() { @Test public void createsRepositoryWithIdTypeLong() { + when(dataAccessStrategy.getDialect()).thenReturn(AnsiDialect.INSTANCE); + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, dataAccessStrategy); MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java index c97c3c7308..be1927a4dc 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/support/SqlInspectingR2dbcRepositoryUnitTests.java @@ -63,7 +63,7 @@ public void before() { } - @Test + @Test // GH-1856 public void replacesSpelExpressionInQuery() { recorder.addStubbing(SqlInspectingR2dbcRepositoryUnitTests::isSelect, List.of()); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/RelationalQueryLookupStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/RelationalQueryLookupStrategy.java new file mode 100644 index 0000000000..de78eeea58 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/RelationalQueryLookupStrategy.java @@ -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 RelationalPersistentProperty> context; + private final Dialect dialect; + + protected RelationalQueryLookupStrategy( + MappingContext, ? 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); + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/TableNameQueryPreprocessor.java similarity index 90% rename from spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/TableNameQueryPreprocessor.java index 811edf9dbe..d54999f535 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/support/TableNameQueryPreprocessor.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package org.springframework.data.relational.repository.query; +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; @@ -26,7 +27,12 @@ import java.util.regex.Pattern; -public class TableNameQueryPreprocessor implements QueryPreprocessor { +/** + * 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__{"; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/support/TableNameQueryPreprocessorUnitTests.java similarity index 92% rename from spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/repository/support/TableNameQueryPreprocessorUnitTests.java index 3e50f6ff9e..6e657c27f4 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/TableNameQueryPreprocessorUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/support/TableNameQueryPreprocessorUnitTests.java @@ -14,13 +14,18 @@ * limitations under the License. */ -package org.springframework.data.relational.repository.query; +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