Enable Single Query Loading for simple aggregates.

Single Query Loading loads as the name suggests complete aggregates using a single select.
While the ultimate goal is to support this for all aggregates, this commit enables it only for simple aggregate and also only for some operations.
A simple aggregate is an aggregate that only reference up to one other entity and does not have embedded entities.
The supported operations are those available via `CrudRepository`: `findAll`, `findById`, and `findAllByIds`.

Single Query Loading does NOT work with the supported in memory databases H2 and HSQLDB, since these do not properly support windowing functions, which are essential for Single Query Loading.

To turn on Single Query Loading call `RelationalMappingContext.setSingleQueryLoadingEnabled(true)`.

Closes #1446
See #1450
See #1445
Original pull request: #1572
This commit is contained in:
Jens Schauder
2023-06-12 10:11:57 +02:00
committed by Mark Paluch
parent dffde372f9
commit 93821f5f42
50 changed files with 4493 additions and 49 deletions

View File

@@ -0,0 +1,134 @@
/*
* 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.ArrayList;
import java.util.Iterator;
import java.util.List;
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.jdbc.core.namedparam.NamedParameterJdbcOperations;
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
*/
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;
AggregateReader(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter,
NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity<T> aggregate) {
this.mappingContext = mappingContext;
this.aggregate = aggregate;
this.converter = converter;
this.jdbcTemplate = jdbcTemplate;
this.sqlGenerator = new CachingSqlGenerator(new SingleQuerySqlGenerator(mappingContext, dialect, aggregate));
this.aliasFactory = sqlGenerator.getAliasFactory();
}
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);
Assert.state(result != null, "result is null");
return (List<T>) result;
}
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();
T returnValue = result.hasNext() ? result.next() : null;
if (result.hasNext()) {
throw new IncorrectResultSizeDataAccessException(1);
}
return returnValue;
}
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);
}
private PathToColumnMapping createPathToColumnMapping(AliasFactory aliasFactory) {
return new PathToColumnMapping() {
@Override
public String column(AggregatePath path) {
String alias = aliasFactory.getColumnAlias(path);
Assert.notNull(alias, () -> "alias for >" + path + "<must not be null");
return alias;
}
@Override
public String keyColumn(AggregatePath path) {
return aliasFactory.getKeyAlias(path);
}
};
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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

@@ -0,0 +1,596 @@
/*
* 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.sql.ResultSet;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.EntityInstantiator;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Extracts complete aggregates from a {@link ResultSet}. The {@literal ResultSet} must have a very special structure
* 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
*/
class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>> {
private final RelationalMappingContext context;
private final RelationalPersistentEntity<T> rootEntity;
private final JdbcConverter converter;
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}.
* @param pathToColumn a mapping from {@link org.springframework.data.relational.core.mapping.AggregatePath} to the
* 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,
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.propertyToColumn = pathToColumn;
}
@Override
public Iterable<T> extractData(ResultSet resultSet) throws DataAccessException {
CachingResultSet crs = new CachingResultSet(resultSet);
CollectionReader reader = new CollectionReader(crs);
while (crs.next()) {
reader.read();
}
return (Iterable<T>) reader.getResultAndReset();
}
/**
* create an instance and populate all its properties
*/
@Nullable
private Object hydrateInstance(EntityInstantiator instantiator, ResultSetParameterValueProvider valueProvider,
RelationalPersistentEntity<?> entity) {
if (!valueProvider.basePath.isRoot() && // this is a nested ValueProvider
valueProvider.basePath.getRequiredLeafProperty().isEmbedded() && // it's an embedded
!valueProvider.basePath.getRequiredLeafProperty().shouldCreateEmptyEmbedded() && // it's embedded
!valueProvider.hasValue()) { // all values have been null
return null;
}
Object instance = instantiator.createInstance(entity, valueProvider);
PersistentPropertyAccessor<?> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
converter.getConversionService());
if (entity.requiresPropertyPopulation()) {
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) p -> {
if (!entity.isCreatorArgument(p)) {
accessor.setProperty(p, valueProvider.getValue(p));
}
});
}
return instance;
}
/**
* 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
*/
private interface Reader {
/**
* read the data needed for creating the result of this {@literal Reader}
*/
void read();
/**
* 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
Object getResultAndReset();
}
/**
* Adapts a {@link Map} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
*
* @since 3.2
* @author Jens Schauder
*/
private static class MapAdapter extends AbstractCollection<Map.Entry<Object, Object>> {
private final Map<Object, Object> map = new HashMap<>();
@Override
public Iterator<Map.Entry<Object, Object>> iterator() {
return map.entrySet().iterator();
}
@Override
public int size() {
return map.size();
}
@Override
public boolean add(Map.Entry<Object, Object> entry) {
map.put(entry.getKey(), entry.getValue());
return true;
}
}
/**
* Adapts a {@link List} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
*
* @since 3.2
* @author Jens Schauder
*/
private static class ListAdapter extends AbstractCollection<Map.Entry<Object, Object>> {
private final List<Object> list = new ArrayList<>();
@Override
public Iterator<Map.Entry<Object, Object>> iterator() {
throw new UnsupportedOperationException("Do we need this?");
}
@Override
public int size() {
return list.size();
}
@Override
public boolean add(Map.Entry<Object, Object> entry) {
Integer index = (Integer) entry.getKey();
while (index >= list.size()) {
list.add(null);
}
list.set(index, entry.getValue());
return true;
}
}
/**
* A {@link Reader} for reading entities.
*
* @since 3.2
* @author Jens Schauder
*/
private class EntityReader implements Reader {
/**
* Debugging the recursive structure of {@link Reader} instances can become a little mind bending. Giving each
* {@literal Reader} a descriptive name helps with that.
*/
private final String name;
private final AggregatePath basePath;
private final CachingResultSet crs;
private final EntityInstantiator instantiator;
@Nullable private final String idColumn;
private ResultSetParameterValueProvider valueProvider;
private boolean result;
Object oldId = null;
private EntityReader(AggregatePath basePath, CachingResultSet crs) {
this(basePath, crs, null);
}
private EntityReader(AggregatePath basePath, CachingResultSet crs, @Nullable String keyColumn) {
this.basePath = basePath;
this.crs = crs;
RelationalPersistentEntity<?> entity = basePath.isRoot() ? rootEntity : basePath.getRequiredLeafEntity();
instantiator = converter.getEntityInstantiators().getInstantiatorFor(entity);
idColumn = entity.hasIdProperty() ? propertyToColumn.column(basePath.append(entity.getRequiredIdProperty()))
: keyColumn;
reset();
name = "EntityReader for " + (basePath.isRoot() ? "<root>" : basePath.toDotPath());
}
@Override
public void read() {
if (idColumn != null && oldId == null) {
oldId = crs.getObject(idColumn);
}
valueProvider.readValues();
if (idColumn == null) {
result = true;
} else {
Object peekedId = crs.peek(idColumn);
if (peekedId == null || !peekedId.equals(oldId)) {
result = true;
oldId = peekedId;
}
}
}
@Override
public boolean hasResult() {
return result;
}
@Override
@Nullable
public Object getResultAndReset() {
try {
return hydrateInstance(instantiator, valueProvider, valueProvider.baseEntity);
} finally {
reset();
}
}
private void reset() {
valueProvider = new ResultSetParameterValueProvider(crs, basePath);
result = false;
}
@Override
public String toString() {
return name;
}
}
/**
* A {@link Reader} for reading collections of entities.
*
* @since 3.2
* @author Jens Schauder
*/
class CollectionReader implements Reader {
// debugging only
private final String name;
private final Supplier<Collection> collectionInitializer;
private final Reader entityReader;
private Collection result;
private static Supplier<Collection> collectionInitializerFor(AggregatePath path) {
RelationalPersistentProperty property = path.getRequiredLeafProperty();
if (List.class.isAssignableFrom(property.getType())) {
return ListAdapter::new;
} else if (property.isMap()) {
return MapAdapter::new;
} else {
return HashSet::new;
}
}
private CollectionReader(AggregatePath basePath, CachingResultSet crs) {
this.collectionInitializer = collectionInitializerFor(basePath);
String keyColumn = null;
final RelationalPersistentProperty property = basePath.getRequiredLeafProperty();
if (property.isMap() || List.class.isAssignableFrom(basePath.getRequiredLeafProperty().getType())) {
keyColumn = propertyToColumn.keyColumn(basePath);
}
if (property.isQualified()) {
this.entityReader = new EntryReader(basePath, crs, keyColumn, property.getQualifierColumnType());
} else {
this.entityReader = new EntityReader(basePath, crs, keyColumn);
}
reset();
name = "Reader for " + basePath.toDotPath();
}
private CollectionReader(CachingResultSet crs) {
this.collectionInitializer = ArrayList::new;
this.entityReader = new EntityReader(context.getAggregatePath(rootEntity), crs);
reset();
name = "Collectionreader for <root>";
}
@Override
public void read() {
entityReader.read();
if (entityReader.hasResult()) {
result.add(entityReader.getResultAndReset());
}
}
@Override
public boolean hasResult() {
return false;
}
@Override
public Object getResultAndReset() {
try {
if (result instanceof MapAdapter) {
return ((MapAdapter) result).map;
}
if (result instanceof ListAdapter) {
return ((ListAdapter) result).list;
}
return result;
} finally {
reset();
}
}
private void reset() {
result = collectionInitializer.get();
}
@Override
public String toString() {
return name;
}
}
/**
* 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
*/
private class EntryReader implements Reader {
final EntityReader delegate;
final String keyColumn;
private final TypeInformation<?> keyColumnType;
Object key;
EntryReader(AggregatePath basePath, CachingResultSet crs, String keyColumn, Class<?> keyColumnType) {
this.keyColumnType = TypeInformation.of(keyColumnType);
this.delegate = new EntityReader(basePath, crs, keyColumn);
this.keyColumn = keyColumn;
}
@Override
public void read() {
if (key == null) {
Object unconvertedKeyObject = delegate.crs.getObject(keyColumn);
key = converter.readValue(unconvertedKeyObject, keyColumnType);
}
delegate.read();
}
@Override
public boolean hasResult() {
return delegate.hasResult();
}
@Override
public Object getResultAndReset() {
try {
return new AbstractMap.SimpleEntry<>(key, delegate.getResultAndReset());
} finally {
key = null;
}
}
}
/**
* 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
*/
private class ResultSetParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
private final CachingResultSet rs;
/**
* The path which is used to determine columnNames
*/
private final AggregatePath basePath;
private final RelationalPersistentEntity<?> baseEntity;
/**
* Holds all the values for the entity, either directly or in the form of an appropriate {@link Reader}.
*/
private final Map<RelationalPersistentProperty, Object> aggregatedValues = new HashMap<>();
ResultSetParameterValueProvider(CachingResultSet rs, AggregatePath basePath) {
this.rs = rs;
this.basePath = basePath;
this.baseEntity = basePath.isRoot() ? rootEntity
: context.getRequiredPersistentEntity(basePath.getRequiredLeafProperty().getActualType());
}
@SuppressWarnings("unchecked")
@Override
@Nullable
public <S> S getParameterValue(Parameter<S, RelationalPersistentProperty> parameter) {
return (S) getValue(baseEntity.getRequiredPersistentProperty(parameter.getName()));
}
@Nullable
private Object getValue(RelationalPersistentProperty property) {
Object value = aggregatedValues.get(property);
if (value instanceof Reader) {
return ((Reader) value).getResultAndReset();
}
value = converter.readValue(value, property.getTypeInformation());
return value;
}
/**
* read values for all collection like properties and aggregate them in a collection.
*/
void readValues() {
baseEntity.forEach(this::readValue);
}
private void readValue(RelationalPersistentProperty p) {
if (p.isEntity()) {
Reader reader = null;
if (p.isCollectionLike() || p.isMap()) { // even when there are no values we still want a (empty) collection.
reader = (Reader) aggregatedValues.computeIfAbsent(p, pp -> new CollectionReader(basePath.append(pp), rs));
}
if (getIndicatorOf(p) != null) {
if (!(p.isCollectionLike() || p.isMap())) { // for single entities we want a null entity instead of on filled
// with null values.
reader = (Reader) aggregatedValues.computeIfAbsent(p, pp -> new EntityReader(basePath.append(pp), rs));
}
Assert.state(reader != null, "reader must not be null");
reader.read();
}
} else {
aggregatedValues.computeIfAbsent(p, this::getObject);
}
}
@Nullable
private Object getIndicatorOf(RelationalPersistentProperty p) {
if (p.isMap() || List.class.isAssignableFrom(p.getType())) {
return rs.getObject(getKeyName(p));
}
if (p.isEmbedded()) {
return true;
}
return rs.getObject(getColumnName(p));
}
/**
* Obtain a single columnValue from the resultset without throwing an exception. If the column does not exist a null
* value is returned. Does not instantiate complex objects.
*
* @param property
* @return
*/
@Nullable
private Object getObject(RelationalPersistentProperty property) {
return rs.getObject(getColumnName(property));
}
/**
* converts a property into a column name representing that property.
*
* @param property
* @return
*/
private String getColumnName(RelationalPersistentProperty property) {
return propertyToColumn.column(basePath.append(property));
}
private String getKeyName(RelationalPersistentProperty property) {
return propertyToColumn.keyColumn(basePath.append(property));
}
private boolean hasValue() {
for (Object value : aggregatedValues.values()) {
if (value != null) {
return true;
}
}
return false;
}
}
}

