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