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
This commit is contained in:
Mark Paluch
2023-08-09 10:27:19 +02:00
parent 93821f5f42
commit d9b548815c
25 changed files with 442 additions and 336 deletions

View File

@@ -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 <T> the type of aggregate produced by this reader.
* @since 3.2
* @author Jens Schauder
* @since 3.2
*/
class AggregateReader<T> {
private final RelationalMappingContext mappingContext;
private final RelationalPersistentEntity<T> 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<T> extractor;
AggregateReader(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter,
AggregateReader(Dialect dialect, JdbcConverter converter, AliasFactory aliasFactory,
NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity<T> 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<T> findAll() {
String sql = sqlGenerator.findAll();
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
pathToColumn);
Iterable<T> result = jdbcTemplate.query(sql, extractor);
Iterable<T> result = jdbcTemplate.query(sqlGenerator.findAll(), extractor);
Assert.state(result != null, "result is null");
return (List<T>) result;
}
@Nullable
public T findById(Object id) {
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
pathToColumn);
String sql = sqlGenerator.findById();
id = converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation());
Iterator<T> result = jdbcTemplate.query(sql, Map.of("id", id), extractor).iterator();
Iterator<T> result = jdbcTemplate.query(sqlGenerator.findById(), Map.of("id", id), extractor).iterator();
T returnValue = result.hasNext() ? result.next() : null;
@@ -101,18 +88,12 @@ class AggregateReader<T> {
public Iterable<T> findAllById(Iterable<?> ids) {
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
pathToColumn);
String sql = sqlGenerator.findAllById();
List<Object> 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<T> {
public String column(AggregatePath path) {
String alias = aliasFactory.getColumnAlias(path);
Assert.notNull(alias, () -> "alias for >" + path + "<must not be null");
Assert.notNull(alias, () -> "alias for >" + path + "< must not be null");
return alias;
}
@@ -131,4 +112,49 @@ class AggregateReader<T> {
}
};
}
/**
* 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();
}
}
}

View File

@@ -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;
}
<T> AggregateReader<T> createAggregateReaderFor(RelationalPersistentEntity<T> entity) {
return new AggregateReader<>(mappingContext, dialect, converter, jdbcTemplate, entity);
}
}

View File

@@ -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 <T> the type of aggregates to extract
* @since 3.2
* @author Jens Schauder
* @since 3.2
*/
class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>> {
@@ -61,8 +61,6 @@ class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>>
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<T> implements ResultSetExtractor<Iterable<T>>
* column of the {@link ResultSet} that holds the data for that
* {@link org.springframework.data.relational.core.mapping.AggregatePath}.
*/
AggregateResultSetExtractor(RelationalMappingContext context, RelationalPersistentEntity<T> rootEntity,
AggregateResultSetExtractor(RelationalPersistentEntity<T> 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<T> implements ResultSetExtractor<Iterable<T>>
/**
* 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<T> implements ResultSetExtractor<Iterable<T>>
/**
* 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<T> implements ResultSetExtractor<Iterable<T>>
/**
* Adapts a {@link Map} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
*
*
* @since 3.2
* @author Jens Schauder
*/
@@ -221,7 +218,7 @@ class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>>
/**
* A {@link Reader} for reading entities.
*
*
* @since 3.2
* @author Jens Schauder
*/
@@ -315,7 +312,7 @@ class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>>
/**
* A {@link Reader} for reading collections of entities.
*
*
* @since 3.2
* @author Jens Schauder
*/
@@ -413,7 +410,7 @@ class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>>
/**
* 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<T> implements ResultSetExtractor<Iterable<T>>
}
/**
* 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
*/

View File

@@ -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 {

View File

@@ -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 <T> the type of the entity.
* @return Might return {@code null}.
*/
@Override
@Nullable
<T> T findById(Object id, Class<T> domainType);
@@ -248,6 +249,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param <T> the type of entities to load.
* @return Guaranteed to be not {@code null}.
*/
@Override
<T> Iterable<T> findAll(Class<T> domainType);
/**
@@ -259,6 +261,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @param <T> type of entities to load.
* @return the loaded entities. Guaranteed to be not {@code null}.
*/
@Override
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
@Override
@@ -274,6 +277,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @return Guaranteed to be not {@code null}.
* @since 2.0
*/
@Override
<T> Iterable<T> findAll(Class<T> domainType, Sort sort);
/**
@@ -285,6 +289,7 @@ public interface DataAccessStrategy extends RelationResolver {
* @return Guaranteed to be not {@code null}.
* @since 2.0
*/
@Override
<T> Iterable<T> findAll(Class<T> 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
<T> Optional<T> findOne(Query query, Class<T> 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
<T> Iterable<T> findAll(Query query, Class<T> 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
<T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable);
}

View File

@@ -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;
}
}

View File

@@ -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> T findById(Object id, Class<T> 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 <T> Iterable<T> findAll(Class<T> 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<RelationalPersistentProperty> 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;
}
}

View File

@@ -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);

View File

@@ -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.
*

View File

@@ -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<RelationalPersistentEntity<?>, 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 <T> AggregateReader<T> getReader(Class<T> domainType) {
RelationalPersistentEntity<T> persistentEntity = (RelationalPersistentEntity<T>) mappingContext
.getRequiredPersistentEntity(domainType);
return readerFactory.createAggregateReaderFor(persistentEntity);
return (AggregateReader<T>) readerCache.get(persistentEntity);
}
}

View File

@@ -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> T findById(Object id, Class<T> domainType) {
if (isSingleSelectQuerySupported(domainType)) {
return singleSelectDelegate.findById(id, domainType);
}
return super.findById(id, domainType);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
if (isSingleSelectQuerySupported(domainType)) {
return singleSelectDelegate.findAll(domainType);
}
return super.findAll(domainType);
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> 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<RelationalPersistentProperty> 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;
}
}

View File

@@ -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)));
}
/**

View File

@@ -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();
}
/**

View File

@@ -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<T extends Repository<S, ID>, 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();
});
}

View File

@@ -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 <T> AggregateResultSetExtractor<T> getExtractor(Class<T> type) {
return (AggregateResultSetExtractor<T>) new AggregateResultSetExtractor<>(context,
return (AggregateResultSetExtractor<T>) new AggregateResultSetExtractor<>(
(RelationalPersistentEntity<DummyRecord>) context.getPersistentEntity(type), converter, column);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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<Expression> 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<? extends Expression> 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<? extends Expression> 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));
}
}

View File

@@ -30,9 +30,9 @@ import org.springframework.util.StringUtils;
public class SimpleFunction extends AbstractSegment implements Expression {
private final String functionName;
private final List<Expression> expressions;
private final List<? extends Expression> expressions;
private SimpleFunction(String functionName, List<Expression> expressions) {
private SimpleFunction(String functionName, List<? extends Expression> 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<Expression> expressions) {
public static SimpleFunction create(String functionName, List<? extends Expression> 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<Expression> expressions, SqlIdentifier alias) {
AliasedFunction(String functionName, List<? extends Expression> expressions, SqlIdentifier alias) {
super(functionName, expressions);
this.alias = alias;
}

View File

@@ -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());
}
}

View File

@@ -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<String> findAll;
private final Lazy<String> findById;
private final Lazy<String> 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();
}
}

View File

@@ -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 {
* <li>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</li>
* </ol>
*
*
* @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<Expression> expressions) {
List<Expression> 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:
* <table>
* <th>
* <td>root value</td>
@@ -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<Expression> expressions) {
List<Expression> 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<Expression> simpleColumns,
Collection<Expression> selectableExpressions, Expression id, Expression backReference, Expression key,
Expression rowNumber, Expression rowCount) {

View File

@@ -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();

View File

@@ -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() {