View File

@@ -394,6 +394,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
}
private <S> ReadingContext<S> extendBy(RelationalPersistentProperty property) {
return new ReadingContext<>(
(RelationalPersistentEntity<S>) getMappingContext().getRequiredPersistentEntity(property.getActualType()),
rootPath.append(property), path.append(property), identifier, key,

View File

@@ -0,0 +1,124 @@
/*
* 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.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
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
*/
class CachingResultSet {
private final ResultSetAccessor accessor;
private final ResultSet resultSet;
private Cache cache;
CachingResultSet(ResultSet resultSet) {
this.accessor = new ResultSetAccessor(resultSet);
this.resultSet = resultSet;
}
public boolean next() {
if (isPeeking()) {
final boolean next = cache.next;
cache = null;
return next;
}
try {
return resultSet.next();
} catch (SQLException e) {
throw new RuntimeException("Failed to advance CachingResultSet", e);
}
}
@Nullable
public Object getObject(String columnLabel) {
Object returnValue;
if (isPeeking()) {
returnValue = cache.values.get(columnLabel);
} else {
returnValue = safeGetFromDelegate(columnLabel);
}
return returnValue;
}
@Nullable
Object peek(String columnLabel) {
if (!isPeeking()) {
createCache();
}
if (!cache.next) {
return null;
}
return safeGetFromDelegate(columnLabel);
}
@Nullable
private Object safeGetFromDelegate(String columnLabel) {
return accessor.getObject(columnLabel);
}
private void createCache() {
cache = new Cache();
try {
int columnCount = resultSet.getMetaData().getColumnCount();
for (int i = 1; i <= columnCount; i++) {
// at least some databases return lower case labels although rs.getObject(UPPERCASE_LABEL) returns the expected
// value. The aliases we use happen to be uppercase. So we transform everything to upper case.
cache.add(resultSet.getMetaData().getColumnLabel(i).toLowerCase(),
accessor.getObject(resultSet.getMetaData().getColumnLabel(i)));
}
cache.next = resultSet.next();
} catch (SQLException se) {
throw new RuntimeException("Can't cache result set data", se);
}
}
private boolean isPeeking() {
return cache != null;
}
private static class Cache {
boolean next;
Map<String, Object> values = new HashMap<>();
void add(String columnName, Object value) {
values.put(columnName, value);
}
}
}

View File

@@ -29,7 +29,6 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.conversion.IdValueSource;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -69,6 +68,7 @@ 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,6 +96,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
this.operations = operations;
this.sqlParametersFactory = sqlParametersFactory;
this.insertStrategyFactory = insertStrategyFactory;
this.singleSelectDelegate = new SingleQueryDataAccessStrategy(context, sqlGeneratorSource.getDialect(), converter, operations);
}
@Override
@@ -260,6 +261,10 @@ 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);
@@ -272,6 +277,11 @@ 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));
}
@@ -282,10 +292,12 @@ 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));
}
@@ -430,4 +442,40 @@ 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

