diff --git a/pom.xml b/pom.xml index cbbdd63bc2..c57d0287a3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT pom Spring Data Relational Parent @@ -47,6 +47,9 @@ 4.2.0 1.0.1 + + 1.37 + 0.4.0.BUILD-SNAPSHOT 2017 @@ -154,6 +157,98 @@ + + + jmh + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + ${mbr.version} + test + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-source + generate-sources + + add-test-source + + + + src/jmh/java + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + run-benchmarks + pre-integration-test + + exec + + + test + java + + -classpath + + org.openjdk.jmh.Main + .* + + + + + + + + + + jitpack.io + https://jitpack.io + + + diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 271486f02a..738f08166e 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.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index c2f44d3f96..7d5e900055 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReader.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReader.java index 77813ea9b3..4070894735 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReader.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReader.java @@ -18,25 +18,33 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; -import java.util.Map; +import java.util.Optional; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.CriteriaDefinition; +import org.springframework.data.relational.core.query.Query; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.core.sqlgeneration.AliasFactory; import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator; import org.springframework.data.relational.core.sqlgeneration.SqlGenerator; import org.springframework.data.relational.domain.RowDocument; +import org.springframework.data.util.Streamable; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Reads complete Aggregates from the database, by generating appropriate SQL using a {@link SingleQuerySqlGenerator} - * through {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. Results are converterd into an + * through {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. Results are converted into an * intermediate {@link RowDocumentResultSetExtractor RowDocument} and mapped via * {@link org.springframework.data.relational.core.conversion.RelationalConverter#read(Class, RowDocument)}. * @@ -45,12 +53,14 @@ * @author Mark Paluch * @since 3.2 */ -class AggregateReader { +class AggregateReader implements PathToColumnMapping { private final RelationalPersistentEntity aggregate; - private final org.springframework.data.relational.core.sqlgeneration.SqlGenerator sqlGenerator; + private final Table table; + private final SqlGenerator sqlGenerator; private final JdbcConverter converter; private final NamedParameterJdbcOperations jdbcTemplate; + private final AliasFactory aliasFactory; private final RowDocumentResultSetExtractor extractor; AggregateReader(Dialect dialect, JdbcConverter converter, AliasFactory aliasFactory, @@ -59,44 +69,88 @@ class AggregateReader { this.converter = converter; this.aggregate = aggregate; this.jdbcTemplate = jdbcTemplate; + this.table = Table.create(aggregate.getQualifiedTableName()); + this.sqlGenerator = new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, aggregate); + this.aliasFactory = aliasFactory; + this.extractor = new RowDocumentResultSetExtractor(converter.getMappingContext(), this); + } + + @Override + public String column(AggregatePath path) { - this.sqlGenerator = new CachingSqlGenerator( - new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, aggregate)); + String alias = aliasFactory.getColumnAlias(path); + + if (alias == null) { + throw new IllegalStateException(String.format("Alias for '%s' must not be null", path)); + } - this.extractor = new RowDocumentResultSetExtractor(converter.getMappingContext(), - createPathToColumnMapping(aliasFactory)); + return alias; } - public List findAll() { - return jdbcTemplate.query(sqlGenerator.findAll(), this::extractAll); + @Override + public String keyColumn(AggregatePath path) { + return aliasFactory.getKeyAlias(path); } @Nullable public T findById(Object id) { - id = converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation()); + Query query = Query.query(Criteria.where(aggregate.getRequiredIdProperty().getName()).is(id)).limit(1); - return jdbcTemplate.query(sqlGenerator.findById(), Map.of("id", id), this::extractZeroOrOne); + return findOne(query); } - public Iterable findAllById(Iterable ids) { + @Nullable + public T findOne(Query query) { + return doFind(query, this::extractZeroOrOne); + } - List convertedIds = new ArrayList<>(); - for (Object id : ids) { - convertedIds.add(converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation())); - } + public List findAllById(Iterable ids) { + + Collection identifiers = ids instanceof Collection idl ? idl : Streamable.of(ids).toList(); + Query query = Query.query(Criteria.where(aggregate.getRequiredIdProperty().getName()).in(identifiers)); + + return findAll(query); + } + + @SuppressWarnings("ConstantConditions") + public List findAll() { + return jdbcTemplate.query(sqlGenerator.findAll(), this::extractAll); + } + + public List findAll(Query query) { + return doFind(query, this::extractAll); + } + + @SuppressWarnings("ConstantConditions") + private R doFind(Query query, ResultSetExtractor extractor) { + + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + Condition condition = createCondition(query, parameterSource); + String sql = sqlGenerator.findAll(condition); - return jdbcTemplate.query(sqlGenerator.findAllById(), Map.of("ids", convertedIds), this::extractAll); + return jdbcTemplate.query(sql, parameterSource, extractor); + } + + @Nullable + private Condition createCondition(Query query, MapSqlParameterSource parameterSource) { + + QueryMapper queryMapper = new QueryMapper(converter); + + Optional criteria = query.getCriteria(); + return criteria + .map(criteriaDefinition -> queryMapper.getMappedObject(parameterSource, criteriaDefinition, table, aggregate)) + .orElse(null); } /** * Extracts a list of aggregates from the given {@link ResultSet} by utilizing the * {@link RowDocumentResultSetExtractor} and the {@link JdbcConverter}. When used as a method reference this conforms * to the {@link org.springframework.jdbc.core.ResultSetExtractor} contract. - * + * * @param rs the {@link ResultSet} from which to extract the data. Must not be {(}@literal null}. * @return a {@code List} of aggregates, fully converted. - * @throws SQLException + * @throws SQLException on underlying JDBC errors. */ private List extractAll(ResultSet rs) throws SQLException { @@ -114,9 +168,10 @@ private List extractAll(ResultSet rs) throws SQLException { * {@link RowDocumentResultSetExtractor} and the {@link JdbcConverter}. When used as a method reference this conforms * to the {@link org.springframework.jdbc.core.ResultSetExtractor} contract. * - * @param @param rs the {@link ResultSet} from which to extract the data. Must not be {(}@literal null}. - * @return The single instance when the conversion results in exactly one instance. If the {@literal ResultSet} is empty, null is returned. - * @throws SQLException + * @param rs the {@link ResultSet} from which to extract the data. Must not be {(}@literal null}. + * @return The single instance when the conversion results in exactly one instance. If the {@literal ResultSet} is + * empty, null is returned. + * @throws SQLException on underlying JDBC errors. * @throws IncorrectResultSizeDataAccessException when the conversion yields more than one instance. */ @Nullable @@ -134,65 +189,4 @@ private T extractZeroOrOne(ResultSet rs) throws SQLException { return null; } - private PathToColumnMapping createPathToColumnMapping(AliasFactory aliasFactory) { - return new PathToColumnMapping() { - @Override - public String column(AggregatePath path) { - - String alias = aliasFactory.getColumnAlias(path); - Assert.notNull(alias, () -> "alias for >" + path + "< must not be null"); - return alias; - } - - @Override - public String keyColumn(AggregatePath path) { - return aliasFactory.getKeyAlias(path); - } - }; - } - - /** - * A wrapper for the {@link org.springframework.data.relational.core.sqlgeneration.SqlGenerator} that caches the - * generated statements. - * - * @author Jens Schauder - * @since 3.2 - */ - static class CachingSqlGenerator implements org.springframework.data.relational.core.sqlgeneration.SqlGenerator { - - private final org.springframework.data.relational.core.sqlgeneration.SqlGenerator delegate; - - private final String findAll; - private final String findById; - private final String findAllById; - - public CachingSqlGenerator(SqlGenerator delegate) { - - this.delegate = delegate; - - findAll = delegate.findAll(); - findById = delegate.findById(); - findAllById = delegate.findAllById(); - } - - @Override - public String findAll() { - return findAll; - } - - @Override - public String findById() { - return findById; - } - - @Override - public String findAllById() { - return findAllById; - } - - @Override - public AliasFactory getAliasFactory() { - return delegate.getAliasFactory(); - } - } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java index 65d9d3c231..6695647197 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -34,7 +33,6 @@ import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.relational.core.dialect.Dialect; -import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.CriteriaDefinition; @@ -60,7 +58,6 @@ public class QueryMapper { private final JdbcConverter converter; - private final Dialect dialect; private final MappingContext, RelationalPersistentProperty> mappingContext; /** @@ -68,16 +65,29 @@ public class QueryMapper { * * @param dialect must not be {@literal null}. * @param converter must not be {@literal null}. + * @deprecated use {@link QueryMapper(JdbcConverter)} instead. */ - @SuppressWarnings({ "unchecked", "rawtypes" }) + @Deprecated(since="3.2") public QueryMapper(Dialect dialect, JdbcConverter converter) { Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(converter, "JdbcConverter must not be null"); this.converter = converter; - this.dialect = dialect; - this.mappingContext = (MappingContext) converter.getMappingContext(); + this.mappingContext = converter.getMappingContext(); + } + + /** + * Creates a new {@link QueryMapper} with the given {@link JdbcConverter}. + * + * @param converter must not be {@literal null}. + */ + public QueryMapper( JdbcConverter converter) { + + Assert.notNull(converter, "JdbcConverter must not be null"); + + this.converter = converter; + this.mappingContext = converter.getMappingContext(); } /** @@ -295,16 +305,13 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc mappedValue = convertValue(comparator, settableValue.getValue(), propertyField.getTypeHint()); sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue); - } else if (criteria.getValue() instanceof ValueFunction) { + } else if (criteria.getValue() instanceof ValueFunction valueFunction) { - ValueFunction valueFunction = (ValueFunction) criteria.getValue(); - Object value = valueFunction.apply(getEscaper(comparator)); - - mappedValue = convertValue(comparator, value, propertyField.getTypeHint()); + mappedValue = valueFunction.map(v -> convertValue(comparator, v, propertyField.getTypeHint())); sqlType = propertyField.getSqlType(); - } else if (propertyField instanceof MetadataBackedField // - && ((MetadataBackedField) propertyField).property != null // + } else if (propertyField instanceof MetadataBackedField metadataBackedField // + && metadataBackedField.property != null // && (criteria.getValue() == null || !criteria.getValue().getClass().isArray())) { RelationalPersistentProperty property = ((MetadataBackedField) propertyField).property; @@ -431,15 +438,6 @@ private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSql return Conditions.nest(condition); } - private Escaper getEscaper(Comparator comparator) { - - if (comparator == Comparator.LIKE || comparator == Comparator.NOT_LIKE) { - return dialect.getLikeEscaper(); - } - - return Escaper.DEFAULT; - } - @Nullable private Object convertValue(Comparator comparator, @Nullable Object value, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java index cb3df05bc9..45b264050d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentResultSetExtractor.java @@ -159,7 +159,7 @@ private class RowDocumentIterator implements Iterator { */ private boolean hasNext; - RowDocumentIterator(RelationalPersistentEntity entity, ResultSet resultSet) throws SQLException { + RowDocumentIterator(RelationalPersistentEntity entity, ResultSet resultSet) { ResultSetAdapter adapter = ResultSetAdapter.INSTANCE; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryDataAccessStrategy.java index 3f43d0652e..d5fc206e0c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryDataAccessStrategy.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.convert; +import java.util.List; import java.util.Optional; import org.springframework.data.domain.Pageable; @@ -56,37 +57,37 @@ public T findById(Object id, Class domainType) { } @Override - public Iterable findAll(Class domainType) { + public List findAll(Class domainType) { return getReader(domainType).findAll(); } @Override - public Iterable findAllById(Iterable ids, Class domainType) { + public List findAllById(Iterable ids, Class domainType) { return getReader(domainType).findAllById(ids); } @Override - public Iterable findAll(Class domainType, Sort sort) { + public List findAll(Class domainType, Sort sort) { throw new UnsupportedOperationException(); } @Override - public Iterable findAll(Class domainType, Pageable pageable) { + public List findAll(Class domainType, Pageable pageable) { throw new UnsupportedOperationException(); } @Override public Optional findOne(Query query, Class domainType) { - return Optional.empty(); + return Optional.ofNullable(getReader(domainType).findOne(query)); } @Override - public Iterable findAll(Query query, Class domainType) { - throw new UnsupportedOperationException(); + public List findAll(Query query, Class domainType) { + return getReader(domainType).findAll(query); } @Override - public Iterable findAll(Query query, Class domainType, Pageable pageable) { + public List findAll(Query query, Class domainType, Pageable pageable) { throw new UnsupportedOperationException(); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java index bc93cd09dd..9628588f7a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java @@ -16,9 +16,11 @@ package org.springframework.data.jdbc.core.convert; import java.util.Collections; +import java.util.Optional; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Query; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.util.Assert; @@ -85,13 +87,38 @@ public Iterable findAllById(Iterable ids, Class domainType) { return super.findAllById(ids, domainType); } + @Override + public Optional findOne(Query query, Class domainType) { + + if (isSingleSelectQuerySupported(domainType) && isSingleSelectQuerySupported(query)) { + return singleSelectDelegate.findOne(query, domainType); + } + + return super.findOne(query, domainType); + } + + @Override + public Iterable findAll(Query query, Class domainType) { + + if (isSingleSelectQuerySupported(domainType) && isSingleSelectQuerySupported(query)) { + return singleSelectDelegate.findAll(query, domainType); + } + + return super.findAll(query, domainType); + } + + private static boolean isSingleSelectQuerySupported(Query query) { + return !query.isSorted() && !query.isLimited(); + } + private boolean isSingleSelectQuerySupported(Class entityType) { - return sqlGeneratorSource.getDialect().supportsSingleQueryLoading()// - && entityQualifiesForSingleSelectQuery(entityType); + return converter.getMappingContext().isSingleQueryLoadingEnabled() + && sqlGeneratorSource.getDialect().supportsSingleQueryLoading()// + && entityQualifiesForSingleQueryLoading(entityType); } - private boolean entityQualifiesForSingleSelectQuery(Class entityType) { + private boolean entityQualifiesForSingleQueryLoading(Class entityType) { boolean referenceFound = false; for (PersistentPropertyPath path : converter.getMappingContext() @@ -111,11 +138,6 @@ private boolean entityQualifiesForSingleSelectQuery(Class entityType) { referenceFound = true; } - - // AggregateReferences aren't supported yet - if (property.isAssociation()) { - return false; - } } return true; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 60e69edec7..3058106226 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -113,7 +113,7 @@ class SqlGenerator { this.renderContext = new RenderContextFactory(dialect).createRenderContext(); this.sqlRenderer = SqlRenderer.create(renderContext); this.columns = new Columns(entity, mappingContext, converter); - this.queryMapper = new QueryMapper(dialect, converter); + this.queryMapper = new QueryMapper(converter); this.dialect = dialect; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/EscapingParameterSource.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/EscapingParameterSource.java new file mode 100644 index 0000000000..16bdad90e6 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/EscapingParameterSource.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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.query; + +import org.springframework.data.relational.core.dialect.Escaper; +import org.springframework.data.relational.core.query.ValueFunction; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * This {@link SqlParameterSource} will apply escaping to its values. + * + * @author Jens Schauder + * @since 3.2 + */ +class EscapingParameterSource implements SqlParameterSource { + + private final SqlParameterSource parameterSource; + private final Escaper escaper; + + public EscapingParameterSource(SqlParameterSource parameterSource, Escaper escaper) { + + this.parameterSource = parameterSource; + this.escaper = escaper; + } + + @Override + public boolean hasValue(String paramName) { + return parameterSource.hasValue(paramName); + } + + @Override + public Object getValue(String paramName) throws IllegalArgumentException { + + Object value = parameterSource.getValue(paramName); + if (value instanceof ValueFunction valueFunction) { + return valueFunction.apply(escaper); + } + return value; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index b1cc21571b..7411036ab5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -102,7 +102,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { this.accessor = accessor; this.entityMetadata = entityMetadata; - this.queryMapper = new QueryMapper(dialect, converter); + this.queryMapper = new QueryMapper(converter); this.renderContextFactory = new RenderContextFactory(dialect); this.isSliceQuery = isSliceQuery; this.returnedType = returnedType; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java index b41e2f87f5..b2f5c6ac93 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java @@ -15,12 +15,15 @@ */ package org.springframework.data.jdbc.repository.query; +import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.jdbc.core.namedparam.SqlParameterSource; /** - * Value object encapsulating a query containing named parameters and a{@link SqlParameterSource} to bind the parameters. + * Value object encapsulating a query containing named parameters and a{@link SqlParameterSource} to bind the + * parameters. * * @author Mark Paluch + * @author Jens Schauder * @since 2.0 */ class ParametrizedQuery { @@ -38,8 +41,8 @@ String getQuery() { return query; } - SqlParameterSource getParameterSource() { - return parameterSource; + SqlParameterSource getParameterSource(Escaper escaper) { + return new EscapingParameterSource(parameterSource, escaper); } @Override diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index 01876f0d66..ecdbbc5152 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -126,7 +126,7 @@ public Object execute(Object[] values) { ParametrizedQuery query = createQuery(accessor, processor.getReturnedType()); JdbcQueryExecution execution = getQueryExecution(processor, accessor); - return execution.execute(query.getQuery(), query.getParameterSource()); + return execution.execute(query.getQuery(), query.getParameterSource(dialect.getLikeEscaper())); } private JdbcQueryExecution getQueryExecution(ResultProcessor processor, @@ -164,7 +164,7 @@ private JdbcQueryExecution getQueryExecution(ResultProcessor processor, ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted()); Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(), - countQuery.getParameterSource()); + countQuery.getParameterSource(dialect.getLikeEscaper())); return converter.getConversionService().convert(count, Long.class); }); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index aa790fc854..858a69a791 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.IntStream; @@ -42,6 +43,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; @@ -55,6 +57,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.testing.EnabledOnFeature; import org.springframework.data.jdbc.testing.IntegrationTest; +import org.springframework.data.jdbc.testing.TestClass; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; @@ -64,8 +67,12 @@ import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.relational.core.query.Criteria; +import org.springframework.data.relational.core.query.CriteriaDefinition; +import org.springframework.data.relational.core.query.Query; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; /** * Integration tests for {@link JdbcAggregateTemplate}. @@ -223,6 +230,62 @@ void findAllById() { .containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother")); } + @Test // GH-1601 + void findAllByQuery() { + + template.save(SimpleListParent.of("one", "one_1")); + SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2")); + template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3")); + + CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id)); + Query query = Query.query(criteria); + Iterable reloadedById = template.findAll(query, SimpleListParent.class); + + assertThat(reloadedById).extracting(e -> e.id, e -> e.content.size()).containsExactly(tuple(two.id, 2)); + } + + @Test // GH-1601 + void findOneByQuery() { + + template.save(SimpleListParent.of("one", "one_1")); + SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2")); + template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3")); + + CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id)); + Query query = Query.query(criteria); + Optional reloadedById = template.findOne(query, SimpleListParent.class); + + assertThat(reloadedById).get().extracting(e -> e.id, e -> e.content.size()).containsExactly(two.id, 2); + } + + @Test // GH-1601 + void findOneByQueryNothingFound() { + + template.save(SimpleListParent.of("one", "one_1")); + SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2")); + template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3")); + + CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(4711)); + Query query = Query.query(criteria); + Optional reloadedById = template.findOne(query, SimpleListParent.class); + + assertThat(reloadedById).isEmpty(); + } + + @Test // GH-1601 + void findOneByQueryToManyResults() { + + template.save(SimpleListParent.of("one", "one_1")); + SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2")); + template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3")); + + CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").not(two.id)); + Query query = Query.query(criteria); + + assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class) + .isThrownBy(() -> template.findOne(query, SimpleListParent.class)); + } + @Test // DATAJDBC-112 @EnabledOnFeature(SUPPORTS_QUOTED_IDS) void saveAndLoadAnEntityWithReferencedEntityById() { @@ -1266,6 +1329,29 @@ static class ChildNoId { private String content; } + @SuppressWarnings("unused") + static class SimpleListParent { + + @Id private Long id; + String name; + List content = new ArrayList<>(); + + static SimpleListParent of(String name, String... contents) { + + SimpleListParent parent = new SimpleListParent(); + parent.name = name; + + for (String content : contents) { + + ElementNoId element = new ElementNoId(); + element.content = content; + parent.content.add(element); + } + + return parent; + } + } + @Table("LIST_PARENT") @SuppressWarnings("unused") static class ListParent { @@ -1851,8 +1937,8 @@ static class WithInsertOnly { static class Config { @Bean - Class testClass() { - return JdbcAggregateTemplateIntegrationTests.class; + TestClass testClass() { + return TestClass.of(JdbcAggregateTemplateIntegrationTests.class); } @Bean @@ -1862,9 +1948,11 @@ JdbcAggregateOperations operations(ApplicationEventPublisher publisher, Relation } } + @ContextConfiguration(classes = Config.class) static class JdbcAggregateTemplateIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {} @ActiveProfiles(value = PROFILE_SINGLE_QUERY_LOADING) + @ContextConfiguration(classes = Config.class) static class JdbcAggregateTemplateSingleQueryLoadingIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java index 185c6d24c5..d2526fc9f2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/QueryMapperUnitTests.java @@ -50,7 +50,7 @@ public class QueryMapperUnitTests { JdbcMappingContext context = new JdbcMappingContext(); JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); - QueryMapper mapper = new QueryMapper(PostgresDialect.INSTANCE, converter); + QueryMapper mapper = new QueryMapper(converter); MapSqlParameterSource parameterSource = new MapSqlParameterSource(); @Test // DATAJDBC-318 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 74b92b47a8..8c13c68de6 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -36,6 +36,7 @@ import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.dialect.H2Dialect; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.MappedCollection; @@ -93,7 +94,7 @@ public void createQueryByAggregateReference() throws Exception { softly.assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"HOBBY_REFERENCE\" = :hobby_reference"); - softly.assertThat(query.getParameterSource().getValue("hobby_reference")).isEqualTo("twentythree"); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("hobby_reference")).isEqualTo("twentythree"); }); } @@ -112,8 +113,8 @@ void createQueryWithPessimisticWriteLock() throws Exception { softly.assertThat(query.getQuery().toUpperCase()).endsWith("FOR UPDATE"); - softly.assertThat(query.getParameterSource().getValue("first_name")).isEqualTo(firstname); - softly.assertThat(query.getParameterSource().getValue("last_name")).isEqualTo(lastname); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("first_name")).isEqualTo(firstname); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("last_name")).isEqualTo(lastname); }); } @@ -133,8 +134,8 @@ void createQueryWithPessimisticReadLock() throws Exception { // this is also for update since h2 dialect does not distinguish between lockmodes softly.assertThat(query.getQuery().toUpperCase()).endsWith("FOR UPDATE"); - softly.assertThat(query.getParameterSource().getValue("first_name")).isEqualTo(firstname); - softly.assertThat(query.getParameterSource().getValue("age")).isEqualTo(age); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("first_name")).isEqualTo(firstname); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("age")).isEqualTo(age); }); } @@ -165,7 +166,7 @@ public void createQueryForQueryByAggregateReference() throws Exception { softly.assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"HOBBY_REFERENCE\" = :hobby_reference"); - softly.assertThat(query.getParameterSource().getValue("hobby_reference")).isEqualTo("twentythree"); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("hobby_reference")).isEqualTo("twentythree"); }); } @@ -182,7 +183,7 @@ public void createQueryForQueryByAggregateReferenceId() throws Exception { softly.assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"HOBBY_REFERENCE\" = :hobby_reference"); - softly.assertThat(query.getParameterSource().getValue("hobby_reference")).isEqualTo("twentythree"); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("hobby_reference")).isEqualTo("twentythree"); }); } @@ -270,8 +271,8 @@ public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Excepti softly.assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" BETWEEN :date_of_birth AND :date_of_birth1"); - softly.assertThat(query.getParameterSource().getValue("date_of_birth")).isEqualTo(from); - softly.assertThat(query.getParameterSource().getValue("date_of_birth1")).isEqualTo(to); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("date_of_birth")).isEqualTo(from); + softly.assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("date_of_birth1")).isEqualTo(to); }); } @@ -405,7 +406,7 @@ public void appendsLikeOperatorParameterWithPercentSymbolForStartingWithQuery() ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); - assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("Jo%"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("first_name")).isEqualTo("Jo%"); } @Test // DATAJDBC-318 @@ -428,7 +429,7 @@ public void prependsLikeOperatorParameterWithPercentSymbolForEndingWithQuery() t ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); - assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%hn"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("first_name")).isEqualTo("%hn"); } @Test // DATAJDBC-318 @@ -451,7 +452,7 @@ public void wrapsLikeOperatorParameterWithPercentSymbolsForContainingQuery() thr ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); - assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%oh%"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("first_name")).isEqualTo("%oh%"); } @Test // DATAJDBC-318 @@ -474,7 +475,7 @@ public void wrapsLikeOperatorParameterWithPercentSymbolsForNotContainingQuery() ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name"); - assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%oh%"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("first_name")).isEqualTo("%oh%"); } @Test // DATAJDBC-318 @@ -638,8 +639,8 @@ public void createsQueryByEmbeddedObject() throws Exception { .contains(TABLE + ".\"USER_STREET\" = :user_street", // " AND ", // TABLE + ".\"USER_CITY\" = :user_city"); - assertThat(query.getParameterSource().getValue("user_street")).isEqualTo("Hello"); - assertThat(query.getParameterSource().getValue("user_city")).isEqualTo("World"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("user_street")).isEqualTo("Hello"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("user_city")).isEqualTo("World"); } @Test // DATAJDBC-318 @@ -653,7 +654,7 @@ public void createsQueryByEmbeddedProperty() throws Exception { String expectedSql = BASE_SELECT + " WHERE " + TABLE + ".\"USER_STREET\" = :user_street"; assertThat(query.getQuery()).isEqualTo(expectedSql); - assertThat(query.getParameterSource().getValue("user_street")).isEqualTo("Hello"); + assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("user_street")).isEqualTo("Hello"); } @Test // DATAJDBC-534 diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql index 8ad4fda2dc..f086a03b5c 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql @@ -6,6 +6,7 @@ DROP TABLE ONE_TO_ONE_PARENT; DROP TABLE ELEMENT_NO_ID; DROP TABLE LIST_PARENT; +DROP TABLE SIMPLE_LIST_PARENT; DROP TABLE BYTE_ARRAY_OWNER; @@ -74,11 +75,18 @@ CREATE TABLE LIST_PARENT "id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); CREATE TABLE ELEMENT_NO_ID ( CONTENT VARCHAR(100), LIST_PARENT_KEY BIGINT, - LIST_PARENT BIGINT + SIMPLE_LIST_PARENT_KEY BIGINT, + LIST_PARENT BIGINT, + SIMPLE_LIST_PARENT BIGINT ); ALTER TABLE ELEMENT_NO_ID ADD FOREIGN KEY (LIST_PARENT) diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index a0aff08ce8..a6e5eabad7 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -32,9 +32,17 @@ CREATE TABLE LIST_PARENT NAME VARCHAR(100) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); + CREATE TABLE element_no_id ( content VARCHAR(100), + SIMPLE_LIST_PARENT_key BIGINT, + SIMPLE_LIST_PARENT INTEGER, LIST_PARENT_key BIGINT, LIST_PARENT INTEGER ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index 4dd1294ab2..dc73899207 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -26,6 +26,11 @@ CREATE TABLE Child_No_Id content VARCHAR(30) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); CREATE TABLE LIST_PARENT ( "id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, @@ -34,6 +39,8 @@ CREATE TABLE LIST_PARENT CREATE TABLE ELEMENT_NO_ID ( CONTENT VARCHAR(100), + SIMPLE_LIST_PARENT_KEY BIGINT, + SIMPLE_LIST_PARENT BIGINT, LIST_PARENT_KEY BIGINT, LIST_PARENT BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 4dd82b9003..4258e7b438 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -31,9 +31,16 @@ CREATE TABLE LIST_PARENT `id4` BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); CREATE TABLE element_no_id ( CONTENT VARCHAR(100), + SIMPLE_LIST_PARENT_key BIGINT, + SIMPLE_LIST_PARENT BIGINT, LIST_PARENT_key BIGINT, LIST_PARENT BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index 880528cdbf..e9a378f49b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -30,14 +30,22 @@ CREATE TABLE Child_No_Id DROP TABLE IF EXISTS element_no_id; DROP TABLE IF EXISTS LIST_PARENT; +DROP TABLE IF EXISTS SIMPLE_LIST_PARENT; CREATE TABLE LIST_PARENT ( [id4] BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); CREATE TABLE element_no_id ( CONTENT VARCHAR(100), + SIMPLE_LIST_PARENT_key BIGINT, + SIMPLE_LIST_PARENT BIGINT, LIST_PARENT_key BIGINT, LIST_PARENT BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 6808c8a912..40e32f1692 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -26,6 +26,11 @@ CREATE TABLE Child_No_Id `content` VARCHAR(30) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); CREATE TABLE LIST_PARENT ( `id4` BIGINT AUTO_INCREMENT PRIMARY KEY, @@ -35,7 +40,9 @@ CREATE TABLE element_no_id ( CONTENT VARCHAR(100), LIST_PARENT_key BIGINT, - LIST_PARENT BIGINT + SIMPLE_LIST_PARENT_key BIGINT, + LIST_PARENT BIGINT, + SIMPLE_LIST_PARENT BIGINT ); CREATE TABLE BYTE_ARRAY_OWNER diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql index 084e5db460..5a5c5baf40 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql @@ -4,6 +4,7 @@ DROP TABLE CHILD_NO_ID CASCADE CONSTRAINTS PURGE; DROP TABLE ONE_TO_ONE_PARENT CASCADE CONSTRAINTS PURGE; DROP TABLE ELEMENT_NO_ID CASCADE CONSTRAINTS PURGE; DROP TABLE LIST_PARENT CASCADE CONSTRAINTS PURGE; +DROP TABLE SIMPLE_LIST_PARENT CASCADE CONSTRAINTS PURGE; DROP TABLE BYTE_ARRAY_OWNER CASCADE CONSTRAINTS PURGE; DROP TABLE CHAIN0 CASCADE CONSTRAINTS PURGE; DROP TABLE CHAIN1 CASCADE CONSTRAINTS PURGE; @@ -64,9 +65,16 @@ CREATE TABLE LIST_PARENT "id4" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, NAME VARCHAR(100) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); CREATE TABLE element_no_id ( CONTENT VARCHAR(100), + SIMPLE_LIST_PARENT_key NUMBER, + SIMPLE_LIST_PARENT NUMBER, LIST_PARENT_key NUMBER, LIST_PARENT NUMBER ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index 0c77c88139..d43b5750b1 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -4,6 +4,7 @@ DROP TABLE ONE_TO_ONE_PARENT; DROP TABLE Child_No_Id; DROP TABLE element_no_id; DROP TABLE "LIST_PARENT"; +DROP TABLE SIMPLE_LIST_PARENT; DROP TABLE "ARRAY_OWNER"; DROP TABLE DOUBLE_LIST_OWNER; DROP TABLE FLOAT_LIST_OWNER; @@ -68,11 +69,19 @@ CREATE TABLE "LIST_PARENT" NAME VARCHAR(100) ); +CREATE TABLE SIMPLE_LIST_PARENT +( + id SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); + CREATE TABLE element_no_id ( content VARCHAR(100), LIST_PARENT_key BIGINT, - "LIST_PARENT" INTEGER + SIMPLE_LIST_PARENT_key BIGINT, + "LIST_PARENT" INTEGER, + SIMPLE_LIST_PARENT INTEGER ); CREATE TABLE "ARRAY_OWNER" diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index a60f8e183a..f46f5fbc29 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java index 3a6a2936ed..0f233cdd64 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java @@ -176,9 +176,8 @@ public Expression getMappedObject(Expression expression, @Nullable RelationalPer return expression; } - if (expression instanceof Column) { + if (expression instanceof Column column) { - Column column = (Column) expression; Field field = createPropertyField(entity, column.getName()); TableLike table = column.getTable(); @@ -186,9 +185,7 @@ public Expression getMappedObject(Expression expression, @Nullable RelationalPer return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable; } - if (expression instanceof SimpleFunction) { - - SimpleFunction function = (SimpleFunction) expression; + if (expression instanceof SimpleFunction function) { List arguments = function.getExpressions(); List mappedArguments = new ArrayList<>(arguments.size()); @@ -367,18 +364,15 @@ private Condition mapCondition(CriteriaDefinition criteria, MutableBindings bind Class typeHint; Comparator comparator = criteria.getComparator(); - if (criteria.getValue() instanceof Parameter) { - - Parameter parameter = (Parameter) criteria.getValue(); + if (criteria.getValue()instanceof Parameter parameter) { mappedValue = convertValue(comparator, parameter.getValue(), propertyField.getTypeHint()); typeHint = getTypeHint(mappedValue, actualType.getType(), parameter); - } else if (criteria.getValue() instanceof ValueFunction) { + } else if (criteria.getValue() instanceof ValueFunction valueFunction) { - ValueFunction valueFunction = (ValueFunction) criteria.getValue(); - Object value = valueFunction.apply(getEscaper(comparator)); + mappedValue = valueFunction.map(v -> convertValue(comparator, v, propertyField.getTypeHint())) + .apply(getEscaper(comparator)); - mappedValue = convertValue(comparator, value, propertyField.getTypeHint()); typeHint = actualType.getType(); } else { diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java index 770010dc31..372ed39048 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java @@ -111,18 +111,14 @@ private Assignment getAssignment(SqlIdentifier columnName, Object value, Mutable Object mappedValue; Class typeHint; - if (value instanceof Parameter) { - - Parameter parameter = (Parameter) value; + if (value instanceof Parameter parameter) { mappedValue = convertValue(parameter.getValue(), propertyField.getTypeHint()); typeHint = getTypeHint(mappedValue, actualType.getType(), parameter); - } else if (value instanceof ValueFunction) { - - ValueFunction valueFunction = (ValueFunction) value; + } else if (value instanceof ValueFunction valueFunction) { - mappedValue = convertValue(valueFunction.apply(Escaper.DEFAULT), propertyField.getTypeHint()); + mappedValue = valueFunction.map(v -> convertValue(v, propertyField.getTypeHint())).apply(Escaper.DEFAULT); if (mappedValue == null) { return Assignments.value(column, SQL.nullLiteral()); diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 74f350faa8..1741efa6c2 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.2.0-SNAPSHOT + 3.2.0-1601-where-clause-SNAPSHOT diff --git a/spring-data-relational/src/jmh/java/org/springframework/data/relational/core/sqlgeneration/BenchmarkSettings.java b/spring-data-relational/src/jmh/java/org/springframework/data/relational/core/sqlgeneration/BenchmarkSettings.java new file mode 100644 index 0000000000..439824bf3c --- /dev/null +++ b/spring-data-relational/src/jmh/java/org/springframework/data/relational/core/sqlgeneration/BenchmarkSettings.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2022 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.core.sqlgeneration; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Global benchmark settings. + * + * @author Mark Paluch + */ +@Warmup(iterations = 5, time = 2000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@Fork(value = 1, warmups = 0) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public abstract class BenchmarkSettings { + +} diff --git a/spring-data-relational/src/jmh/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorBenchmark.java b/spring-data-relational/src/jmh/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorBenchmark.java new file mode 100644 index 0000000000..f8a1399b5a --- /dev/null +++ b/spring-data-relational/src/jmh/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorBenchmark.java @@ -0,0 +1,66 @@ +/* + * Copyright 2023 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.core.sqlgeneration; + +import jmh.mbr.junit5.Microbenchmark; + +import java.util.List; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.dialect.PostgresDialect; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; + +/** + * Benchmark for {@link SingleQuerySqlGenerator}. + * + * @author Mark Paluch + */ +@Microbenchmark +public class SingleQuerySqlGeneratorBenchmark extends BenchmarkSettings { + + @Benchmark + public String findAll(StateHolder state) { + return new SingleQuerySqlGenerator(state.context, state.aliasFactory, PostgresDialect.INSTANCE, + state.persistentEntity).findAll(null); + } + + @State(Scope.Benchmark) + public static class StateHolder { + + RelationalMappingContext context = new RelationalMappingContext(); + + RelationalPersistentEntity persistentEntity; + + AliasFactory aliasFactory = new AliasFactory(); + + @Setup + public void setup() { + persistentEntity = context.getRequiredPersistentEntity(SingleReferenceAggregate.class); + } + } + + record TrivialAggregate(@Id Long id, String name) { + } + + record SingleReferenceAggregate(@Id Long id, String name, List trivials) { + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index f924ba9d39..015857e985 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -20,6 +20,7 @@ import java.util.Objects; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -38,6 +39,10 @@ class DefaultAggregatePath implements AggregatePath { private final @Nullable PersistentPropertyPath path; + private final Lazy tableInfo = Lazy.of(() -> TableInfo.of(this)); + + private final Lazy columnInfo = Lazy.of(() -> ColumnInfo.of(this)); + @SuppressWarnings("unchecked") DefaultAggregatePath(RelationalMappingContext context, PersistentPropertyPath path) { @@ -189,14 +194,24 @@ private AggregatePath getTableOwningAncestor() { return AggregatePathTraversal.getTableOwningPath(this); } + /** + * Creates an {@link Iterator} that iterates over the current path and all ancestors. It will start with the current + * path, followed by its parent until ending with the root. + */ @Override - public String toString() { - return "AggregatePath[" - + (rootType == null ? path.getBaseProperty().getOwner().getType().getName() : rootType.getName()) + "]" - + ((isRoot()) ? "/" : path.toDotPath()); + public Iterator iterator() { + return new AggregatePathIterator(this); } + @Override + public TableInfo getTableInfo() { + return this.tableInfo.get(); + } + @Override + public ColumnInfo getColumnInfo() { + return this.columnInfo.get(); + } @Override public boolean equals(Object o) { @@ -215,13 +230,12 @@ public int hashCode() { return Objects.hash(context, rootType, path); } - /** - * Creates an {@link Iterator} that iterates over the current path and all ancestors. It will start with the current - * path, followed by its parent until ending with the root. - */ + @Override - public Iterator iterator() { - return new AggregatePathIterator(this); + public String toString() { + return "AggregatePath[" + + (rootType == null ? path.getBaseProperty().getOwner().getType().getName() : rootType.getName()) + "]" + + ((isRoot()) ? "/" : path.toDotPath()); } private static class AggregatePathIterator implements Iterator { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java index cd6908174e..8951ac2a81 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/ValueFunction.java @@ -56,4 +56,20 @@ default Supplier toSupplier(Escaper escaper) { return () -> apply(escaper); } + + /** + * Return a new ValueFunction applying the given mapping {@link Function}. The mapping function is applied after + * applying {@link Escaper}. + * + * @param mapper the mapping function to apply to the value. + * @param the type of the value returned from the mapping function. + * @return a new {@literal ValueFunction}. + * @since 3.2 + */ + default ValueFunction map(Function mapper) { + + Assert.notNull(mapper, "Mapping function must not be null"); + + return escaper -> mapper.apply(this.apply(escaper)); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java index 204c741187..e239521488 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; import java.util.OptionalLong; +import java.util.function.Consumer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -92,15 +93,17 @@ public void visit(Visitor visitor) { Assert.notNull(visitor, "Visitor must not be null"); + Consumer action = it -> it.visit(visitor); + visitor.enter(this); selectList.visit(visitor); from.visit(visitor); - joins.forEach(it -> it.visit(visitor)); + joins.forEach(action); visitIfNotNull(where, visitor); - orderBy.forEach(it -> it.visit(visitor)); + orderBy.forEach(action); visitor.leave(this); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java index e06da61327..08ca542af2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelectBuilder.java @@ -200,11 +200,14 @@ public SelectLock lock(LockMode lockMode) { } @Override - public Select build() { + public Select build(boolean validate) { DefaultSelect select = new DefaultSelect(distinct, selectList, from, limit, offset, joins, where, orderBy, lockMode); - SelectValidator.validate(select); + + if (validate) { + SelectValidator.validate(select); + } return select; } @@ -359,9 +362,9 @@ public SelectLock lock(LockMode lockMode) { } @Override - public Select build() { + public Select build(boolean validate) { selectBuilder.join(finishJoin()); - return selectBuilder.build(); + return selectBuilder.build(validate); } } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java index 140eb7ad14..8f6c8c2075 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectBuilder.java @@ -297,6 +297,7 @@ interface SelectFromAndJoin * @param offset row offset, zero-based. * @return {@code this} builder. */ + @Override SelectFromAndJoin limitOffset(long limit, long offset); /** @@ -305,6 +306,7 @@ interface SelectFromAndJoin * @param limit rows to read. * @return {@code this} builder. */ + @Override SelectFromAndJoin limit(long limit); /** @@ -313,6 +315,7 @@ interface SelectFromAndJoin * @param offset start offset. * @return {@code this} builder. */ + @Override SelectFromAndJoin offset(long offset); } @@ -331,6 +334,7 @@ interface SelectFromAndJoinCondition * @param offset row offset, zero-based. * @return {@code this} builder. */ + @Override SelectFromAndJoin limitOffset(long limit, long offset); /** @@ -339,6 +343,7 @@ interface SelectFromAndJoinCondition * @param limit rows to read. * @return {@code this} builder. */ + @Override SelectFromAndJoin limit(long limit); /** @@ -347,6 +352,7 @@ interface SelectFromAndJoinCondition * @param offset start offset. * @return {@code this} builder. */ + @Override SelectFromAndJoin offset(long offset); } @@ -488,11 +494,11 @@ interface SelectJoin extends SelectLock, BuildSelect { SelectOn leftOuterJoin(TableLike table); /** - * Declar a join, where the join type ({@code INNER}, {@code LEFT OUTER}, {@code RIGHT OUTER}, {@code FULL OUTER}) + * Declare a join, where the join type ({@code INNER}, {@code LEFT OUTER}, {@code RIGHT OUTER}, {@code FULL OUTER}) * is specified by an extra argument. - * + * * @param table the table to join. Must not be {@literal null}. - * @param joinType the type of joi. Must not be {@literal null}. + * @param joinType the type of join. Must not be {@literal null}. * @return {@code this} builder. */ SelectOn join(TableLike table, Join.JoinType joinType); @@ -577,8 +583,20 @@ interface BuildSelect { * Build the {@link Select} statement and verify basic relationship constraints such as all referenced columns have * a {@code FROM} or {@code JOIN} table import. * - * @return the build and immutable {@link Select} statement. + * @return the built and immutable {@link Select} statement. + */ + default Select build() { + return build(true); + } + + /** + * Build the {@link Select} statement. + * + * @param validate whether to validate the generated select by checking basic relationship constraints such as all + * referenced columns have a {@code FROM} or {@code JOIN} table import. + * @return the built and immutable {@link Select} statement. + * @since 3.2 */ - Select build(); + Select build(boolean validate); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java index 26d1e9d178..23d7d0e47e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TypedSubtreeVisitor.java @@ -21,6 +21,7 @@ import org.springframework.data.relational.core.sql.Visitable; import org.springframework.data.relational.core.sql.Visitor; import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; /** * Type-filtering {@link DelegatingVisitor visitor} applying a {@link Class type filter} derived from the generic type @@ -37,28 +38,36 @@ * {@link Visitable}. * *

