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:
committed by
Jens Schauder
parent
48b037fb9b
commit
d6d99571b1
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user