@@ -0,0 +1,36 @@
/*
* 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.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
*/
public interface PathToColumnMapping {
String column(AggregatePath path);
String keyColumn(AggregatePath path);
}

View File

@@ -0,0 +1,120 @@
/*
* 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.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.relational.core.query.Query;
import org.springframework.lang.Nullable;
/**
* The finding methods of a {@link DataAccessStrategy}.
*
* @since 3.2
* @author Jens Schauder
*/
interface ReadingDataAccessStrategy {
/**
* Loads a single entity identified by type and id.
*
* @param id the id of the entity to load. Must not be {@code null}.
* @param domainType the domain type of the entity. Must not be {@code null}.
* @param <T> the type of the entity.
* @return Might return {@code null}.
*/
@Nullable
<T> T findById(Object id, Class<T> domainType);
/**
* Loads all entities of the given type.
*
* @param domainType the type of entities to load. Must not be {@code null}.
* @param <T> the type of entities to load.
* @return Guaranteed to be not {@code null}.
*/
<T> Iterable<T> findAll(Class<T> domainType);
/**
* Loads all entities that match one of the ids passed as an argument. It is not guaranteed that the number of ids
* passed in matches the number of entities returned.
*
* @param ids the Ids of the entities to load. Must not be {@code null}.
* @param domainType the type of entities to load. Must not be {@code null}.
* @param <T> type of entities to load.
* @return the loaded entities. Guaranteed to be not {@code null}.
*/
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
/**
* Loads all entities of the given type, sorted.
*
* @param domainType the type of entities to load. Must not be {@code null}.
* @param <T> the type of entities to load.
* @param sort the sorting information. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
* @since 2.0
*/
<T> Iterable<T> findAll(Class<T> domainType, Sort sort);
/**
* Loads all entities of the given type, paged and sorted.
*
* @param domainType the type of entities to load. Must not be {@code null}.
* @param <T> the type of entities to load.
* @param pageable the pagination information. Must not be {@code null}.
* @return Guaranteed to be not {@code null}.
* @since 2.0
*/
<T> Iterable<T> findAll(Class<T> domainType, Pageable pageable);
/**
* Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result.
*
* @param query must not be {@literal null}.
* @param domainType the type of entities. Must not be {@code null}.
* @return exactly one result or {@link Optional#empty()} if no match found.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/
<T> Optional<T> findOne(Query query, Class<T> domainType);
/**
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}.
*
* @param query must not be {@literal null}.
* @param domainType the type of entities. Must not be {@code null}.
* @return a non-null list with all the matching results.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/
<T> Iterable<T> findAll(Query query, Class<T> domainType);
/**
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. Applies the {@link Pageable}
* to the result.
*
* @param query must not be {@literal null}.
* @param domainType the type of entities. Must not be {@literal null}.
* @param pageable the pagination that should be applied. Must not be {@literal null}.
* @return a non-null list with all the matching results.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
* @since 3.0
*/
<T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable);
}