- * + * * @author Mark Paluch * @since 1.1 * @see FilteredSubtreeVisitor */ abstract class TypedSubtreeVisitor extends DelegatingVisitor { + private static final ConcurrentReferenceHashMap, ResolvableType> refCache = new ConcurrentReferenceHashMap<>(); + private static final ConcurrentReferenceHashMap, Assignable> assignable = new ConcurrentReferenceHashMap<>(); + private final ResolvableType type; private @Nullable Visitable currentSegment; + enum Assignable { + YES, NO, + } + /** * Creates a new {@link TypedSubtreeVisitor}. */ TypedSubtreeVisitor() { - this.type = ResolvableType.forClass(getClass()).as(TypedSubtreeVisitor.class).getGeneric(0); + this.type = refCache.computeIfAbsent(this.getClass(), + key -> ResolvableType.forClass(key).as(TypedSubtreeVisitor.class).getGeneric(0)); } /** * Creates a new {@link TypedSubtreeVisitor} with an explicitly provided type. */ - TypedSubtreeVisitor(Class type) { - this.type = ResolvableType.forType(type); + TypedSubtreeVisitor(Class type) { + this.type = refCache.computeIfAbsent(type, key -> ResolvableType.forClass(type)); } /** @@ -117,7 +126,7 @@ public final Delegation doEnter(Visitable segment) { if (currentSegment == null) { - if (this.type.isInstance(segment)) { + if (isAssignable(this.type, segment)) { currentSegment = segment; return enterMatched((T) segment); @@ -142,4 +151,16 @@ public final Delegation doLeave(Visitable segment) { return leaveNested(segment); } } + + private static boolean isAssignable(ResolvableType type, Visitable segment) { + + Assignable assignable = TypedSubtreeVisitor.assignable.get(segment.getClass()); + + if (assignable == null) { + assignable = type.isInstance(segment) ? Assignable.YES : Assignable.NO; + TypedSubtreeVisitor.assignable.put(segment.getClass(), assignable); + } + + return assignable == Assignable.YES; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java index 5bb11e4b81..9326a55f1f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java @@ -21,6 +21,7 @@ import java.util.Map; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; @@ -45,7 +46,6 @@ public class SingleQuerySqlGenerator implements SqlGenerator { private final Dialect dialect; private final AliasFactory aliases; private final RelationalPersistentEntity aggregate; - private final Table table; public SingleQuerySqlGenerator(RelationalMappingContext context, AliasFactory aliasFactory, Dialect dialect, RelationalPersistentEntity aggregate) { @@ -54,47 +54,14 @@ public SingleQuerySqlGenerator(RelationalMappingContext context, AliasFactory al this.aliases = aliasFactory; this.dialect = dialect; this.aggregate = aggregate; - - this.table = Table.create(aggregate.getQualifiedTableName()); } @Override - public String findAll() { - return createSelect(null); - } - - @Override - public String findById() { - - AggregatePath path = getRootIdPath(); - Condition condition = Conditions.isEqual(table.column(path.getColumnInfo().name()), Expressions.just(":id")); - + public String findAll(@Nullable Condition condition) { return createSelect(condition); } - @Override - public String findAllById() { - - AggregatePath path = getRootIdPath(); - Condition condition = Conditions.in(table.column(path.getColumnInfo().name()), Expressions.just(":ids")); - - return createSelect(condition); - } - - /** - * @return The {@link AggregatePath} to the id property of the aggregate root. - */ - private AggregatePath getRootIdPath() { - return context.getAggregatePath(aggregate).append(aggregate.getRequiredIdProperty()); - } - - /** - * Creates a SQL suitable of loading all the data required for constructing complete aggregates. - * - * @param condition a constraint for limiting the aggregates to be loaded. - * @return a {@literal String} containing the generated SQL statement - */ - private String createSelect(Condition condition) { + String createSelect(@Nullable Condition condition) { AggregatePath rootPath = context.getAggregatePath(aggregate); QueryMeta queryMeta = createInlineQuery(rootPath, condition); @@ -137,7 +104,7 @@ private String createSelect(Condition condition) { finalColumns.add(rootIdExpression); Select fullQuery = StatementBuilder.select(finalColumns).from(inlineQuery).orderBy(rootIdExpression, just("rn")) - .build(); + .build(false); return SqlRenderer.create(new RenderContextFactory(dialect).createRenderContext()).render(fullQuery); } @@ -151,7 +118,7 @@ private InlineQuery createMainSelect(List columns, AggregatePath roo select = applyJoins(rootPath, inlineQueries, select); SelectBuilder.BuildSelect buildSelect = applyWhereCondition(rootPath, inlineQueries, select); - Select mainSelect = buildSelect.build(); + Select mainSelect = buildSelect.build(false); return InlineQuery.create(mainSelect, "main"); } @@ -167,7 +134,7 @@ private List createInlineQueries(PersistentPropertyPaths inlineQueries = new ArrayList<>(); - for (PersistentPropertyPath ppp : paths) { + for (PersistentPropertyPath ppp : paths) { QueryMeta queryMeta = createInlineQuery(context.getAggregatePath(ppp), null); inlineQueries.add(queryMeta); @@ -187,7 +154,7 @@ private List createInlineQueries(PersistentPropertyPaths entity = basePath.getRequiredLeafEntity(); Table table = Table.create(entity.getQualifiedTableName()); @@ -248,7 +215,7 @@ private QueryMeta createInlineQuery(AggregatePath basePath, Condition condition) SelectBuilder.BuildSelect buildSelect = condition != null ? select.where(condition) : select; - InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(), + InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(false), aliases.getTableAlias(context.getAggregatePath(entity))); return QueryMeta.of(basePath, inlineQuery, columnAliases, just(id), just(backReferenceAlias), just(keyAlias), just(rowNumberAlias), just(rowCountAlias)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SqlGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SqlGenerator.java index 78049657e0..fe783882a5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SqlGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SqlGenerator.java @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.sqlgeneration; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.lang.Nullable; + /** * Generates SQL statements for loading aggregates. * @@ -22,11 +25,12 @@ * @since 3.2 */ public interface SqlGenerator { - String findAll(); - String findById(); + default String findAll() { + return findAll(null); + } - String findAllById(); + String findAll(@Nullable Condition condition); AliasFactory getAliasFactory(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java index 818fa8578f..2f781e89c3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java @@ -137,16 +137,12 @@ protected Object prepareParameterValue(@Nullable Object value, Class valueTyp return value; } - switch (partType) { - case STARTING_WITH: - return (ValueFunction) escaper -> escaper.escape(value.toString()) + "%"; - case ENDING_WITH: - return (ValueFunction) escaper -> "%" + escaper.escape(value.toString()); - case CONTAINING: - case NOT_CONTAINING: - return (ValueFunction) escaper -> "%" + escaper.escape(value.toString()) + "%"; - default: - return value; - } + return switch (partType) { + case STARTING_WITH -> (ValueFunction) escaper -> escaper.escape(value.toString()) + "%"; + case ENDING_WITH -> (ValueFunction) escaper -> "%" + escaper.escape(value.toString()); + case CONTAINING, NOT_CONTAINING -> (ValueFunction) escaper -> "%" + escaper.escape(value.toString()) + + "%"; + default -> value; + }; } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifierUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifierUnitTests.java index 5742a2c4ed..bb62ab7b91 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifierUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifierUnitTests.java @@ -16,7 +16,6 @@ package org.springframework.data.relational.core.mapping; import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.*; import org.junit.jupiter.api.Test; import org.springframework.data.relational.core.sql.IdentifierProcessing; @@ -44,7 +43,6 @@ public void quotedSimpleObjectIdentifierWithAdjustableLetterCasing() { assertThat(identifier.toSql(BRACKETS_LOWER_CASE)).isEqualTo("[somename]"); assertThat(identifier.getReference(BRACKETS_LOWER_CASE)).isEqualTo("someName"); assertThat(identifier.getReference()).isEqualTo("someName"); - } @Test // DATAJDBC-386 @@ -77,12 +75,12 @@ public void equality() { SqlIdentifier notSimple = SqlIdentifier.from(new DerivedSqlIdentifier("simple", false), new DerivedSqlIdentifier("not", false)); - assertSoftly(softly -> { + assertThat(basis).isEqualTo(equal).isEqualTo(SqlIdentifier.unquoted("simple")) + .hasSameHashCodeAs(SqlIdentifier.unquoted("simple")); + assertThat(equal).isEqualTo(basis); + assertThat(basis).isNotEqualTo(quoted); + assertThat(basis).isNotEqualTo(notSimple); - softly.assertThat(basis).isEqualTo(equal); - softly.assertThat(equal).isEqualTo(basis); - softly.assertThat(basis).isNotEqualTo(quoted); - softly.assertThat(basis).isNotEqualTo(notSimple); - }); + assertThat(quoted).isEqualTo(SqlIdentifier.quoted("SIMPLE")).hasSameHashCodeAs(SqlIdentifier.quoted("SIMPLE")); } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java index 5721ce2b42..ade6e0dad1 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.relational.core.sqlgeneration; import static org.springframework.data.relational.core.sqlgeneration.SqlAssert.*; @@ -28,7 +27,10 @@ import org.springframework.data.relational.core.dialect.PostgresDialect; import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.Conditions; +import org.springframework.data.relational.core.sql.Table; /** * Tests for {@link SingleQuerySqlGenerator}. @@ -76,7 +78,8 @@ void createSelectForFindAll() { @Test // GH-1446 void createSelectForFindById() { - String sql = sqlGenerator.findById(); + Table table = Table.create(persistentEntity.getQualifiedTableName()); + String sql = sqlGenerator.findAll(table.column("id").isEqualTo(Conditions.just(":id"))); SqlAssert baseSelect = assertThatParsed(sql).hasInlineView(); @@ -94,13 +97,14 @@ void createSelectForFindById() { col("\"id\"").as(alias("id")), // col("\"name\"").as(alias("name")) // ) // - .extractWhereClause().isEqualTo("\"trivial_aggregate\".\"id\" = :id"); + .extractWhereClause().isEqualTo("\"trivial_aggregate\".id = :id"); } @Test // GH-1446 void createSelectForFindAllById() { - String sql = sqlGenerator.findAllById(); + Table table = Table.create(persistentEntity.getQualifiedTableName()); + String sql = sqlGenerator.findAll(table.column("id").in(Conditions.just(":ids"))); SqlAssert baseSelect = assertThatParsed(sql).hasInlineView(); @@ -118,7 +122,7 @@ void createSelectForFindAllById() { col("\"id\"").as(alias("id")), // col("\"name\"").as(alias("name")) // ) // - .extractWhereClause().isEqualTo("\"trivial_aggregate\".\"id\" IN (:ids)"); + .extractWhereClause().isEqualTo("\"trivial_aggregate\".id IN (:ids)"); } } @@ -133,7 +137,8 @@ private AggregateWithSingleReference() { @Test // GH-1446 void createSelectForFindById() { - String sql = sqlGenerator.findById(); + Table table = Table.create(persistentEntity.getQualifiedTableName()); + String sql = sqlGenerator.findAll(table.column("id").isEqualTo(Conditions.just(":id"))); String rootRowNumber = rnAlias(); String rootCount = rcAlias(); @@ -167,7 +172,7 @@ void createSelectForFindById() { col("\"id\"").as(alias("id")), // col("\"name\"").as(alias("name")) // ) // - .extractWhereClause().isEqualTo("\"single_reference_aggregate\".\"id\" = :id"); + .extractWhereClause().isEqualTo("\"single_reference_aggregate\".id = :id"); baseSelect.hasInlineViewSelectingFrom("\"trivial_aggregate\"") // .hasExactlyColumns( // rn(col("\"single_reference_aggregate\"")).as(trivialsRowNumber), // @@ -206,13 +211,14 @@ record SingleReferenceAggregate(@Id Long id, String name, List private class AbstractTestFixture { final Class aggregateRootType; final SingleQuerySqlGenerator sqlGenerator; + final RelationalPersistentEntity persistentEntity; final AliasFactory aliases; private AbstractTestFixture(Class aggregateRootType) { this.aggregateRootType = aggregateRootType; - this.sqlGenerator = new SingleQuerySqlGenerator(context, new AliasFactory(), dialect, - context.getRequiredPersistentEntity(aggregateRootType)); + this.persistentEntity = context.getRequiredPersistentEntity(aggregateRootType); + this.sqlGenerator = new SingleQuerySqlGenerator(context, new AliasFactory(), dialect, persistentEntity); this.aliases = sqlGenerator.getAliasFactory(); }