From d9b548815c116ec43e031b2a08a86f9592ecf560 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 9 Aug 2023 10:27:19 +0200 Subject: [PATCH] Polishing. Extract Single Query Loading branching to SingleQueryFallbackDataAccessStrategy. Inline AggregateReaderFactory into SingleQueryDataAccessStrategy. Move CachingSqlGenerator to AggregateReader as caching root. Introduce DataAccessStrategyFactory to encapsulate configuration. Fix Javadoc tag ordering. Remove superfluous MappingContext parameters when Converter is available. Simplify code. Reformat code. Reorder Functions methods. Tweak Javadoc, move composite function into SingleQuerySqlGenerator. See #1446 See #1450 See #1445 Original pull request: #1572 --- .../jdbc/core/convert/AggregateReader.java | 96 +++++++++----- .../core/convert/AggregateReaderFactory.java | 49 ------- .../convert/AggregateResultSetExtractor.java | 30 ++--- .../jdbc/core/convert/CachingResultSet.java | 2 +- .../jdbc/core/convert/DataAccessStrategy.java | 10 +- .../convert/DataAccessStrategyFactory.java | 82 ++++++++++++ .../convert/DefaultDataAccessStrategy.java | 50 ------- .../core/convert/PathToColumnMapping.java | 7 +- .../convert/ReadingDataAccessStrategy.java | 3 +- .../SingleQueryDataAccessStrategy.java | 28 ++-- ...SingleQueryFallbackDataAccessStrategy.java | 123 ++++++++++++++++++ .../mybatis/MyBatisDataAccessStrategy.java | 14 +- .../config/AbstractJdbcConfiguration.java | 11 +- .../support/JdbcRepositoryFactoryBean.java | 7 +- .../AggregateResultSetExtractorUnitTests.java | 4 +- .../DefaultDataAccessStrategyUnitTests.java | 7 +- ...nableJdbcRepositoriesIntegrationTests.java | 6 +- .../data/jdbc/testing/TestConfiguration.java | 4 +- .../data/relational/core/sql/Functions.java | 96 ++++++++------ .../relational/core/sql/SimpleFunction.java | 8 +- .../core/sqlgeneration/AliasFactory.java | 10 +- .../sqlgeneration/CachingSqlGenerator.java | 62 --------- .../SingleQuerySqlGenerator.java | 57 ++++---- .../core/sqlgeneration/SqlGenerator.java | 4 +- .../SingleQuerySqlGeneratorUnitTests.java | 8 +- 25 files changed, 442 insertions(+), 336 deletions(-) delete mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReaderFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategyFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java delete mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/CachingSqlGenerator.java 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 d34a93dc..8078e5df 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 @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.jdbc.core.convert; import java.util.ArrayList; @@ -24,71 +23,59 @@ import java.util.Map; 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.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.sqlgeneration.AliasFactory; -import org.springframework.data.relational.core.sqlgeneration.CachingSqlGenerator; import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator; +import org.springframework.data.relational.core.sqlgeneration.SqlGenerator; 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} * and a matching {@link AggregateResultSetExtractor} and invoking a * {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} - * + * * @param the type of aggregate produced by this reader. - * @since 3.2 * @author Jens Schauder + * @since 3.2 */ class AggregateReader { - private final RelationalMappingContext mappingContext; private final RelationalPersistentEntity aggregate; - private final AliasFactory aliasFactory; private final org.springframework.data.relational.core.sqlgeneration.SqlGenerator sqlGenerator; private final JdbcConverter converter; private final NamedParameterJdbcOperations jdbcTemplate; + private final AggregateResultSetExtractor extractor; - AggregateReader(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter, + AggregateReader(Dialect dialect, JdbcConverter converter, AliasFactory aliasFactory, NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity aggregate) { - this.mappingContext = mappingContext; - - this.aggregate = aggregate; this.converter = converter; + this.aggregate = aggregate; this.jdbcTemplate = jdbcTemplate; - this.sqlGenerator = new CachingSqlGenerator(new SingleQuerySqlGenerator(mappingContext, dialect, aggregate)); - this.aliasFactory = sqlGenerator.getAliasFactory(); + this.sqlGenerator = new CachingSqlGenerator( + new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, aggregate)); + + this.extractor = new AggregateResultSetExtractor<>(aggregate, converter, createPathToColumnMapping(aliasFactory)); } public List findAll() { - String sql = sqlGenerator.findAll(); - - PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory); - AggregateResultSetExtractor extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter, - pathToColumn); - - Iterable result = jdbcTemplate.query(sql, extractor); + Iterable result = jdbcTemplate.query(sqlGenerator.findAll(), extractor); Assert.state(result != null, "result is null"); return (List) result; } + @Nullable public T findById(Object id) { - PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory); - AggregateResultSetExtractor extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter, - pathToColumn); - - String sql = sqlGenerator.findById(); - id = converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation()); - Iterator result = jdbcTemplate.query(sql, Map.of("id", id), extractor).iterator(); + Iterator result = jdbcTemplate.query(sqlGenerator.findById(), Map.of("id", id), extractor).iterator(); T returnValue = result.hasNext() ? result.next() : null; @@ -101,18 +88,12 @@ class AggregateReader { public Iterable findAllById(Iterable ids) { - PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory); - AggregateResultSetExtractor extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter, - pathToColumn); - - String sql = sqlGenerator.findAllById(); - List convertedIds = new ArrayList<>(); for (Object id : ids) { convertedIds.add(converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation())); } - return jdbcTemplate.query(sql, Map.of("ids", convertedIds), extractor); + return jdbcTemplate.query(sqlGenerator.findAllById(), Map.of("ids", convertedIds), extractor); } private PathToColumnMapping createPathToColumnMapping(AliasFactory aliasFactory) { @@ -121,7 +102,7 @@ class AggregateReader { public String column(AggregatePath path) { String alias = aliasFactory.getColumnAlias(path); - Assert.notNull(alias, () -> "alias for >" + path + " "alias for >" + path + "< must not be null"); return alias; } @@ -131,4 +112,49 @@ class AggregateReader { } }; } + + /** + * A wrapper for the {@link org.springframework.data.relational.core.sqlgeneration.SqlGenerator} that caches the + * generated statements. + * + * @since 3.2 + * @author Jens Schauder + */ + 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/AggregateReaderFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReaderFactory.java deleted file mode 100644 index 98f2eb7c..00000000 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReaderFactory.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.core.convert; - -import org.springframework.data.relational.core.dialect.Dialect; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; - -/** - * Creates {@link AggregateReader} instances. - * - * @since 3.2 - * @author Jens Schauder - */ -class AggregateReaderFactory { - - private final RelationalMappingContext mappingContext; - private final Dialect dialect; - private final JdbcConverter converter; - private final NamedParameterJdbcOperations jdbcTemplate; - - public AggregateReaderFactory(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter, - NamedParameterJdbcOperations jdbcTemplate) { - - this.mappingContext = mappingContext; - this.dialect = dialect; - this.converter = converter; - this.jdbcTemplate = jdbcTemplate; - } - - AggregateReader createAggregateReaderFor(RelationalPersistentEntity entity) { - return new AggregateReader<>(mappingContext, dialect, converter, jdbcTemplate, entity); - } -} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractor.java index 0d9c6efa..8a0f534b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractor.java @@ -48,10 +48,10 @@ import org.springframework.util.Assert; * which looks somewhat how one would represent an aggregate in a single excel table. The first row contains data of the * aggregate root, any single valued reference and the first element of any collection. Following rows do NOT repeat the * aggregate root data but contain data of second elements of any collections. For details see accompanying unit tests. - * + * * @param the type of aggregates to extract - * @since 3.2 * @author Jens Schauder + * @since 3.2 */ class AggregateResultSetExtractor implements ResultSetExtractor> { @@ -61,8 +61,6 @@ class AggregateResultSetExtractor implements ResultSetExtractor> private final PathToColumnMapping propertyToColumn; /** - * @param context the {@link org.springframework.data.mapping.context.MappingContext} providing the metadata for the - * aggregate and its entity. Must not be {@literal null}. * @param rootEntity the aggregate root. Must not be {@literal null}. * @param converter Used for converting objects from the database to whatever is required by the aggregate. Must not * be {@literal null}. @@ -70,17 +68,16 @@ class AggregateResultSetExtractor implements ResultSetExtractor> * column of the {@link ResultSet} that holds the data for that * {@link org.springframework.data.relational.core.mapping.AggregatePath}. */ - AggregateResultSetExtractor(RelationalMappingContext context, RelationalPersistentEntity rootEntity, + AggregateResultSetExtractor(RelationalPersistentEntity rootEntity, JdbcConverter converter, PathToColumnMapping pathToColumn) { - Assert.notNull(context, "context must not be null"); Assert.notNull(rootEntity, "rootEntity must not be null"); Assert.notNull(converter, "converter must not be null"); Assert.notNull(pathToColumn, "propertyToColumn must not be null"); - this.context = context; this.rootEntity = rootEntity; this.converter = converter; + this.context = converter.getMappingContext(); this.propertyToColumn = pathToColumn; } @@ -131,7 +128,7 @@ class AggregateResultSetExtractor implements ResultSetExtractor> /** * A {@link Reader} is responsible for reading a single entity or collection of entities from a set of columns - * + * * @since 3.2 * @author Jens Schauder */ @@ -145,14 +142,14 @@ class AggregateResultSetExtractor implements ResultSetExtractor> /** * Checks if this {@literal Reader} has all the data needed for a complete result, or if it needs to read further * rows. - * + * * @return the result of the check. */ boolean hasResult(); /** * Constructs the result, returns it and resets the state of the reader to read the next instance. - * + * * @return an instance of whatever this {@literal Reader} is supposed to read. */ @Nullable @@ -161,7 +158,7 @@ class AggregateResultSetExtractor implements ResultSetExtractor> /** * Adapts a {@link Map} to the interface of a {@literal Collection>}. - * + * * @since 3.2 * @author Jens Schauder */ @@ -221,7 +218,7 @@ class AggregateResultSetExtractor implements ResultSetExtractor> /** * A {@link Reader} for reading entities. - * + * * @since 3.2 * @author Jens Schauder */ @@ -315,7 +312,7 @@ class AggregateResultSetExtractor implements ResultSetExtractor> /** * A {@link Reader} for reading collections of entities. - * + * * @since 3.2 * @author Jens Schauder */ @@ -413,7 +410,7 @@ class AggregateResultSetExtractor implements ResultSetExtractor> /** * A {@link Reader} for reading collection entries. Most of the work is done by an {@link EntityReader}, but a * additional key column might get read. The result is - * + * * @since 3.2 * @author Jens Schauder */ @@ -459,8 +456,9 @@ class AggregateResultSetExtractor implements ResultSetExtractor> } /** - * A {@link ParameterValueProvider} that provided the values for an entity from a continues set of rows in a {@link ResultSet}. These might be referenced entities or collections of such entities. {@link ResultSet}. - * + * A {@link ParameterValueProvider} that provided the values for an entity from a continues set of rows in a + * {@link ResultSet}. These might be referenced entities or collections of such entities. {@link ResultSet}. + * * @since 3.2 * @author Jens Schauder */ diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CachingResultSet.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CachingResultSet.java index 5b92e21c..363b810c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CachingResultSet.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CachingResultSet.java @@ -26,8 +26,8 @@ import org.springframework.lang.Nullable; * Despite its name not really a {@link ResultSet}, but it offers the part of the {@literal ResultSet} API that is used * by {@link AggregateReader}. It allows peeking in the next row of a ResultSet by caching one row of the ResultSet. * - * @since 3.2 * @author Jens Schauder + * @since 3.2 */ class CachingResultSet { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java index a3abea57..0f685b09 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java @@ -42,7 +42,7 @@ import org.springframework.lang.Nullable; * @author Chirag Tailor * @author Diego Krupitza */ -public interface DataAccessStrategy extends RelationResolver { +public interface DataAccessStrategy extends ReadingDataAccessStrategy, RelationResolver { /** * Inserts the data of a single entity. Referenced entities don't get handled. @@ -238,6 +238,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param the type of the entity. * @return Might return {@code null}. */ + @Override @Nullable T findById(Object id, Class domainType); @@ -248,6 +249,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param the type of entities to load. * @return Guaranteed to be not {@code null}. */ + @Override Iterable findAll(Class domainType); /** @@ -259,6 +261,7 @@ public interface DataAccessStrategy extends RelationResolver { * @param type of entities to load. * @return the loaded entities. Guaranteed to be not {@code null}. */ + @Override Iterable findAllById(Iterable ids, Class domainType); @Override @@ -274,6 +277,7 @@ public interface DataAccessStrategy extends RelationResolver { * @return Guaranteed to be not {@code null}. * @since 2.0 */ + @Override Iterable findAll(Class domainType, Sort sort); /** @@ -285,6 +289,7 @@ public interface DataAccessStrategy extends RelationResolver { * @return Guaranteed to be not {@code null}. * @since 2.0 */ + @Override Iterable findAll(Class domainType, Pageable pageable); /** @@ -296,6 +301,7 @@ public interface DataAccessStrategy extends RelationResolver { * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @since 3.0 */ + @Override Optional findOne(Query query, Class domainType); /** @@ -307,6 +313,7 @@ public interface DataAccessStrategy extends RelationResolver { * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @since 3.0 */ + @Override Iterable findAll(Query query, Class domainType); /** @@ -320,6 +327,7 @@ public interface DataAccessStrategy extends RelationResolver { * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. * @since 3.0 */ + @Override Iterable findAll(Query query, Class domainType, Pageable pageable); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategyFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategyFactory.java new file mode 100644 index 00000000..f80d8c23 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategyFactory.java @@ -0,0 +1,82 @@ +/* + * 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.core.convert; + +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.util.Assert; + +/** + * Factory to create a {@link DataAccessStrategy} based on the configuration of the provided components. Specifically, + * this factory creates a {@link SingleQueryFallbackDataAccessStrategy} that falls back to + * {@link DefaultDataAccessStrategy} if Single Query Loading is not supported. This factory encapsulates + * {@link DataAccessStrategy} for consistent access strategy creation. + * + * @author Mark Paluch + * @since 3.2 + */ +public class DataAccessStrategyFactory { + + private final SqlGeneratorSource sqlGeneratorSource; + private final JdbcConverter converter; + private final NamedParameterJdbcOperations operations; + private final SqlParametersFactory sqlParametersFactory; + private final InsertStrategyFactory insertStrategyFactory; + + /** + * Creates a new {@link DataAccessStrategyFactory}. + * + * @param sqlGeneratorSource must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param sqlParametersFactory must not be {@literal null}. + * @param insertStrategyFactory must not be {@literal null}. + */ + public DataAccessStrategyFactory(SqlGeneratorSource sqlGeneratorSource, JdbcConverter converter, + NamedParameterJdbcOperations operations, SqlParametersFactory sqlParametersFactory, + InsertStrategyFactory insertStrategyFactory) { + + Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); + Assert.notNull(converter, "JdbcConverter must not be null"); + Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); + Assert.notNull(sqlParametersFactory, "SqlParametersFactory must not be null"); + Assert.notNull(insertStrategyFactory, "InsertStrategyFactory must not be null"); + + this.sqlGeneratorSource = sqlGeneratorSource; + this.converter = converter; + this.operations = operations; + this.sqlParametersFactory = sqlParametersFactory; + this.insertStrategyFactory = insertStrategyFactory; + } + + /** + * Creates a new {@link DataAccessStrategy}. + * + * @return a new {@link DataAccessStrategy}. + */ + public DataAccessStrategy create() { + + DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy(sqlGeneratorSource, + this.converter.getMappingContext(), this.converter, this.operations, sqlParametersFactory, + insertStrategyFactory); + + if (this.converter.getMappingContext().isSingleQueryLoadingEnabled()) { + return new SingleQueryFallbackDataAccessStrategy(sqlGeneratorSource, converter, operations, + defaultDataAccessStrategy); + } + + return defaultDataAccessStrategy; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index 307d9baa..e1ad40cf 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -68,7 +68,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { private final NamedParameterJdbcOperations operations; private final SqlParametersFactory sqlParametersFactory; private final InsertStrategyFactory insertStrategyFactory; - private final ReadingDataAccessStrategy singleSelectDelegate; /** * Creates a {@link DefaultDataAccessStrategy} @@ -96,7 +95,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { this.operations = operations; this.sqlParametersFactory = sqlParametersFactory; this.insertStrategyFactory = insertStrategyFactory; - this.singleSelectDelegate = new SingleQueryDataAccessStrategy(context, sqlGeneratorSource.getDialect(), converter, operations); } @Override @@ -261,10 +259,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @Override public T findById(Object id, Class domainType) { - if (isSingleSelectQuerySupported(domainType)) { - return singleSelectDelegate.findById(id, domainType); - } - String findOneSql = sql(domainType).getFindOne(); SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); @@ -277,11 +271,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { @Override public Iterable findAll(Class domainType) { - - if (isSingleSelectQuerySupported(domainType)){ - return singleSelectDelegate.findAll(domainType); - } - return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType)); } @@ -292,10 +281,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { return Collections.emptyList(); } - if (isSingleSelectQuerySupported(domainType)){ - return singleSelectDelegate.findAllById(ids, domainType); - } - SqlParameterSource parameterSource = sqlParametersFactory.forQueryByIds(ids, domainType); String findAllInListSql = sql(domainType).getFindAllInList(); return operations.query(findAllInListSql, parameterSource, getEntityRowMapper(domainType)); @@ -443,39 +428,4 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { return baseProperty.getOwner().getType(); } - private boolean isSingleSelectQuerySupported(Class entityType) { - - return context.isSingleQueryLoadingEnabled() && sqlGeneratorSource.getDialect().supportsSingleQueryLoading()// - && entityQualifiesForSingleSelectQuery(entityType); - } - - private boolean entityQualifiesForSingleSelectQuery(Class entityType) { - - boolean referenceFound = false; - for (PersistentPropertyPath path : context.findPersistentPropertyPaths(entityType, __ -> true)) { - RelationalPersistentProperty property = path.getLeafProperty(); - if (property.isEntity()) { - - // embedded entities are currently not supported - if (property.isEmbedded()) { - return false; - } - - // only a single reference is currently supported - if (referenceFound) { - return false; - } - - 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/PathToColumnMapping.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/PathToColumnMapping.java index b2b2f435..1aaf362e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/PathToColumnMapping.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/PathToColumnMapping.java @@ -17,18 +17,17 @@ package org.springframework.data.jdbc.core.convert; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.AggregatePath; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** * A mapping between {@link PersistentPropertyPath} and column names of a query. Column names are intentionally * represented by {@link String} values, since this is what a {@link java.sql.ResultSet} uses, and since all the query * columns should be aliases there is no need for quoting or similar as provided by * {@link org.springframework.data.relational.core.sql.SqlIdentifier}. - * - * @since 3.2 + * * @author Jens Schauder + * @since 3.2 */ -public interface PathToColumnMapping { +interface PathToColumnMapping { String column(AggregatePath path); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ReadingDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ReadingDataAccessStrategy.java index e2e7f638..7d86bae1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ReadingDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/ReadingDataAccessStrategy.java @@ -26,10 +26,11 @@ import org.springframework.lang.Nullable; /** * The finding methods of a {@link DataAccessStrategy}. * - * @since 3.2 * @author Jens Schauder + * @since 3.2 */ interface ReadingDataAccessStrategy { + /** * Loads a single entity identified by type and id. * 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 fa8dfc60..3f43d065 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 @@ -24,24 +24,30 @@ import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.query.Query; +import org.springframework.data.relational.core.sqlgeneration.AliasFactory; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.util.ConcurrentLruCache; /** * A {@link ReadingDataAccessStrategy} that uses an {@link AggregateReader} to load entities with a single query. - * - * @since 3.2 + * * @author Jens Schauder + * @author Mark Paluch + * @since 3.2 */ -public class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy { - private final AggregateReaderFactory readerFactory; +class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy { + private final RelationalMappingContext mappingContext; + private final AliasFactory aliasFactory; + private final ConcurrentLruCache, AggregateReader> readerCache; - public SingleQueryDataAccessStrategy(RelationalMappingContext mappingContext, Dialect dialect, - JdbcConverter converter, NamedParameterJdbcOperations jdbcTemplate) { + public SingleQueryDataAccessStrategy(Dialect dialect, JdbcConverter converter, + NamedParameterJdbcOperations jdbcTemplate) { - this.mappingContext = mappingContext; - this.readerFactory = new AggregateReaderFactory(mappingContext, dialect, converter, jdbcTemplate); - ; + this.mappingContext = converter.getMappingContext(); + this.aliasFactory = new AliasFactory(); + this.readerCache = new ConcurrentLruCache<>(256, + entity -> new AggregateReader<>(dialect, converter, aliasFactory, jdbcTemplate, entity)); } @Override @@ -84,10 +90,12 @@ public class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy throw new UnsupportedOperationException(); } + @SuppressWarnings("unchecked") private AggregateReader getReader(Class domainType) { RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) mappingContext .getRequiredPersistentEntity(domainType); - return readerFactory.createAggregateReaderFor(persistentEntity); + + return (AggregateReader) readerCache.get(persistentEntity); } } 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 new file mode 100644 index 00000000..bc93cd09 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java @@ -0,0 +1,123 @@ +/* + * 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.core.convert; + +import java.util.Collections; + +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.util.Assert; + +/** + * {@link DelegatingDataAccessStrategy} applying Single Query Loading if the underlying aggregate type allows Single + * Query Loading. + * + * @author Mark Paluch + * @since 3.2 + */ +class SingleQueryFallbackDataAccessStrategy extends DelegatingDataAccessStrategy { + + private final SqlGeneratorSource sqlGeneratorSource; + private final SingleQueryDataAccessStrategy singleSelectDelegate; + private final JdbcConverter converter; + + public SingleQueryFallbackDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, JdbcConverter converter, + NamedParameterJdbcOperations operations, DataAccessStrategy fallback) { + + super(fallback); + + Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); + Assert.notNull(converter, "JdbcConverter must not be null"); + Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); + + this.sqlGeneratorSource = sqlGeneratorSource; + this.converter = converter; + + this.singleSelectDelegate = new SingleQueryDataAccessStrategy(sqlGeneratorSource.getDialect(), converter, + operations); + } + + @Override + public T findById(Object id, Class domainType) { + + if (isSingleSelectQuerySupported(domainType)) { + return singleSelectDelegate.findById(id, domainType); + } + + return super.findById(id, domainType); + } + + @Override + public Iterable findAll(Class domainType) { + + if (isSingleSelectQuerySupported(domainType)) { + return singleSelectDelegate.findAll(domainType); + } + + return super.findAll(domainType); + } + + @Override + public Iterable findAllById(Iterable ids, Class domainType) { + + if (!ids.iterator().hasNext()) { + return Collections.emptyList(); + } + + if (isSingleSelectQuerySupported(domainType)) { + return singleSelectDelegate.findAllById(ids, domainType); + } + + return super.findAllById(ids, domainType); + } + + private boolean isSingleSelectQuerySupported(Class entityType) { + + return sqlGeneratorSource.getDialect().supportsSingleQueryLoading()// + && entityQualifiesForSingleSelectQuery(entityType); + } + + private boolean entityQualifiesForSingleSelectQuery(Class entityType) { + + boolean referenceFound = false; + for (PersistentPropertyPath path : converter.getMappingContext() + .findPersistentPropertyPaths(entityType, __ -> true)) { + RelationalPersistentProperty property = path.getLeafProperty(); + if (property.isEntity()) { + + // embedded entities are currently not supported + if (property.isEmbedded()) { + return false; + } + + // only a single reference is currently supported + if (referenceFound) { + return false; + } + + 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/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 24c94394..e1c36e7c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -91,28 +91,24 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter); InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(operations, new BatchJdbcOperations(operations.getJdbcOperations()), dialect); - DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy( // + + DataAccessStrategy defaultDataAccessStrategy = new DataAccessStrategyFactory( // sqlGeneratorSource, // - context, // converter, // operations, // sqlParametersFactory, // insertStrategyFactory // - ); + ).create(); // the DefaultDataAccessStrategy needs a reference to the returned DataAccessStrategy. This creates a dependency // cycle. In order to create it, we need something that allows to defer closing the cycle until all the elements are // created. That is the purpose of the DelegatingAccessStrategy. - DelegatingDataAccessStrategy delegatingDataAccessStrategy = new DelegatingDataAccessStrategy( - defaultDataAccessStrategy); MyBatisDataAccessStrategy myBatisDataAccessStrategy = new MyBatisDataAccessStrategy(sqlSession, dialect.getIdentifierProcessing()); myBatisDataAccessStrategy.setNamespaceStrategy(namespaceStrategy); - CascadingDataAccessStrategy cascadingDataAccessStrategy = new CascadingDataAccessStrategy( - asList(myBatisDataAccessStrategy, delegatingDataAccessStrategy)); - - return cascadingDataAccessStrategy; + return new CascadingDataAccessStrategy( + asList(myBatisDataAccessStrategy, new DelegatingDataAccessStrategy(defaultDataAccessStrategy))); } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index ce920a59..fde40ff9 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -101,7 +101,8 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { /** * Register a {@link JdbcMappingContext} and apply an optional {@link NamingStrategy}. * - * @param namingStrategy optional {@link NamingStrategy}. Use {@link org.springframework.data.relational.core.mapping.DefaultNamingStrategy#INSTANCE} as fallback. + * @param namingStrategy optional {@link NamingStrategy}. Use + * {@link org.springframework.data.relational.core.mapping.DefaultNamingStrategy#INSTANCE} as fallback. * @param customConversions see {@link #jdbcCustomConversions()}. * @param jdbcManagedTypes JDBC managed types, typically discovered through {@link #jdbcManagedTypes() an entity * scan}. @@ -204,9 +205,13 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { @Bean public DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations operations, JdbcConverter jdbcConverter, JdbcMappingContext context, Dialect dialect) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, jdbcConverter, dialect), context, - jdbcConverter, operations, new SqlParametersFactory(context, jdbcConverter), + + SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(context, jdbcConverter, dialect); + DataAccessStrategyFactory factory = new DataAccessStrategyFactory(sqlGeneratorSource, jdbcConverter, operations, + new SqlParametersFactory(context, jdbcConverter), new InsertStrategyFactory(operations, new BatchJdbcOperations(operations.getJdbcOperations()), dialect)); + + return factory.create(); } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index f0d5390a..f36ae2f4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -23,7 +23,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.jdbc.core.convert.BatchJdbcOperations; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.convert.DataAccessStrategyFactory; import org.springframework.data.jdbc.core.convert.InsertStrategyFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; @@ -181,8 +181,11 @@ public class JdbcRepositoryFactoryBean, S, ID extend SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(this.mappingContext, this.converter); InsertStrategyFactory insertStrategyFactory = new InsertStrategyFactory(this.operations, new BatchJdbcOperations(this.operations.getJdbcOperations()), this.dialect); - return new DefaultDataAccessStrategy(sqlGeneratorSource, this.mappingContext, this.converter, + + DataAccessStrategyFactory factory = new DataAccessStrategyFactory(sqlGeneratorSource, this.converter, this.operations, sqlParametersFactory, insertStrategyFactory); + + return factory.create(); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractorUnitTests.java index f20ac36b..07fe8b5c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateResultSetExtractorUnitTests.java @@ -49,7 +49,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp public class AggregateResultSetExtractorUnitTests { RelationalMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy()); - private final JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); + JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); private final PathToColumnMapping column = new PathToColumnMapping() { @Override @@ -136,7 +136,7 @@ public class AggregateResultSetExtractorUnitTests { @NotNull private AggregateResultSetExtractor getExtractor(Class type) { - return (AggregateResultSetExtractor) new AggregateResultSetExtractor<>(context, + return (AggregateResultSetExtractor) new AggregateResultSetExtractor<>( (RelationalPersistentEntity) context.getPersistentEntity(type), converter, column); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java index ddbd1922..076e877f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategyUnitTests.java @@ -52,7 +52,7 @@ class DefaultDataAccessStrategyUnitTests { private InsertStrategyFactory insertStrategyFactory = mock(InsertStrategyFactory.class); private JdbcConverter converter; - private DefaultDataAccessStrategy accessStrategy; + private DataAccessStrategy accessStrategy; @BeforeEach void before() { @@ -61,13 +61,12 @@ class DefaultDataAccessStrategyUnitTests { Dialect dialect = HsqlDbDialect.INSTANCE; converter = new BasicJdbcConverter(context, relationResolver, new JdbcCustomConversions(), new DefaultJdbcTypeFactory(jdbcOperations), dialect.getIdentifierProcessing()); - accessStrategy = new DefaultDataAccessStrategy( // + accessStrategy = new DataAccessStrategyFactory( // new SqlGeneratorSource(context, converter, dialect), // - context, // converter, // namedJdbcOperations, // sqlParametersFactory, // - insertStrategyFactory); + insertStrategyFactory).create(); relationResolver.setDelegate(accessStrategy); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index dc5db486..0a6bfaff 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -35,7 +35,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.JdbcAggregateTemplate; import org.springframework.data.jdbc.core.convert.BatchJdbcOperations; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; -import org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy; +import org.springframework.data.jdbc.core.convert.DataAccessStrategyFactory; import org.springframework.data.jdbc.core.convert.InsertStrategyFactory; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; @@ -172,9 +172,9 @@ public class EnableJdbcRepositoriesIntegrationTests { DataAccessStrategy defaultDataAccessStrategy( @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, converter, dialect), context, converter, + return new DataAccessStrategyFactory(new SqlGeneratorSource(context, converter, dialect), converter, template, new SqlParametersFactory(context, converter), - new InsertStrategyFactory(template, new BatchJdbcOperations(template.getJdbcOperations()), dialect)); + new InsertStrategyFactory(template, new BatchJdbcOperations(template.getJdbcOperations()), dialect)).create(); } @Bean diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 4b069217..c6d84cf0 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -102,9 +102,9 @@ public class TestConfiguration { @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { - return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, converter, dialect), context, converter, + return new DataAccessStrategyFactory(new SqlGeneratorSource(context, converter, dialect), converter, template, new SqlParametersFactory(context, converter), - new InsertStrategyFactory(template, new BatchJdbcOperations(template.getJdbcOperations()), dialect)); + new InsertStrategyFactory(template, new BatchJdbcOperations(template.getJdbcOperations()), dialect)).create(); } @Bean diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java index 158e5415..1c6ce033 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Functions.java @@ -35,11 +35,25 @@ import org.springframework.util.Assert; */ public class Functions { + // Utility constructor. + private Functions() {} + + /** + * Creates a new {@code COALESCE} function. + * + * @param expressions expressions to apply {@code COALESCE}, must not be {@literal null}. + * @return the new {@link SimpleFunction COALESCE function} for {@code expression}. + * @since 3.2 + */ + public static SimpleFunction coalesce(Expression... expressions) { + return SimpleFunction.create("COALESCE", Arrays.asList(expressions)); + } + /** * Creates a new {@code COUNT} function. * - * @param columns columns to apply count, must not be {@literal null}. - * @return the new {@link SimpleFunction count function} for {@code columns}. + * @param columns columns to apply {@code COUNT}, must not be {@literal null}. + * @return the new {@link SimpleFunction COUNT function} for {@code columns}. */ public static SimpleFunction count(Expression... columns) { @@ -49,32 +63,11 @@ public class Functions { return SimpleFunction.create("COUNT", Arrays.asList(columns)); } - public static SimpleFunction least(Expression... expressions) { - return SimpleFunction.create("LEAST", Arrays.asList(expressions)); - } - - /** - * Creates a {@literal GREATEST} function with the given arguments. - * @since 3.2 - */ - public static SimpleFunction greatest(Expression... expressions) { - return greatest(Arrays.asList(expressions)); - } - - - /** - * Creates a {@literal GREATEST} function with the given arguments. - * @since 3.2 - */ - public static SimpleFunction greatest(List list) { - return SimpleFunction.create("GREATEST", list); - } - /** * Creates a new {@code COUNT} function. * - * @param columns columns to apply count, must not be {@literal null}. - * @return the new {@link SimpleFunction count function} for {@code columns}. + * @param columns columns to apply {@code COUNT}, must not be {@literal null}. + * @return the new {@link SimpleFunction COUNT function} for {@code columns}. */ public static SimpleFunction count(Collection columns) { @@ -84,24 +77,43 @@ public class Functions { } /** - * Creates a new {@code UPPER} function. + * Creates a new {@code GREATEST} function. * - * @param expression expression to apply count, must not be {@literal null}. - * @return the new {@link SimpleFunction upper function} for {@code expression}. - * @since 2.0 + * @param expressions expressions to apply {@code GREATEST}, must not be {@literal null}. + * @return the new {@link SimpleFunction GREATEST function} for {@code expression}. + * @since 3.2 */ - public static SimpleFunction upper(Expression expression) { + public static SimpleFunction greatest(Expression... expressions) { + return greatest(Arrays.asList(expressions)); + } - Assert.notNull(expression, "Expression must not be null"); + /** + * Creates a new {@code GREATEST} function. + * + * @param expressions expressions to apply {@code GREATEST}, must not be {@literal null}. + * @return the new {@link SimpleFunction GREATEST function} for {@code expression}. + * @since 3.2 + */ + public static SimpleFunction greatest(List expressions) { + return SimpleFunction.create("GREATEST", expressions); + } - return SimpleFunction.create("UPPER", Collections.singletonList(expression)); + /** + * Creates a new {@code LEAST} function. + * + * @param expressions expressions to apply {@code LEAST}, must not be {@literal null}. + * @return the new {@link SimpleFunction LEAST function} for {@code expression}. + * @since 3.2 + */ + public static SimpleFunction least(Expression... expressions) { + return SimpleFunction.create("LEAST", Arrays.asList(expressions)); } /** * Creates a new {@code LOWER} function. * - * @param expression expression to apply lower, must not be {@literal null}. - * @return the new {@link SimpleFunction lower function} for {@code expression}. + * @param expression expression to apply {@code LOWER}, must not be {@literal null}. + * @return the new {@link SimpleFunction LOWER function} for {@code expression}. * @since 2.0 */ public static SimpleFunction lower(Expression expression) { @@ -111,10 +123,18 @@ public class Functions { return SimpleFunction.create("LOWER", Collections.singletonList(expression)); } - // Utility constructor. - private Functions() {} + /** + * Creates a new {@code UPPER} function. + * + * @param expression expression to apply {@code UPPER}, must not be {@literal null}. + * @return the new {@link SimpleFunction UPPER function} for {@code expression}. + * @since 2.0 + */ + public static SimpleFunction upper(Expression expression) { - public static SimpleFunction coalesce(Expression... expressions) { - return SimpleFunction.create("COALESCE", Arrays.asList(expressions)); + Assert.notNull(expression, "Expression must not be null"); + + return SimpleFunction.create("UPPER", Collections.singletonList(expression)); } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java index 57f6b62f..2853c94b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java @@ -30,9 +30,9 @@ import org.springframework.util.StringUtils; public class SimpleFunction extends AbstractSegment implements Expression { private final String functionName; - private final List expressions; + private final List expressions; - private SimpleFunction(String functionName, List expressions) { + private SimpleFunction(String functionName, List expressions) { super(expressions.toArray(new Expression[0])); @@ -47,7 +47,7 @@ public class SimpleFunction extends AbstractSegment implements Expression { * @param expressions zero or many {@link Expression}s, must not be {@literal null}. * @return */ - public static SimpleFunction create(String functionName, List expressions) { + public static SimpleFunction create(String functionName, List expressions) { Assert.hasText(functionName, "Function name must not be null or empty"); Assert.notNull(expressions, "Expressions name must not be null"); @@ -109,7 +109,7 @@ public class SimpleFunction extends AbstractSegment implements Expression { private final SqlIdentifier alias; - AliasedFunction(String functionName, List expressions, SqlIdentifier alias) { + AliasedFunction(String functionName, List expressions, SqlIdentifier alias) { super(functionName, expressions); this.alias = alias; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/AliasFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/AliasFactory.java index e428cbd1..32f5b9b6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/AliasFactory.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/AliasFactory.java @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.relational.core.sqlgeneration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.AggregatePathTraversal; /** * Creates aliases to be used in SQL generation - * - * @since 3.2 + * * @author Jens Schauder + * @since 3.2 */ public class AliasFactory { private final SingleAliasFactory columnAliases = new SingleAliasFactory("c"); @@ -35,7 +35,7 @@ public class AliasFactory { private final SingleAliasFactory rowCountAliases = new SingleAliasFactory("rc"); private final SingleAliasFactory backReferenceAliases = new SingleAliasFactory("br"); private final SingleAliasFactory keyAliases = new SingleAliasFactory("key"); - private int counter = 0; + private final AtomicInteger counter = new AtomicInteger(); private static String sanitize(String name) { return name.replaceAll("\\W", ""); @@ -78,7 +78,7 @@ public class AliasFactory { } private String createName(AggregatePath path) { - return prefix + getName(path) + "_" + ++counter; + return prefix + getName(path) + "_" + (counter.incrementAndGet()); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/CachingSqlGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/CachingSqlGenerator.java deleted file mode 100644 index f8e46043..00000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/CachingSqlGenerator.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 org.springframework.data.util.Lazy; - -/** - * A wrapper for the {@link SqlGenerator} that caches the generated statements. - * @since 3.2 - * @author Jens Schauder - */ -public class CachingSqlGenerator implements SqlGenerator{ - - private final SqlGenerator delegate; - - private final Lazy findAll; - private final Lazy findById; - private final Lazy findAllById; - - public CachingSqlGenerator(SqlGenerator delegate) { - - this.delegate = delegate; - - findAll = Lazy.of(delegate.findAll()); - findById = Lazy.of(delegate.findById()); - findAllById = Lazy.of(delegate.findAllById()); - } - - @Override - public String findAll() { - return findAll.get(); - } - - @Override - public String findById() { - return findById.get(); - } - - @Override - public String findAllById() { - return findAllById.get(); - } - - @Override - public AliasFactory getAliasFactory() { - return delegate.getAliasFactory(); - } -} 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 dce99128..5bb11e4b 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 @@ -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 java.util.ArrayList; @@ -36,23 +35,23 @@ import org.springframework.data.relational.core.sql.render.SqlRenderer; /** * A {@link SqlGenerator} that creates SQL statements for loading complete aggregates with a single statement. - * - * @since 3.2 + * * @author Jens Schauder + * @since 3.2 */ public class SingleQuerySqlGenerator implements SqlGenerator { private final RelationalMappingContext context; private final Dialect dialect; - private final AliasFactory aliases = new AliasFactory(); - + private final AliasFactory aliases; private final RelationalPersistentEntity aggregate; private final Table table; - public SingleQuerySqlGenerator(RelationalMappingContext context, Dialect dialect, + public SingleQuerySqlGenerator(RelationalMappingContext context, AliasFactory aliasFactory, Dialect dialect, RelationalPersistentEntity aggregate) { this.context = context; + this.aliases = aliasFactory; this.dialect = dialect; this.aggregate = aggregate; @@ -91,7 +90,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator { /** * 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 */ @@ -266,7 +265,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator { /** * Adds joins to a select. - * + * * @param rootPath the AggregatePath that gets selected by the select in question. * @param inlineQueries all the inline queries to added as joins as returned by * {@link #createInlineQueries(PersistentPropertyPaths)} @@ -298,7 +297,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator { *
  • if for a given rownumber no matching element is present for a given child the columns for that child are either * null (when there is no child elements at all) or the values for rownumber 1 are used for that child
  • * - * + * * @param rootPath path to the root entity that gets selected. * @param inlineQueries all in the inline queries for all the children, as returned by * {@link #createInlineQueries(PersistentPropertyPaths)} @@ -328,27 +327,10 @@ public class SingleQuerySqlGenerator implements SqlGenerator { return aliases; } - /** - * Constructs a SQL function of the following form - * {@code GREATEST(Coalesce(x1, 1), Coalesce(x2, 1), ..., Coalesce(xN, 1)}. this is used for cobining rownumbers from - * different child tables. The {@code coalesce} is used because the values {@code x1 ... xN} might be {@code null} and - * we want {@code null} to be equivalent with the first entry. - * - * @param expressions the different values to combined. - */ - private static SimpleFunction greatest(List expressions) { - - List guarded = new ArrayList<>(); - for (Expression expression : expressions) { - guarded.add(Functions.coalesce(expression, SQL.literalOf(1))); - } - return Functions.greatest(guarded); - } - /** * Constructs SQL of the form {@code CASE WHEN x = rn THEN alias ELSE NULL END AS ALIAS}. This expression is used to * replace values that would appear multiple times in the result with {@code null} values in all but the first - * occurrence. With out this the result for an aggregate root with a single collection item would look like this: + * occurrence. Without this the result for an aggregate root with a single collection item would look like this: * * @@ -395,11 +377,11 @@ public class SingleQuerySqlGenerator implements SqlGenerator { * * @param rowNumberAlias the alias of the rownumber column of the subselect under consideration. This determines if * the other value is replaced by null or not. - * @param alias the column potentially to be replace by null + * @param alias the column potentially to be replaced by null * @return a SQL expression. */ private static Expression filteredColumnExpression(String rowNumberAlias, String alias) { - return just("case when " + rowNumberAlias + " = rn THEN " + alias + " else null end as " + alias); + return just(String.format("case when %s = rn THEN %s else null end as %s", rowNumberAlias, alias, alias)); } private static Expression just(String alias) { @@ -409,6 +391,23 @@ public class SingleQuerySqlGenerator implements SqlGenerator { return Expressions.just(alias); } + /** + * Constructs a SQL function of the following form + * {@code GREATEST(Coalesce(x1, 1), Coalesce(x2, 1), ..., Coalesce(xN, 1)}. this is used for cobining rownumbers from + * different child tables. The {@code coalesce} is used because the values {@code x1 ... xN} might be {@code null} and + * we want {@code null} to be equivalent with the first entry. + * + * @param expressions the different values to combined. + */ + private static SimpleFunction greatest(List expressions) { + + List guarded = new ArrayList<>(); + for (Expression expression : expressions) { + guarded.add(Functions.coalesce(expression, SQL.literalOf(1))); + } + return Functions.greatest(guarded); + } + record QueryMeta(AggregatePath basePath, InlineQuery inlineQuery, Collection simpleColumns, Collection selectableExpressions, Expression id, Expression backReference, Expression key, Expression rowNumber, Expression rowCount) { 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 ce247e13..78049657 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 @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.relational.core.sqlgeneration; /** * Generates SQL statements for loading aggregates. - * @since 3.2 + * * @author Jens Schauder + * @since 3.2 */ public interface SqlGenerator { String findAll(); 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 5901fadf..5721ce2b 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 @@ -32,7 +32,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProp /** * Tests for {@link SingleQuerySqlGenerator}. - * + * * @author Jens Schauder */ class SingleQuerySqlGeneratorUnitTests { @@ -152,7 +152,8 @@ class SingleQuerySqlGeneratorUnitTests { col(trivialsRowNumber), // col(alias("trivials.id")), // col(alias("trivials.name")), // - func("greatest", func("coalesce",col(rootRowNumber), lit(1)), func("coalesce",col(trivialsRowNumber), lit(1))), // + func("greatest", func("coalesce", col(rootRowNumber), lit(1)), + func("coalesce", col(trivialsRowNumber), lit(1))), // col(backref), // col(keyAlias) // ).extractWhereClause() // @@ -210,10 +211,9 @@ class SingleQuerySqlGeneratorUnitTests { private AbstractTestFixture(Class aggregateRootType) { this.aggregateRootType = aggregateRootType; - this.sqlGenerator = new SingleQuerySqlGenerator(context, dialect, + this.sqlGenerator = new SingleQuerySqlGenerator(context, new AliasFactory(), dialect, context.getRequiredPersistentEntity(aggregateRootType)); this.aliases = sqlGenerator.getAliasFactory(); - } AggregatePath path() {
    * root value