View File

@@ -0,0 +1,93 @@
/*
* 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.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
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.jdbc.core.namedparam.NamedParameterJdbcOperations;
/**
* A {@link ReadingDataAccessStrategy} that uses an {@link AggregateReader} to load entities with a single query.
*
* @since 3.2
* @author Jens Schauder
*/
public class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy {
private final AggregateReaderFactory readerFactory;
private final RelationalMappingContext mappingContext;
public SingleQueryDataAccessStrategy(RelationalMappingContext mappingContext, Dialect dialect,
JdbcConverter converter, NamedParameterJdbcOperations jdbcTemplate) {
this.mappingContext = mappingContext;
this.readerFactory = new AggregateReaderFactory(mappingContext, dialect, converter, jdbcTemplate);
;
}
@Override
public <T> T findById(Object id, Class<T> domainType) {
return getReader(domainType).findById(id);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType) {
return getReader(domainType).findAll();
}
@Override
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
return getReader(domainType).findAllById(ids);
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType, Sort sort) {
throw new UnsupportedOperationException();
}
@Override
public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
throw new UnsupportedOperationException();
}
@Override
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
return Optional.empty();
}
@Override
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
throw new UnsupportedOperationException();
}
@Override
public <T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable) {
throw new UnsupportedOperationException();
}
private <T> AggregateReader<T> getReader(Class<T> domainType) {
RelationalPersistentEntity<T> persistentEntity = (RelationalPersistentEntity<T>) mappingContext
.getRequiredPersistentEntity(domainType);
return readerFactory.createAggregateReaderFor(persistentEntity);
}
}

View File

