Introduce MappingRelationalConverter.

Add sophisticated converter to read aggregates from a RowDocument including support for maps, collections, subdocuments, and embeddables considering registered converters.
Use ResultSetRowDocumentExtractor to extract result multi-sets into RowDocument and then later apply object mapping.

Original pull request #1604
Closes #1586
This commit is contained in:
Mark Paluch
2023-09-04 16:08:38 +02:00
committed by Jens Schauder
parent 48b037fb9b
commit d6d99571b1
23 changed files with 2490 additions and 1434 deletions

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.jdbc.core.convert;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -27,73 +29,87 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti
import org.springframework.data.relational.core.sqlgeneration.AliasFactory;
import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator;
import org.springframework.data.relational.core.sqlgeneration.SqlGenerator;
import org.springframework.data.relational.domain.RowDocument;
import org.springframework.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}
* through {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. Results are converterd into an
* intermediate {@link ResultSetRowDocumentExtractor RowDocument} and mapped via
* {@link org.springframework.data.relational.core.conversion.RelationalConverter#read(Class, RowDocument)}.
*
* @param <T> the type of aggregate produced by this reader.
* @author Jens Schauder
* @author Mark Paluch
* @since 3.2
*/
class AggregateReader<T> {
private final RelationalPersistentEntity<T> aggregate;
private final RelationalPersistentEntity<T> entity;
private final org.springframework.data.relational.core.sqlgeneration.SqlGenerator sqlGenerator;
private final JdbcConverter converter;
private final NamedParameterJdbcOperations jdbcTemplate;
private final AggregateResultSetExtractor<T> extractor;
private final ResultSetRowDocumentExtractor extractor;
AggregateReader(Dialect dialect, JdbcConverter converter, AliasFactory aliasFactory,
NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity<T> aggregate) {
NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity<T> entity) {
this.converter = converter;
this.aggregate = aggregate;
this.entity = entity;
this.jdbcTemplate = jdbcTemplate;
this.sqlGenerator = new CachingSqlGenerator(
new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, aggregate));
new SingleQuerySqlGenerator(converter.getMappingContext(), aliasFactory, dialect, entity));
this.extractor = new AggregateResultSetExtractor<>(aggregate, converter, createPathToColumnMapping(aliasFactory));
this.extractor = new ResultSetRowDocumentExtractor(converter.getMappingContext(),
createPathToColumnMapping(aliasFactory));
}
public List<T> findAll() {
Iterable<T> result = jdbcTemplate.query(sqlGenerator.findAll(), extractor);
Assert.state(result != null, "result is null");
return (List<T>) result;
return jdbcTemplate.query(sqlGenerator.findAll(), this::extractAll);
}
@Nullable
public T findById(Object id) {
id = converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation());
id = converter.writeValue(id, entity.getRequiredIdProperty().getTypeInformation());
Iterator<T> result = jdbcTemplate.query(sqlGenerator.findById(), Map.of("id", id), extractor).iterator();
return jdbcTemplate.query(sqlGenerator.findById(), Map.of("id", id), rs -> {
T returnValue = result.hasNext() ? result.next() : null;
Iterator<RowDocument> iterate = extractor.iterate(entity, rs);
if (iterate.hasNext()) {
if (result.hasNext()) {
throw new IncorrectResultSizeDataAccessException(1);
}
return returnValue;
RowDocument object = iterate.next();
if (iterate.hasNext()) {
throw new IncorrectResultSizeDataAccessException(1);
}
return converter.read(entity.getType(), object);
}
return null;
});
}
public Iterable<T> findAllById(Iterable<?> ids) {
List<Object> convertedIds = new ArrayList<>();
for (Object id : ids) {
convertedIds.add(converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation()));
convertedIds.add(converter.writeValue(id, entity.getRequiredIdProperty().getTypeInformation()));
}
return jdbcTemplate.query(sqlGenerator.findAllById(), Map.of("ids", convertedIds), extractor);
return jdbcTemplate.query(sqlGenerator.findAllById(), Map.of("ids", convertedIds), this::extractAll);
}
private List<T> extractAll(ResultSet rs) throws SQLException {
Iterator<RowDocument> iterate = extractor.iterate(entity, rs);
List<T> resultList = new ArrayList<>();
while (iterate.hasNext()) {
resultList.add(converter.read(entity.getType(), iterate.next()));
}
return resultList;
}
private PathToColumnMapping createPathToColumnMapping(AliasFactory aliasFactory) {
@@ -117,8 +133,8 @@ 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
* @since 3.2
*/
static class CachingSqlGenerator implements org.springframework.data.relational.core.sqlgeneration.SqlGenerator {

View File

@@ -1,596 +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 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
* @author Jens Schauder
* @since 3.2
*/
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 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(RelationalPersistentEntity<T> rootEntity, JdbcConverter converter,
PathToColumnMapping pathToColumn) {
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.rootEntity = rootEntity;
this.converter = converter;
this.context = converter.getMappingContext();
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

@@ -25,7 +25,6 @@ import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.ConverterNotFoundException;
@@ -45,7 +44,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
import org.springframework.data.relational.core.conversion.MappingRelationalConverter;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.AggregatePath;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
@@ -72,7 +71,7 @@ import org.springframework.util.Assert;
* @see CustomConversions
* @since 1.1
*/
public class BasicJdbcConverter extends BasicRelationalConverter implements JdbcConverter, ApplicationContextAware {
public class BasicJdbcConverter extends MappingRelationalConverter implements JdbcConverter, ApplicationContextAware {
private static final Log LOG = LogFactory.getLog(BasicJdbcConverter.class);
private static final Converter<Iterable<?>, Map<?, ?>> ITERABLE_OF_ENTRY_TO_MAP_CONVERTER = new IterableOfEntryToMapConverter();

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.jdbc.core.convert;
import java.sql.Array;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
@@ -58,7 +59,13 @@ class ResultSetRowDocumentExtractor {
@Override
public Object getObject(ResultSet row, int index) {
try {
return JdbcUtils.getResultSetValue(row, index);
Object resultSetValue = JdbcUtils.getResultSetValue(row, index);
if (resultSetValue instanceof Array a) {
return a.getArray();
}
return resultSetValue;
} catch (SQLException e) {
throw new DataRetrievalFailureException("Cannot retrieve column " + index + " from ResultSet", e);
}
@@ -140,8 +147,6 @@ class ResultSetRowDocumentExtractor {
private final RelationalPersistentEntity<?> rootEntity;
private final Integer identifierIndex;
private final AggregateContext<ResultSet> aggregateContext;
private final boolean initiallyConsumed;
private boolean hasNext;
RowDocumentIterator(RelationalPersistentEntity<?> entity, ResultSet resultSet) throws SQLException {
@@ -150,9 +155,10 @@ class ResultSetRowDocumentExtractor {
if (resultSet.isBeforeFirst()) {
hasNext = resultSet.next();
} else {
hasNext = !resultSet.isAfterLast();
}
this.initiallyConsumed = resultSet.isAfterLast();
this.rootPath = context.getAggregatePath(entity);
this.rootEntity = entity;
@@ -166,11 +172,6 @@ class ResultSetRowDocumentExtractor {
@Override
public boolean hasNext() {
if (initiallyConsumed) {
return false;
}
return hasNext;
}
@@ -182,6 +183,7 @@ class ResultSetRowDocumentExtractor {
Object key = ResultSetAdapter.INSTANCE.getObject(resultSet, identifierIndex);
try {
do {
Object nextKey = ResultSetAdapter.INSTANCE.getObject(resultSet, identifierIndex);

View File

@@ -73,8 +73,8 @@ abstract class RowDocumentExtractorSupport {
protected static class AggregateContext<RS> {
private final TabularResultAdapter<RS> adapter;
final RelationalMappingContext context;
final PathToColumnMapping propertyToColumn;
private final RelationalMappingContext context;
private final PathToColumnMapping propertyToColumn;
private final Map<String, Integer> columnMap;
protected AggregateContext(TabularResultAdapter<RS> adapter, RelationalMappingContext context,
@@ -174,8 +174,11 @@ abstract class RowDocumentExtractorSupport {
private final AggregateContext<RS> aggregateContext;
private final RelationalPersistentEntity<?> entity;
private final AggregatePath basePath;
private RowDocument result;
private String keyColumnName;
private @Nullable Object key;
private final Map<RelationalPersistentProperty, TabularSink<RS>> readerState = new LinkedHashMap<>();
public RowDocumentSink(AggregateContext<RS> aggregateContext, RelationalPersistentEntity<?> entity,
@@ -183,6 +186,15 @@ abstract class RowDocumentExtractorSupport {
this.aggregateContext = aggregateContext;
this.entity = entity;
this.basePath = basePath;
String keyColumnName;
if (entity.hasIdProperty()) {
keyColumnName = aggregateContext.getColumnName(basePath.append(entity.getRequiredIdProperty()));
} else {
keyColumnName = aggregateContext.getColumnName(basePath);
}
this.keyColumnName = keyColumnName;
}
@Override
@@ -206,17 +218,29 @@ abstract class RowDocumentExtractorSupport {
*/
private void readFirstRow(RS row, RowDocument document) {
// key marker
if (aggregateContext.containsColumn(keyColumnName)) {
key = aggregateContext.getObject(row, keyColumnName);
}
readEntity(row, document, basePath, entity);
}
private void readEntity(RS row, RowDocument document, AggregatePath basePath,
RelationalPersistentEntity<?> entity) {
for (RelationalPersistentProperty property : entity) {
AggregatePath path = basePath.append(property);
if (property.isQualified()) {
if (property.isEntity() && !property.isEmbedded() && (property.isCollectionLike() || property.isQualified())) {
readerState.put(property, new ContainerSink<>(aggregateContext, property, path));
continue;
}
if (property.isEmbedded()) {
collectEmbeddedValues(row, document, property, path);
RelationalPersistentEntity<?> embeddedEntity = aggregateContext.getRequiredPersistentEntity(property);
readEntity(row, document, path, embeddedEntity);
continue;
}
@@ -262,7 +286,11 @@ abstract class RowDocumentExtractorSupport {
}
}
return !result.isEmpty();
if (result.isEmpty() && key == null) {
return false;
}
return true;
}
@Override

View File

@@ -1,789 +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 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.RelationalPersistentProperty;
import org.springframework.data.relational.domain.RowDocument;
/**
* Unit tests for the {@link AggregateResultSetExtractor}.
*
* @author Jens Schauder
* @author Mark Paluch
*/
public class AggregateResultSetExtractorUnitTests {
RelationalMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
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);
ResultSetRowDocumentExtractor documentExtractor = new ResultSetRowDocumentExtractor(context, column);
@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() throws SQLException {
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"));
resultSet.close();
RowDocument document = documentExtractor.extractNextDocument(SimpleEntity.class, resultSet);
assertThat(document).containsEntry("id1", 1).containsEntry("name", "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.getPersistentEntity(type),
converter, column);
}
@Nested
class EmbeddedReference {
@Test // GH-1446
void embeddedGetsExtractedFromSingleRow() throws SQLException {
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"));
resultSet.close();
RowDocument document = documentExtractor.extractNextDocument(SimpleEntity.class, resultSet);
assertThat(document).containsEntry("id1", 1).containsEntry("dummy_name", "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() throws SQLException {
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"));
resultSet.close();
RowDocument document = documentExtractor.extractNextDocument(SimpleEntity.class, resultSet);
assertThat(document).containsKey("dummy").containsEntry("dummy",
new RowDocument().append("dummy_name", "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() throws SQLException {
AggregateResultSetExtractor<WithList> extractor = getExtractor(WithList.class);
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id", WithList.class), column("people", KEY, WithList.class),
column("people.name", WithList.class)), //
1, 0, "Dummy Alfred", //
1, 1, "Dummy Berta", //
1, 2, "Dummy Carl");
Iterable<WithList> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id).containsExactly(1L);
assertThat(result).flatExtracting(e -> e.people).extracting(e -> e.name) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
resultSet.close();
RowDocument document = documentExtractor.extractNextDocument(WithList.class, resultSet);
assertThat(document).containsKey("people");
List<RowDocument> dummy_list = document.getList("people");
assertThat(dummy_list).hasSize(3).contains(new RowDocument().append("name", "Dummy Alfred"))
.contains(new RowDocument().append("name", "Dummy Berta"))
.contains(new RowDocument().append("name", "Dummy Carl"));
}
@Test // GH-1446
void extractSingleUnorderedListReference() throws SQLException {
AggregateResultSetExtractor<WithList> extractor = getExtractor(WithList.class);
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
asList(column("id", WithList.class), column("people", KEY, WithList.class),
column("people.name", WithList.class)), //
1, 0, "Dummy Alfred", //
1, 2, "Dummy Carl", //
1, 1, "Dummy Berta" //
);
Iterable<WithList> result = extractor.extractData(resultSet);
assertThat(result).extracting(e -> e.id).containsExactly(1L);
assertThat(result).flatExtracting(e -> e.people).extracting(e -> e.name) //
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
resultSet.close();
RowDocument document = documentExtractor.extractNextDocument(WithList.class, resultSet);
assertThat(document).containsKey("people");
List<RowDocument> dummy_list = document.getList("people");
assertThat(dummy_list).hasSize(3).contains(new RowDocument().append("name", "Dummy Alfred"))
.contains(new RowDocument().append("name", "Dummy Berta"))
.contains(new RowDocument().append("name", "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, Class<?> entityType) {
return column(path, NORMAL, entityType);
}
private String column(String path, ColumnType columnType) {
return column(path, columnType, SimpleEntity.class);
}
private String column(String path, ColumnType columnType, Class<?> entityType) {
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
entityType);
return column(context.getAggregatePath(propertyPath)) + (columnType == KEY ? "_key" : "");
}
private String column(AggregatePath path) {
return path.toDotPath();
}
enum ColumnType {
NORMAL, KEY
}
private static class Person {
String name;
}
private static class PersonWithId {
@Id Long id;
String name;
}
private static class WithList {
@Id long id;
List<Person> people;
List<PersonWithId> peopleWithIds;
}
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,573 @@
/*
* 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 org.assertj.core.api.Assertions.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ThrowingConsumer;
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.RelationalPersistentProperty;
import org.springframework.data.relational.domain.RowDocument;
/**
* Unit tests for the {@link ResultSetRowDocumentExtractor}.
*
* @author Jens Schauder
* @author Mark Paluch
*/
public class ResultSetRowDocumentExtractorUnitTests {
RelationalMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
private final PathToColumnMapping column = new PathToColumnMapping() {
@Override
public String column(AggregatePath path) {
return ResultSetRowDocumentExtractorUnitTests.this.column(path);
}
@Override
public String keyColumn(AggregatePath path) {
return column(path) + "_key";
}
};
ResultSetRowDocumentExtractor documentExtractor = new ResultSetRowDocumentExtractor(context, column);
@Test // GH-1446
void emptyResultSetYieldsEmptyResult() {
Assertions.setMaxElementsForPrinting(20);
new ResultSetTester(WithEmbedded.class, context).resultSet(rsc -> {
rsc.withPaths("id1", "name");
}).run(resultSet -> {
assertThatIllegalStateException()
.isThrownBy(() -> documentExtractor.extractNextDocument(WithEmbedded.class, resultSet));
});
}
@Test // GH-1446
void singleSimpleEntityGetsExtractedFromSingleRow() throws SQLException {
testerFor(WithEmbedded.class).resultSet(rsc -> {
rsc.withPaths("id1", "name") //
.withRow(1, "Alfred");
}).run(document -> {
assertThat(document).containsEntry("id1", 1).containsEntry("name", "Alfred");
});
}
@Test // GH-1446
void multipleSimpleEntitiesGetExtractedFromMultipleRows() throws SQLException {
new ResultSetTester(WithEmbedded.class, context).resultSet(rsc -> {
rsc.withPaths("id1", "name") //
.withRow(1, "Alfred") //
.withRow(2, "Bertram");
}).run(resultSet -> {
RowDocument document = documentExtractor.extractNextDocument(WithEmbedded.class, resultSet);
assertThat(document).containsEntry("id1", 1).containsEntry("name", "Alfred");
RowDocument nextDocument = documentExtractor.extractNextDocument(WithEmbedded.class, resultSet);
assertThat(nextDocument).containsEntry("id1", 2).containsEntry("name", "Bertram");
});
}
@Nested
class EmbeddedReference {
@Test // GH-1446
void embeddedGetsExtractedFromSingleRow() {
testerFor(WithEmbedded.class).resultSet(rsc -> {
rsc.withPaths("id1", "embeddedNullable.dummyName") //
.withRow(1, "Imani");
}).run(document -> {
assertThat(document).containsEntry("id1", 1).containsEntry("dummy_name", "Imani");
});
}
@Test // GH-1446
void emptyEmbeddedGetsExtractedFromSingleRow() throws SQLException {
testerFor(WithEmbedded.class).resultSet(rsc -> {
rsc.withPaths("id1", "embeddedNullable.dummyName") //
.withRow(1, null);
}).run(document -> {
assertThat(document).hasSize(1).containsEntry("id1", 1);
});
}
}
@Nested
class ToOneRelationships {
@Test // GH-1446
void entityReferenceGetsExtractedFromSingleRow() {
testerFor(WithOneToOne.class).resultSet(rsc -> {
rsc.withPaths("id1", "related", "related.dummyName") //
.withRow(1, 1, "Dummy Alfred");
}).run(document -> {
assertThat(document).containsKey("related").containsEntry("related",
new RowDocument().append("dummy_name", "Dummy Alfred"));
});
}
@Test // GH-1446
void nullEntityReferenceGetsExtractedFromSingleRow() {
testerFor(WithOneToOne.class).resultSet(rsc -> {
rsc.withPaths("id1", "related", "related.dummyName") //
.withRow(1, null, "Dummy Alfred");
}).run(document -> {
assertThat(document).containsKey("related").containsEntry("related",
new RowDocument().append("dummy_name", "Dummy Alfred"));
});
}
}
@Nested
class Sets {
@Test // GH-1446
void extractEmptySetReference() {
testerFor(WithSets.class).resultSet(rsc -> {
rsc.withPaths("id1", "first", "first.dummyName") //
.withRow(1, null, null)//
.withRow(1, null, null) //
.withRow(1, null, null);
}).run(document -> {
assertThat(document).hasSize(1).containsEntry("id1", 1);
});
}
@Test // GH-1446
void extractSingleSetReference() {
testerFor(WithSets.class).resultSet(rsc -> {
rsc.withPath("id1").withKey("first").withPath("first.dummyName") //
.withRow(1, 1, "Dummy Alfred")//
.withRow(1, 2, "Dummy Berta") //
.withRow(1, 3, "Dummy Carl");
}).run(document -> {
assertThat(document).containsEntry("id1", 1).containsEntry("first",
Arrays.asList(RowDocument.of("dummy_name", "Dummy Alfred"), RowDocument.of("dummy_name", "Dummy Berta"),
RowDocument.of("dummy_name", "Dummy Carl")));
});
}
@Test // GH-1446
void extractSetReferenceAndSimpleProperty() {
testerFor(WithSets.class).resultSet(rsc -> {
rsc.withPaths("id1", "name").withKey("first").withPath("first.dummyName") //
.withRow(1, "Simplicissimus", 1, "Dummy Alfred")//
.withRow(1, null, 2, "Dummy Berta") //
.withRow(1, null, 3, "Dummy Carl");
}).run(document -> {
assertThat(document).containsEntry("id1", 1).containsEntry("name", "Simplicissimus").containsEntry("first",
Arrays.asList(RowDocument.of("dummy_name", "Dummy Alfred"), RowDocument.of("dummy_name", "Dummy Berta"),
RowDocument.of("dummy_name", "Dummy Carl")));
});
}
@Test // GH-1446
void extractMultipleSetReference() {
testerFor(WithSets.class).resultSet(rsc -> {
rsc.withPaths("id1").withKey("first").withPath("first.dummyName").withKey("second").withPath("second.dummyName") //
.withRow(1, 1, "Dummy Alfred", 1, "Other Ephraim")//
.withRow(1, 2, "Dummy Berta", 2, "Other Zeno") //
.withRow(1, 3, "Dummy Carl", null, null);
}).run(document -> {
assertThat(document).hasSize(3)
.containsEntry("first",
Arrays.asList(RowDocument.of("dummy_name", "Dummy Alfred"), RowDocument.of("dummy_name", "Dummy Berta"),
RowDocument.of("dummy_name", "Dummy Carl")))
.containsEntry("second", Arrays.asList(RowDocument.of("dummy_name", "Other Ephraim"),
RowDocument.of("dummy_name", "Other Zeno")));
});
}
@Nested
class Lists {
@Test // GH-1446
void extractSingleListReference() {
testerFor(WithList.class).resultSet(rsc -> {
rsc.withPaths("id").withKey("withoutIds").withPath("withoutIds.name") //
.withRow(1, 1, "Dummy Alfred")//
.withRow(1, 2, "Dummy Berta") //
.withRow(1, 3, "Dummy Carl");
}).run(document -> {
assertThat(document).hasSize(2).containsEntry("without_ids",
Arrays.asList(RowDocument.of("name", "Dummy Alfred"), RowDocument.of("name", "Dummy Berta"),
RowDocument.of("name", "Dummy Carl")));
});
}
@Test // GH-1446
void extractSingleUnorderedListReference() {
testerFor(WithList.class).resultSet(rsc -> {
rsc.withPaths("id").withKey("withoutIds").withPath("withoutIds.name") //
.withRow(1, 0, "Dummy Alfred")//
.withRow(1, 2, "Dummy Carl") //
.withRow(1, 1, "Dummy Berta");
}).run(document -> {
assertThat(document).containsKey("without_ids");
List<RowDocument> dummy_list = document.getList("without_ids");
assertThat(dummy_list).hasSize(3).contains(new RowDocument().append("name", "Dummy Alfred"))
.contains(new RowDocument().append("name", "Dummy Berta"))
.contains(new RowDocument().append("name", "Dummy Carl"));
});
}
}
}
@Nested
class Maps {
@Test
// GH-1446
void extractSingleMapReference() {
testerFor(WithMaps.class).resultSet(rsc -> {
rsc.withPaths("id1").withKey("first").withPath("first.dummyName") //
.withRow(1, "alpha", "Dummy Alfred")//
.withRow(1, "beta", "Dummy Berta") //
.withRow(1, "gamma", "Dummy Carl");
}).run(document -> {
assertThat(document).containsEntry("first", Map.of("alpha", RowDocument.of("dummy_name", "Dummy Alfred"),
"beta", RowDocument.of("dummy_name", "Dummy Berta"), "gamma", RowDocument.of("dummy_name", "Dummy Carl")));
});
}
@Test
// GH-1446
void extractMultipleCollectionReference() {
testerFor(WithMapsAndList.class).resultSet(rsc -> {
rsc.withPaths("id1").withKey("map").withPath("map.dummyName").withKey("list").withPath("list.name") //
.withRow(1, "alpha", "Dummy Alfred", 1, "Other Ephraim")//
.withRow(1, "beta", "Dummy Berta", 2, "Other Zeno") //
.withRow(1, "gamma", "Dummy Carl", null, null);
}).run(document -> {
assertThat(document).containsEntry("map", Map.of("alpha", RowDocument.of("dummy_name", "Dummy Alfred"), //
"beta", RowDocument.of("dummy_name", "Dummy Berta"), //
"gamma", RowDocument.of("dummy_name", "Dummy Carl"))) //
.containsEntry("list",
Arrays.asList(RowDocument.of("name", "Other Ephraim"), RowDocument.of("name", "Other Zeno")));
});
}
@Test
// GH-1446
void extractNestedMapsWithId() {
testerFor(WithMaps.class).resultSet(rsc -> {
rsc.withPaths("id1", "name").withKey("intermediate")
.withPaths("intermediate.iId", "intermediate.intermediateName").withKey("intermediate.dummyMap")
.withPaths("intermediate.dummyMap.dummyName")
//
.withRow(1, "Alfred", "alpha", 23, "Inami", "omega", "Dustin") //
.withRow(1, null, "alpha", 23, null, "zeta", "Dora") //
.withRow(1, null, "beta", 24, "Ina", "eta", "Dotty") //
.withRow(1, null, "gamma", 25, "Ion", null, null);
}).run(document -> {
assertThat(document).containsEntry("id1", 1).containsEntry("name", "Alfred");
Map<String, Object> intermediate = document.getMap("intermediate");
assertThat(intermediate).containsKeys("alpha", "beta", "gamma");
RowDocument alpha = (RowDocument) intermediate.get("alpha");
assertThat(alpha).containsEntry("i_id", 23).containsEntry("intermediate_name", "Inami");
Map<String, Object> dummyMap = alpha.getMap("dummy_map");
assertThat(dummyMap).containsEntry("omega", RowDocument.of("dummy_name", "Dustin")).containsEntry("zeta",
RowDocument.of("dummy_name", "Dora"));
RowDocument gamma = (RowDocument) intermediate.get("gamma");
assertThat(gamma).hasSize(2).containsEntry("i_id", 25).containsEntry("intermediate_name", "Ion");
});
}
}
private String column(AggregatePath path) {
return path.toDotPath();
}
private static class WithEmbedded {
@Id long id1;
String name;
@Embedded.Nullable DummyEntity embeddedNullable;
@Embedded.Empty DummyEntity embeddedNonNull;
}
private static class WithOneToOne {
@Id long id1;
String name;
DummyEntity related;
}
private static class Person {
String name;
}
private static class PersonWithId {
@Id Long id;
String name;
}
private static class WithList {
@Id long id;
List<Person> withoutIds;
List<PersonWithId> withIds;
}
private static class WithSets {
@Id long id1;
String name;
Set<DummyEntity> first;
Set<DummyEntity> second;
}
private static class WithMaps {
@Id long id1;
String name;
Map<String, DummyEntity> first;
Map<String, Intermediate> intermediate;
Map<String, IntermediateNoId> noId;
}
private static class WithMapsAndList {
@Id long id1;
Map<String, DummyEntity> map;
List<Person> list;
}
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;
}
/**
* Configurer for a {@link ResultSet}.
*/
interface ResultSetConfigurer {
ResultSetConfigurer withColumns(String... columns);
/**
* Add mapped paths.
*
* @param path
* @return
*/
ResultSetConfigurer withPath(String path);
/**
* Add mapped paths.
*
* @param paths
* @return
*/
default ResultSetConfigurer withPaths(String... paths) {
for (String path : paths) {
withPath(path);
}
return this;
}
/**
* Add mapped key paths.
*
* @param path
* @return
*/
ResultSetConfigurer withKey(String path);
ResultSetConfigurer withRow(Object... values);
}
DocumentTester testerFor(Class<?> entityType) {
return new DocumentTester(entityType, context, documentExtractor);
}
private static class AbstractTester {
private final Class<?> entityType;
private final RelationalMappingContext context;
ResultSet resultSet;
AbstractTester(Class<?> entityType, RelationalMappingContext context) {
this.entityType = entityType;
this.context = context;
}
AbstractTester resultSet(Consumer<ResultSetConfigurer> configuration) {
List<Object> values = new ArrayList<>();
List<String> columns = new ArrayList<>();
ResultSetConfigurer configurer = new ResultSetConfigurer() {
@Override
public ResultSetConfigurer withColumns(String... columnNames) {
columns.addAll(Arrays.asList(columnNames));
return this;
}
public ResultSetConfigurer withPath(String path) {
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
entityType);
columns.add(context.getAggregatePath(propertyPath).toDotPath());
return this;
}
public ResultSetConfigurer withKey(String path) {
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
entityType);
columns.add(context.getAggregatePath(propertyPath).toDotPath() + "_key");
return this;
}
@Override
public ResultSetConfigurer withRow(Object... rowValues) {
values.addAll(Arrays.asList(rowValues));
return this;
}
};
configuration.accept(configurer);
this.resultSet = ResultSetTestUtil.mockResultSet(columns, values.toArray());
return this;
}
}
private static class DocumentTester extends AbstractTester {
private final Class<?> entityType;
private final ResultSetRowDocumentExtractor extractor;
DocumentTester(Class<?> entityType, RelationalMappingContext context, ResultSetRowDocumentExtractor extractor) {
super(entityType, context);
this.entityType = entityType;
this.extractor = extractor;
}
@Override
DocumentTester resultSet(Consumer<ResultSetConfigurer> configuration) {
super.resultSet(configuration);
return this;
}
public void run(ThrowingConsumer<RowDocument> action) {
try {
action.accept(extractor.extractNextDocument(entityType, resultSet));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
private static class ResultSetTester extends AbstractTester {
ResultSetTester(Class<?> entityType, RelationalMappingContext context) {
super(entityType, context);
}
@Override
ResultSetTester resultSet(Consumer<ResultSetConfigurer> configuration) {
super.resultSet(configuration);
return this;
}
public void run(ThrowingConsumer<ResultSet> action) {
action.accept(resultSet);
}
}
}

View File

@@ -136,11 +136,15 @@ class ResultSetTestUtil {
}
private boolean isBeforeFirst() {
return index < 0 && !values.isEmpty();
return index < 0;
}
private Object getObject(String column) throws SQLException {
if (index == -1) {
throw new SQLException("ResultSet.isBeforeFirst. Make sure to call next() before calling this method");
}
Map<String, Object> rowMap = values.get(index);
if (!rowMap.containsKey(column)) {