@@ -36,6 +36,7 @@ import java.util.function.Function;
import java.util.stream.IntStream;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -90,13 +91,21 @@ import org.springframework.transaction.annotation.Transactional;
@Transactional
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
@ExtendWith(SpringExtension.class)
class JdbcAggregateTemplateIntegrationTests {
abstract class AbstractJdbcAggregateTemplateIntegrationTests {
@Autowired JdbcAggregateOperations template;
@Autowired NamedParameterJdbcOperations jdbcTemplate;
@Autowired RelationalMappingContext mappingContext;
LegoSet legoSet = createLegoSet("Star Destroyer");
@BeforeEach
void beforeEach(){
mappingContext.setSingleQueryLoadingEnabled(useSingleQuery());
}
abstract boolean useSingleQuery();
/**
* creates an instance of {@link NoIdListChain4} with the following properties:
* <ul>
@@ -193,6 +202,42 @@ class JdbcAggregateTemplateIntegrationTests {
return entity;
}
@Test // GH-1446
void findById() {
WithInsertOnly entity = new WithInsertOnly();
entity.insertOnly = "entity";
entity = template.save(entity);
WithInsertOnly other = new WithInsertOnly();
other.insertOnly = "other";
other = template.save(other);
assertThat(template.findById(entity.id, WithInsertOnly.class).insertOnly).isEqualTo("entity");
assertThat(template.findById(other.id, WithInsertOnly.class).insertOnly).isEqualTo("other");
}
@Test // GH-1446
void findAllById() {
WithInsertOnly entity = new WithInsertOnly();
entity.insertOnly = "entity";
entity = template.save(entity);
WithInsertOnly other = new WithInsertOnly();
other.insertOnly = "other";
other = template.save(other);
WithInsertOnly yetAnother = new WithInsertOnly();
yetAnother.insertOnly = "yetAnother";
yetAnother = template.save(yetAnother);
Iterable<WithInsertOnly> reloadedById = template.findAllById(asList(entity.id, yetAnother.id),
WithInsertOnly.class);
assertThat(reloadedById).extracting(e -> e.id, e -> e.insertOnly)
.containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother"));
}
@Test // DATAJDBC-112
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
void saveAndLoadAnEntityWithReferencedEntityById() {
@@ -1833,4 +1878,17 @@ class JdbcAggregateTemplateIntegrationTests {
return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy);
}
}
static class JdbcAggregateTemplateIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {
@Override
boolean useSingleQuery() {
return false;
}
}
static class JdbcAggregateTemplateSqlIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {
@Override
boolean useSingleQuery() {
return true;
}
}
}

View File

@@ -0,0 +1,718 @@
/*
* 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 static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.jdbc.core.convert.AggregateResultSetExtractorUnitTests.ColumnType.*;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
/**
* Unit tests for the {@link AggregateResultSetExtractor}.
*
* @author Jens Schauder
*/
public class AggregateResultSetExtractorUnitTests {
RelationalMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
private final JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class));
private final PathToColumnMapping column = new PathToColumnMapping() {
@Override
public String column(AggregatePath path) {
return AggregateResultSetExtractorUnitTests.this.column(path);
}
@Override
public String keyColumn(AggregatePath path) {
return column(path) + "_key";
}
};
AggregateResultSetExtractor<SimpleEntity> extractor = getExtractor(SimpleEntity.class);
@Test // GH-1446
void emptyResultSetYieldsEmptyResult() throws SQLException {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList("T0_C0_ID1", "T0_C1_NAME"));
assertThat(extractor.extractData(resultSet)).isEmpty();
}
@Test // GH-1446
void singleSimpleEntityGetsExtractedFromSingleRow() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
1, "Alfred");
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
.containsExactly(tuple(1L, "Alfred"));
}
@Test // GH-1446
void multipleSimpleEntitiesGetExtractedFromMultipleRows() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
1, "Alfred", //
2, "Bertram" //
);
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name).containsExactly( //
tuple(1L, "Alfred"), //
tuple(2L, "Bertram") //
);
}
@Nested
class Conversions {
@Test // GH-1446
void appliesConversionToProperty() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
new BigDecimal(1), "Alfred");
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
.containsExactly(tuple(1L, "Alfred"));
}
@Test // GH-1446
void appliesConversionToConstructorValue() {
AggregateResultSetExtractor<DummyRecord> extractor = getExtractor(DummyRecord.class);
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
new BigDecimal(1), "Alfred");
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
.containsExactly(tuple(1L, "Alfred"));
}
@Test // GH-1446
void appliesConversionToKeyValue() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
1, new BigDecimal(0), "Dummy Alfred", //
1, new BigDecimal(1), "Dummy Berta", //
1, new BigDecimal(2), "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
}
}
@NotNull
private <T> AggregateResultSetExtractor<T> getExtractor(Class<T> type) {
return (AggregateResultSetExtractor<T>) new AggregateResultSetExtractor<>(context,
(RelationalPersistentEntity<DummyRecord>) context.getPersistentEntity(type), converter, column);
}
@Nested
class EmbeddedReference {
@Test // GH-1446
void embeddedGetsExtractedFromSingleRow() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNullable.dummyName")), //
1, "Imani");
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.embeddedNullable.dummyName)
.containsExactly(tuple(1L, "Imani"));
}
@Test // GH-1446
void nullEmbeddedGetsExtractedFromSingleRow() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNullable.dummyName")), //
1, null);
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.embeddedNullable)
.containsExactly(tuple(1L, null));
}
@Test // GH-1446
void emptyEmbeddedGetsExtractedFromSingleRow() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNonNull.dummyName")), //
1, null);
assertThat(extractor.extractData(resultSet)) //
.extracting(e -> e.id1, e -> e.embeddedNonNull.dummyName) //
.containsExactly(tuple(1L, null));
}
}
@Nested
class ToOneRelationships {
@Test // GH-1446
void entityReferenceGetsExtractedFromSingleRow() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummy"), column("dummy.dummyName")), //
1, 1, "Dummy Alfred");
assertThat(extractor.extractData(resultSet)) //
.extracting(e -> e.id1, e -> e.dummy.dummyName) //
.containsExactly(tuple(1L, "Dummy Alfred"));
}
@Test // GH-1446
void nullEntityReferenceGetsExtractedFromSingleRow() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummy"), column("dummy.dummyName")), //
1, null, "Dummy Alfred");
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.dummy)
.containsExactly(tuple(1L, null));
}
}
@Nested
class Sets {
@Test // GH-1446
void extractEmptySetReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummies"), column("dummies.dummyName")), //
1, null, null, //
1, null, null, //
1, null, null);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummies).isEmpty();
}
@Test // GH-1446
void extractSingleSetReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummies"), column("dummies.dummyName")), //
1, 1, "Dummy Alfred", //
1, 1, "Dummy Berta", //
1, 1, "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
}
@Test // GH-1446
void extractSetReferenceAndSimpleProperty() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("name"), column("dummies"), column("dummies.dummyName")), //
1, "Simplicissimus", 1, "Dummy Alfred", //
1, null, 1, "Dummy Berta", //
1, null, 1, "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
}
@Test // GH-1446
void extractMultipleSetReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
column("dummies"), column("dummies.dummyName"), //
column("otherDummies"), column("otherDummies.dummyName")), //
1, 1, "Dummy Alfred", 1, "Other Ephraim", //
1, 1, "Dummy Berta", 1, "Other Zeno", //
1, 1, "Dummy Carl", null, null);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
}
@Test // GH-1446
void extractNestedSetsWithId() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
column("intermediates"), column("intermediates.iId"), column("intermediates.intermediateName"), //
column("intermediates.dummies"), column("intermediates.dummies.dummyName")), //
1, "Alfred", 1, 23, "Inami", 23, "Dustin", //
1, null, 1, 23, null, 23, "Dora", //
1, null, 1, 24, "Ina", 24, "Dotty", //
1, null, 1, 25, "Ion", null, null, //
2, "Bon Jovi", 2, 26, "Judith", 26, "Ephraim", //
2, null, 2, 26, null, 26, "Erin", //
2, null, 2, 27, "Joel", 27, "Erika", //
2, null, 2, 28, "Justin", null, null //
);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediates.size())
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediates.size())
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
final Iterator<SimpleEntity> iter = result.iterator();
SimpleEntity alfred = iter.next();
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
assertThat(alfred.intermediates).extracting(d -> d.intermediateName).containsExactlyInAnyOrder("Inami", "Ina",
"Ion");
assertThat(alfred.findInIntermediates("Inami").dummies).extracting(d -> d.dummyName)
.containsExactlyInAnyOrder("Dustin", "Dora");
assertThat(alfred.findInIntermediates("Ina").dummies).extracting(d -> d.dummyName)
.containsExactlyInAnyOrder("Dotty");
assertThat(alfred.findInIntermediates("Ion").dummies).isEmpty();
SimpleEntity bonJovy = iter.next();
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
assertThat(bonJovy.intermediates).extracting(d -> d.intermediateName).containsExactlyInAnyOrder("Judith", "Joel",
"Justin");
assertThat(bonJovy.findInIntermediates("Judith").dummies).extracting(d -> d.dummyName)
.containsExactlyInAnyOrder("Ephraim", "Erin");
assertThat(bonJovy.findInIntermediates("Joel").dummies).extracting(d -> d.dummyName).containsExactly("Erika");
assertThat(bonJovy.findInIntermediates("Justin").dummyList).isEmpty();
}
}
@Nested
class Lists {
@Test // GH-1446
void extractSingleListReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
1, 0, "Dummy Alfred", //
1, 1, "Dummy Berta", //
1, 2, "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
}
@Test // GH-1446
void extractSingleUnorderedListReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
1, 0, "Dummy Alfred", //
1, 2, "Dummy Carl", 1, 1, "Dummy Berta" //
);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
}
@Test // GH-1446
void extractListReferenceAndSimpleProperty() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("name"), column("dummyList", KEY), column("dummyList.dummyName")), //
1, "Simplicissimus", 0, "Dummy Alfred", //
1, null, 1, "Dummy Berta", //
1, null, 2, "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
}
@Test // GH-1446
void extractMultipleCollectionReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
column("dummyList", KEY), column("dummyList.dummyName"), //
column("otherDummies"), column("otherDummies.dummyName")), //
1, 0, "Dummy Alfred", 1, "Other Ephraim", //
1, 1, "Dummy Berta", 1, "Other Zeno", //
1, 2, "Dummy Carl", null, null);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
}
@Test // GH-1446
void extractNestedListsWithId() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
column("intermediateList", KEY), column("intermediateList.iId"), column("intermediateList.intermediateName"), //
column("intermediateList.dummyList", KEY), column("intermediateList.dummyList.dummyName")), //
1, "Alfred", 0, 23, "Inami", 0, "Dustin", //
1, null, 0, 23, null, 1, "Dora", //
1, null, 1, 24, "Ina", 0, "Dotty", //
1, null, 2, 25, "Ion", null, null, //
2, "Bon Jovi", 0, 26, "Judith", 0, "Ephraim", //
2, null, 0, 26, null, 1, "Erin", //
2, null, 1, 27, "Joel", 0, "Erika", //
2, null, 2, 28, "Justin", null, null //
);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateList.size())
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
final Iterator<SimpleEntity> iter = result.iterator();
SimpleEntity alfred = iter.next();
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
assertThat(alfred.intermediateList).extracting(d -> d.intermediateName).containsExactly("Inami", "Ina", "Ion");
assertThat(alfred.findInIntermediateList("Inami").dummyList).extracting(d -> d.dummyName)
.containsExactly("Dustin", "Dora");
assertThat(alfred.findInIntermediateList("Ina").dummyList).extracting(d -> d.dummyName).containsExactly("Dotty");
assertThat(alfred.findInIntermediateList("Ion").dummyList).isEmpty();
SimpleEntity bonJovy = iter.next();
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
assertThat(bonJovy.intermediateList).extracting(d -> d.intermediateName).containsExactly("Judith", "Joel",
"Justin");
assertThat(bonJovy.findInIntermediateList("Judith").dummyList).extracting(d -> d.dummyName)
.containsExactly("Ephraim", "Erin");
assertThat(bonJovy.findInIntermediateList("Joel").dummyList).extracting(d -> d.dummyName)
.containsExactly("Erika");
assertThat(bonJovy.findInIntermediateList("Justin").dummyList).isEmpty();
}
@Test // GH-1446
void extractNestedListsWithOutId() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
column("intermediateListNoId", KEY), column("intermediateListNoId.intermediateName"), //
column("intermediateListNoId.dummyList", KEY), column("intermediateListNoId.dummyList.dummyName")), //
1, "Alfred", 0, "Inami", 0, "Dustin", //
1, null, 0, null, 1, "Dora", //
1, null, 1, "Ina", 0, "Dotty", //
1, null, 2, "Ion", null, null, //
2, "Bon Jovi", 0, "Judith", 0, "Ephraim", //
2, null, 0, null, 1, "Erin", //
2, null, 1, "Joel", 0, "Erika", //
2, null, 2, "Justin", null, null //
);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateListNoId.size())
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
final Iterator<SimpleEntity> iter = result.iterator();
SimpleEntity alfred = iter.next();
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
assertThat(alfred.intermediateListNoId).extracting(d -> d.intermediateName).containsExactly("Inami", "Ina",
"Ion");
assertThat(alfred.findInIntermediateListNoId("Inami").dummyList).extracting(d -> d.dummyName)
.containsExactly("Dustin", "Dora");
assertThat(alfred.findInIntermediateListNoId("Ina").dummyList).extracting(d -> d.dummyName)
.containsExactly("Dotty");
assertThat(alfred.findInIntermediateListNoId("Ion").dummyList).isEmpty();
SimpleEntity bonJovy = iter.next();
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
assertThat(bonJovy.intermediateListNoId).extracting(d -> d.intermediateName).containsExactly("Judith", "Joel",
"Justin");
assertThat(bonJovy.findInIntermediateListNoId("Judith").dummyList).extracting(d -> d.dummyName)
.containsExactly("Ephraim", "Erin");
assertThat(bonJovy.findInIntermediateListNoId("Joel").dummyList).extracting(d -> d.dummyName)
.containsExactly("Erika");
assertThat(bonJovy.findInIntermediateListNoId("Justin").dummyList).isEmpty();
}
}
@Nested
class Maps {
@Test // GH-1446
void extractSingleMapReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("dummyMap", KEY), column("dummyMap.dummyName")), //
1, "alpha", "Dummy Alfred", //
1, "beta", "Dummy Berta", //
1, "gamma", "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
}
@Test // GH-1446
void extractMapReferenceAndSimpleProperty() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id1"), column("name"), column("dummyMap", KEY), column("dummyMap.dummyName")), //
1, "Simplicissimus", "alpha", "Dummy Alfred", //
1, null, "beta", "Dummy Berta", //
1, null, "gamma", "Dummy Carl");
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
}
@Test // GH-1446
void extractMultipleCollectionReference() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
column("dummyMap", KEY), column("dummyMap.dummyName"), //
column("otherDummies"), column("otherDummies.dummyName")), //
1, "alpha", "Dummy Alfred", 1, "Other Ephraim", //
1, "beta", "Dummy Berta", 1, "Other Zeno", //
1, "gamma", "Dummy Carl", null, null);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
}
@Test // GH-1446
void extractNestedMapsWithId() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
column("intermediateMap", KEY), column("intermediateMap.iId"), column("intermediateMap.intermediateName"), //
column("intermediateMap.dummyMap", KEY), column("intermediateMap.dummyMap.dummyName")), //
1, "Alfred", "alpha", 23, "Inami", "omega", "Dustin", //
1, null, "alpha", 23, null, "zeta", "Dora", //
1, null, "beta", 24, "Ina", "eta", "Dotty", //
1, null, "gamma", 25, "Ion", null, null, //
2, "Bon Jovi", "phi", 26, "Judith", "theta", "Ephraim", //
2, null, "phi", 26, null, "jota", "Erin", //
2, null, "chi", 27, "Joel", "sigma", "Erika", //
2, null, "psi", 28, "Justin", null, null //
);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateMap.size())
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
final Iterator<SimpleEntity> iter = result.iterator();
SimpleEntity alfred = iter.next();
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
assertThat(alfred.intermediateMap.get("alpha").dummyMap.get("omega").dummyName).isEqualTo("Dustin");
assertThat(alfred.intermediateMap.get("alpha").dummyMap.get("zeta").dummyName).isEqualTo("Dora");
assertThat(alfred.intermediateMap.get("beta").dummyMap.get("eta").dummyName).isEqualTo("Dotty");
assertThat(alfred.intermediateMap.get("gamma").dummyMap).isEmpty();
SimpleEntity bonJovy = iter.next();
assertThat(bonJovy.intermediateMap.get("phi").dummyMap.get("theta").dummyName).isEqualTo("Ephraim");
assertThat(bonJovy.intermediateMap.get("phi").dummyMap.get("jota").dummyName).isEqualTo("Erin");
assertThat(bonJovy.intermediateMap.get("chi").dummyMap.get("sigma").dummyName).isEqualTo("Erika");
assertThat(bonJovy.intermediateMap.get("psi").dummyMap).isEmpty();
}
@Test // GH-1446
void extractNestedMapsWithOutId() {
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
column("intermediateMapNoId", KEY), column("intermediateMapNoId.intermediateName"), //
column("intermediateMapNoId.dummyMap", KEY), column("intermediateMapNoId.dummyMap.dummyName")), //
1, "Alfred", "alpha", "Inami", "omega", "Dustin", //
1, null, "alpha", null, "zeta", "Dora", //
1, null, "beta", "Ina", "eta", "Dotty", //
1, null, "gamma", "Ion", null, null, //
2, "Bon Jovi", "phi", "Judith", "theta", "Ephraim", //
2, null, "phi", null, "jota", "Erin", //
2, null, "chi", "Joel", "sigma", "Erika", //
2, null, "psi", "Justin", null, null //
);
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateMapNoId.size())
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
final Iterator<SimpleEntity> iter = result.iterator();
SimpleEntity alfred = iter.next();
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
assertThat(alfred.intermediateMapNoId.get("alpha").dummyMap.get("omega").dummyName).isEqualTo("Dustin");
assertThat(alfred.intermediateMapNoId.get("alpha").dummyMap.get("zeta").dummyName).isEqualTo("Dora");
assertThat(alfred.intermediateMapNoId.get("beta").dummyMap.get("eta").dummyName).isEqualTo("Dotty");
assertThat(alfred.intermediateMapNoId.get("gamma").dummyMap).isEmpty();
SimpleEntity bonJovy = iter.next();
assertThat(bonJovy.intermediateMapNoId.get("phi").dummyMap.get("theta").dummyName).isEqualTo("Ephraim");
assertThat(bonJovy.intermediateMapNoId.get("phi").dummyMap.get("jota").dummyName).isEqualTo("Erin");
assertThat(bonJovy.intermediateMapNoId.get("chi").dummyMap.get("sigma").dummyName).isEqualTo("Erika");
assertThat(bonJovy.intermediateMapNoId.get("psi").dummyMap).isEmpty();
}
}
private String column(String path) {
return column(path, NORMAL);
}
private String column(String path, ColumnType columnType) {
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
SimpleEntity.class);
return column(context.getAggregatePath(propertyPath)) + (columnType == KEY ? "_key" : "");
}
private String column(AggregatePath path) {
return path.toDotPath();
}
enum ColumnType {
NORMAL, KEY
}
private static class SimpleEntity {
@Id long id1;
String name;
DummyEntity dummy;
@Embedded.Nullable DummyEntity embeddedNullable;
@Embedded.Empty DummyEntity embeddedNonNull;
Set<Intermediate> intermediates;
Set<DummyEntity> dummies;
Set<DummyEntity> otherDummies;
List<DummyEntity> dummyList;
List<Intermediate> intermediateList;
List<IntermediateNoId> intermediateListNoId;
Map<String, DummyEntity> dummyMap;
Map<String, Intermediate> intermediateMap;
Map<String, IntermediateNoId> intermediateMapNoId;
Intermediate findInIntermediates(String name) {
for (Intermediate intermediate : intermediates) {
if (intermediate.intermediateName.equals(name)) {
return intermediate;
}
}
fail("No intermediate with name " + name + " found in intermediates.");
return null;
}
Intermediate findInIntermediateList(String name) {
for (Intermediate intermediate : intermediateList) {
if (intermediate.intermediateName.equals(name)) {
return intermediate;
}
}
fail("No intermediate with name " + name + " found in intermediateList.");
return null;
}
IntermediateNoId findInIntermediateListNoId(String name) {
for (IntermediateNoId intermediate : intermediateListNoId) {
if (intermediate.intermediateName.equals(name)) {
return intermediate;
}
}
fail("No intermediates with name " + name + " found in intermediateListNoId.");
return null;
}
}
private static class Intermediate {
@Id long iId;
String intermediateName;
Set<DummyEntity> dummies;
List<DummyEntity> dummyList;
Map<String, DummyEntity> dummyMap;
}
private static class IntermediateNoId {
String intermediateName;
Set<DummyEntity> dummies;
List<DummyEntity> dummyList;
Map<String, DummyEntity> dummyMap;
}
private static class DummyEntity {
String dummyName;
Long longValue;
}
private record DummyRecord(Long id1, String name) {
}
}

View File

@@ -0,0 +1,272 @@
/*
* 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.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import javax.naming.OperationNotSupportedException;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.mockito.Mockito.*;
/**
* Utility for mocking ResultSets for tests.
*
* @author Jens Schauder
*/
class ResultSetTestUtil {
static ResultSet mockResultSet(List<String> columns, Object... values) {
Assert.isTrue( //
values.length % columns.size() == 0, //
String //
.format( //
"Number of values [%d] must be a multiple of the number of columns [%d]", //
values.length, //
columns.size() //
) //
);
List<Map<String, Object>> result = convertValues(columns, values);
return mock(ResultSet.class, new ResultSetAnswer(columns, result));
}
private static List<Map<String, Object>> convertValues(List<String> columns, Object[] values) {
List<Map<String, Object>> result = new ArrayList<>();
int index = 0;
while (index < values.length) {
Map<String, Object> row = new LinkedCaseInsensitiveMap<>();
result.add(row);
for (String column : columns) {
row.put(column, values[index]);
index++;
}
}
return result;
}
private static class ResultSetAnswer implements Answer<Object> {
private final List<String> names;
private final List<Map<String, Object>> values;
private int index = -1;
ResultSetAnswer(List<String> names, List<Map<String, Object>> values) {
this.names = names;
this.values = values;
}
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
switch (invocation.getMethod().getName()) {
case "next" -> {
return next();
}
case "getObject" -> {
Object argument = invocation.getArgument(0);
String name = argument instanceof Integer ? names.get(((Integer) argument) - 1) : (String) argument;
return getObject(name);
}
case "isAfterLast" -> {
return isAfterLast();
}
case "isBeforeFirst" -> {
return isBeforeFirst();
}
case "getRow" -> {
return isAfterLast() || isBeforeFirst() ? 0 : index + 1;
}
case "toString" -> {
return this.toString();
}
case "findColumn" -> {
return isThereAColumnNamed(invocation.getArgument(0));
}
case "getMetaData" -> {
return new MockedMetaData();
}
default -> throw new OperationNotSupportedException(invocation.getMethod().getName());
}
}
private int isThereAColumnNamed(String name) {
throw new UnsupportedOperationException("duh");
// Optional<Map<String, Object>> first = values.stream().filter(s -> s.equals(name)).findFirst();
// return (first.isPresent()) ? 1 : 0;
}
private boolean isAfterLast() {
return index >= values.size() && !values.isEmpty();
}
private boolean isBeforeFirst() {
return index < 0 && !values.isEmpty();
}
private Object getObject(String column) throws SQLException {
Map<String, Object> rowMap = values.get(index);
if (!rowMap.containsKey(column)) {
throw new SQLException(String.format("Trying to access a column (%s) that does not exist", column));
}
return rowMap.get(column);
}
private boolean next() {
index++;
return index < values.size();
}
private class MockedMetaData implements ResultSetMetaData {
@Override
public int getColumnCount() {
return names.size();
}
@Override
public boolean isAutoIncrement(int i) {
return false;
}
@Override
public boolean isCaseSensitive(int i) {
return false;
}
@Override
public boolean isSearchable(int i) {
return false;
}
@Override
public boolean isCurrency(int i) {
return false;
}
@Override
public int isNullable(int i) {
return 0;
}
@Override
public boolean isSigned(int i) {
return false;
}
@Override
public int getColumnDisplaySize(int i) {
return 0;
}
@Override
public String getColumnLabel(int i) {
return names.get(i - 1);
}
@Override
public String getColumnName(int i) {
return null;
}
@Override
public String getSchemaName(int i) {
return null;
}
@Override
public int getPrecision(int i) {
return 0;
}
@Override
public int getScale(int i) {
return 0;
}
@Override
public String getTableName(int i) {
return null;
}
@Override
public String getCatalogName(int i) {
return null;
}
@Override
public int getColumnType(int i) {
return 0;
}
@Override
public String getColumnTypeName(int i) {
return null;
}
@Override
public boolean isReadOnly(int i) {
return false;
}
@Override
public boolean isWritable(int i) {
return false;
}
@Override
public boolean isDefinitelyWritable(int i) {
return false;
}
@Override
public String getColumnClassName(int i) {
return null;
}
@Override
public <T> T unwrap(Class<T> aClass) {
return null;
}
@Override
public boolean isWrapperFor(Class<?> aClass) {
return false;
}
}
}
}

View File

@@ -160,8 +160,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
repository.saveAll(asList(entityA, entityB, entityC));
assertThat(repository.findByEnumTypeIn(Set.of(Direction.LEFT, Direction.RIGHT)))
.extracting(entity -> entity.direction)
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
.extracting(entity -> entity.direction).containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
}
@Test // GH-1212
@@ -175,8 +174,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
entityC.direction = Direction.RIGHT;
repository.saveAll(asList(entityA, entityB, entityC));
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER)))
.extracting(entity -> entity.direction)
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER))).extracting(entity -> entity.direction)
.containsExactly(Direction.CENTER);
}

View File

@@ -19,13 +19,11 @@ import javax.sql.DataSource;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.testcontainers.containers.MSSQLServerContainer;
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
/**
* {@link DataSource} setup for PostgreSQL.
* <p>
@@ -36,14 +34,14 @@ import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
* @see <a href="https://github.com/testcontainers/testcontainers-java/tree/master/modules/mssqlserver"></a>
*/
@Configuration
@Profile({"mssql"})
@Profile({ "mssql" })
public class MsSqlDataSourceConfiguration extends DataSourceConfiguration {
public static final String MS_SQL_SERVER_VERSION = "mcr.microsoft.com/mssql/server:2019-CU16-ubuntu-20.04";
public static final String MS_SQL_SERVER_VERSION = "mcr.microsoft.com/mssql/server:2022-CU5-ubuntu-20.04";
private static MSSQLServerContainer<?> MSSQL_CONTAINER;
@Override
protected DataSource createDataSource() {
@Override
protected DataSource createDataSource() {
if (MSSQL_CONTAINER == null) {
@@ -54,14 +52,13 @@ public class MsSqlDataSourceConfiguration extends DataSourceConfiguration {
MSSQL_CONTAINER = container;
}
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
sqlServerDataSource.setURL(MSSQL_CONTAINER.getJdbcUrl());
sqlServerDataSource.setUser(MSSQL_CONTAINER.getUsername());
sqlServerDataSource.setPassword(MSSQL_CONTAINER.getPassword());
return sqlServerDataSource;
}
return sqlServerDataSource;
}
@Override
protected void customizePopulator(ResourceDatabasePopulator populator) {

View File

@@ -1,3 +1,3 @@
DROP TABLE DUMMY_ENTITY;
DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS;
CREATE TABLE DUMMY_ENTITY ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY);

View File

@@ -1,3 +1,3 @@
DROP TABLE DUMMY_ENTITY;
DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS;
CREATE TABLE DUMMY_ENTITY ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY);