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
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -47,7 +47,7 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator;
|
||||
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
|
||||
import org.springframework.data.r2dbc.mapping.OutboundRow;
|
||||
import org.springframework.data.r2dbc.support.ArrayUtils;
|
||||
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.dialect.ArrayColumns;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
@@ -66,7 +66,7 @@ import org.springframework.util.CollectionUtils;
|
||||
* @author Mark Paluch
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public class MappingR2dbcConverter extends BasicRelationalConverter implements R2dbcConverter {
|
||||
public class MappingR2dbcConverter extends MappingRelationalConverter implements R2dbcConverter {
|
||||
|
||||
/**
|
||||
* Creates a new {@link MappingR2dbcConverter} given {@link MappingContext}.
|
||||
|
||||
@@ -51,6 +51,7 @@ public interface R2dbcConverter
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
@Override
|
||||
ConversionService getConversionService();
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
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.relational.domain.RowDocument;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -125,6 +126,11 @@ public class BasicRelationalConverter implements RelationalConverter {
|
||||
return new ConvertingPropertyAccessor<>(accessor, conversionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R read(Class<R> type, RowDocument source) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T createInstance(PersistentEntity<T, RelationalPersistentProperty> entity,
|
||||
Function<Parameter<?, RelationalPersistentProperty>, Object> parameterValueProvider) {
|
||||
@@ -271,9 +277,8 @@ public class BasicRelationalConverter implements RelationalConverter {
|
||||
* @param type {@link TypeInformation} into which the value is to be converted. Must not be {@code null}.
|
||||
* @return the converted value if a conversion applies or the original value. Might return {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation<?> type) {
|
||||
protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation<?> type) {
|
||||
|
||||
Class<?> target = type.getType();
|
||||
if (ClassUtils.isAssignableValue(target, value)) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.expression.MapAccessor;
|
||||
import org.springframework.data.relational.domain.RowDocument;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.expression.PropertyAccessor} to allow entity based field access to
|
||||
* {@link org.springframework.data.relational.domain.RowDocument}s.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class DocumentPropertyAccessor extends MapAccessor {
|
||||
|
||||
static final MapAccessor INSTANCE = new DocumentPropertyAccessor();
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return new Class[] { RowDocument.class };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
|
||||
|
||||
if (target == null) {
|
||||
return TypedValue.NULL;
|
||||
}
|
||||
|
||||
Map<String, Object> source = (Map<String, Object>) target;
|
||||
|
||||
Object value = source.get(name);
|
||||
return value == null ? TypedValue.NULL : new TypedValue(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.core.CollectionFactory;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.mapping.InstanceCreatorMetadata;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.Parameter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
|
||||
import org.springframework.data.mapping.model.EntityInstantiator;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
|
||||
import org.springframework.data.mapping.model.PropertyValueProvider;
|
||||
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.mapping.Embedded;
|
||||
import org.springframework.data.relational.core.mapping.Embedded.OnEmpty;
|
||||
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.relational.domain.RowDocument;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link RelationalConverter} that uses a {@link MappingContext} to apply sophisticated mapping of domain objects from
|
||||
* {@link RowDocument}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
public class MappingRelationalConverter extends BasicRelationalConverter {
|
||||
|
||||
private SpELContext spELContext;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MappingRelationalConverter} given the new {@link RelationalMappingContext}.
|
||||
*
|
||||
* @param context must not be {@literal null}.
|
||||
*/
|
||||
public MappingRelationalConverter(RelationalMappingContext context) {
|
||||
super(context);
|
||||
this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MappingRelationalConverter} given the new {@link RelationalMappingContext} and
|
||||
* {@link CustomConversions}.
|
||||
*
|
||||
* @param context must not be {@literal null}.
|
||||
* @param conversions must not be {@literal null}.
|
||||
*/
|
||||
public MappingRelationalConverter(RelationalMappingContext context, CustomConversions conversions) {
|
||||
super(context, conversions);
|
||||
this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConversionContext}.
|
||||
*
|
||||
* @return the {@link ConversionContext}.
|
||||
*/
|
||||
protected ConversionContext getConversionContext(ObjectPath path) {
|
||||
|
||||
Assert.notNull(path, "ObjectPath must not be null");
|
||||
|
||||
return new DefaultConversionContext(this, getConversions(), path, this::readAggregate, this::readCollectionOrArray,
|
||||
this::readMap, this::getPotentiallyConvertedSimpleRead);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a {@link RowDocument} into the requested {@link Class aggregate type}.
|
||||
*
|
||||
* @param type target aggregate type.
|
||||
* @param source source {@link RowDocument}.
|
||||
* @return the converted object.
|
||||
* @param <R> aggregate type.
|
||||
*/
|
||||
@Override
|
||||
public <R> R read(Class<R> type, RowDocument source) {
|
||||
return read(TypeInformation.of(type), source);
|
||||
}
|
||||
|
||||
protected <S extends Object> S read(TypeInformation<S> type, RowDocument source) {
|
||||
return readAggregate(getConversionContext(ObjectPath.ROOT), source, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversion method to materialize an object from a {@link RowDocument document}. Can be overridden by subclasses.
|
||||
*
|
||||
* @param context must not be {@literal null}
|
||||
* @param document must not be {@literal null}
|
||||
* @param typeHint the {@link TypeInformation} to be used to unmarshall this {@link RowDocument}.
|
||||
* @return the converted object, will never be {@literal null}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <S extends Object> S readAggregate(ConversionContext context, RowDocument document,
|
||||
TypeInformation<? extends S> typeHint) {
|
||||
|
||||
Class<? extends S> rawType = typeHint.getType();
|
||||
|
||||
if (getConversions().hasCustomReadTarget(document.getClass(), rawType)) {
|
||||
return doConvert(document, rawType, typeHint.getType());
|
||||
}
|
||||
|
||||
if (RowDocument.class.isAssignableFrom(rawType)) {
|
||||
return (S) document;
|
||||
}
|
||||
|
||||
if (typeHint.isMap()) {
|
||||
return context.convert(document, typeHint);
|
||||
}
|
||||
|
||||
RelationalPersistentEntity<?> entity = getMappingContext().getPersistentEntity(rawType);
|
||||
|
||||
if (entity == null) {
|
||||
throw new MappingException(
|
||||
String.format("Expected to read Document %s into type %s but didn't find a PersistentEntity for the latter",
|
||||
document, rawType));
|
||||
}
|
||||
|
||||
return read(context, (RelationalPersistentEntity<S>) entity, document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the given {@link RowDocument} into a {@link Map}. will recursively resolve nested {@link Map}s as well. Can
|
||||
* be overridden by subclasses.
|
||||
*
|
||||
* @param context must not be {@literal null}
|
||||
* @param source must not be {@literal null}
|
||||
* @param targetType the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link RowDocument}.
|
||||
* @return the converted {@link Map}, will never be {@literal null}.
|
||||
*/
|
||||
protected Map<Object, Object> readMap(ConversionContext context, Map<?, ?> source, TypeInformation<?> targetType) {
|
||||
|
||||
Assert.notNull(source, "Document must not be null");
|
||||
Assert.notNull(targetType, "TypeInformation must not be null");
|
||||
|
||||
Class<?> mapType = targetType.getType();
|
||||
|
||||
TypeInformation<?> keyType = targetType.getComponentType();
|
||||
TypeInformation<?> valueType = targetType.getMapValueType() == null ? TypeInformation.OBJECT
|
||||
: targetType.getRequiredMapValueType();
|
||||
|
||||
Class<?> rawKeyType = keyType != null ? keyType.getType() : Object.class;
|
||||
|
||||
Map<Object, Object> map = CollectionFactory.createMap(mapType, rawKeyType,
|
||||
((Map<String, Object>) source).keySet().size());
|
||||
|
||||
source.forEach((k, v) -> {
|
||||
|
||||
Object key = k;
|
||||
|
||||
if (!rawKeyType.isAssignableFrom(key.getClass())) {
|
||||
key = doConvert(key, rawKeyType);
|
||||
}
|
||||
|
||||
map.put(key, v == null ? v : context.convert(v, valueType));
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the given {@link Collection} into a collection of the given {@link TypeInformation}. Can be overridden by
|
||||
* subclasses.
|
||||
*
|
||||
* @param context must not be {@literal null}
|
||||
* @param source must not be {@literal null}
|
||||
* @param targetType the {@link Map} {@link TypeInformation} to be used to unmarshall this {@link RowDocument}.
|
||||
* @return the converted {@link Collection} or array, will never be {@literal null}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Object readCollectionOrArray(ConversionContext context, Collection<?> source,
|
||||
TypeInformation<?> targetType) {
|
||||
|
||||
Assert.notNull(targetType, "Target type must not be null");
|
||||
|
||||
Class<?> collectionType = targetType.isSubTypeOf(Collection.class) //
|
||||
? targetType.getType() //
|
||||
: List.class;
|
||||
|
||||
TypeInformation<?> componentType = targetType.getComponentType() != null //
|
||||
? targetType.getComponentType() //
|
||||
: TypeInformation.OBJECT;
|
||||
Class<?> rawComponentType = componentType.getType();
|
||||
|
||||
Collection<Object> items = targetType.getType().isArray() //
|
||||
? new ArrayList<>(source.size()) //
|
||||
: CollectionFactory.createCollection(collectionType, rawComponentType, source.size());
|
||||
|
||||
if (source.isEmpty()) {
|
||||
return getPotentiallyConvertedSimpleRead(items, targetType);
|
||||
}
|
||||
|
||||
for (Object element : source) {
|
||||
items.add(element != null ? context.convert(element, componentType) : element);
|
||||
}
|
||||
|
||||
return getPotentiallyConvertedSimpleRead(items, targetType);
|
||||
}
|
||||
|
||||
private <T extends Object> T doConvert(Object value, Class<? extends T> target) {
|
||||
return doConvert(value, target, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private <T extends Object> T doConvert(Object value, Class<? extends T> target,
|
||||
@Nullable Class<? extends T> fallback) {
|
||||
|
||||
if (getConversionService().canConvert(value.getClass(), target) || fallback == null) {
|
||||
return getConversionService().convert(value, target);
|
||||
}
|
||||
return getConversionService().convert(value, fallback);
|
||||
}
|
||||
|
||||
private <S> S read(ConversionContext context, RelationalPersistentEntity<S> entity, RowDocument document) {
|
||||
|
||||
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(document, spELContext);
|
||||
RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document);
|
||||
|
||||
InstanceCreatorMetadata<RelationalPersistentProperty> instanceCreatorMetadata = entity.getInstanceCreatorMetadata();
|
||||
|
||||
ParameterValueProvider<RelationalPersistentProperty> provider = instanceCreatorMetadata != null
|
||||
&& instanceCreatorMetadata.hasParameters() ? getParameterProvider(context, entity, documentAccessor, evaluator)
|
||||
: NoOpParameterValueProvider.INSTANCE;
|
||||
|
||||
EntityInstantiator instantiator = getEntityInstantiators().getInstantiatorFor(entity);
|
||||
S instance = instantiator.createInstance(entity, provider);
|
||||
|
||||
if (entity.requiresPropertyPopulation()) {
|
||||
return populateProperties(context, entity, documentAccessor, evaluator, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private ParameterValueProvider<RelationalPersistentProperty> getParameterProvider(ConversionContext context,
|
||||
RelationalPersistentEntity<?> entity, RowDocumentAccessor source, SpELExpressionEvaluator evaluator) {
|
||||
|
||||
RelationalPropertyValueProvider provider = new RelationalPropertyValueProvider(context, source, evaluator,
|
||||
spELContext);
|
||||
|
||||
// TODO: Add support for enclosing object (non-static inner classes)
|
||||
PersistentEntityParameterValueProvider<RelationalPersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<>(
|
||||
entity, provider, context.getPath().getCurrentObject());
|
||||
|
||||
return new ConverterAwareSpELExpressionParameterValueProvider(context, evaluator, getConversionService(),
|
||||
parameterProvider);
|
||||
}
|
||||
|
||||
private <S> S populateProperties(ConversionContext context, RelationalPersistentEntity<S> entity,
|
||||
RowDocumentAccessor documentAccessor, SpELExpressionEvaluator evaluator, S instance) {
|
||||
|
||||
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
|
||||
getConversionService());
|
||||
|
||||
// Make sure id property is set before all other properties
|
||||
ObjectPath currentPath = context.getPath().push(accessor.getBean(), entity);
|
||||
ConversionContext contextToUse = context.withPath(currentPath);
|
||||
|
||||
RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(contextToUse, documentAccessor,
|
||||
evaluator, spELContext);
|
||||
|
||||
Predicate<RelationalPersistentProperty> propertyFilter = isConstructorArgument(entity).negate();
|
||||
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter);
|
||||
|
||||
return accessor.getBean();
|
||||
}
|
||||
|
||||
private void readProperties(ConversionContext context, RelationalPersistentEntity<?> entity,
|
||||
PersistentPropertyAccessor<?> accessor, RowDocumentAccessor documentAccessor,
|
||||
RelationalPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator,
|
||||
Predicate<RelationalPersistentProperty> propertyFilter) {
|
||||
|
||||
for (RelationalPersistentProperty prop : entity) {
|
||||
|
||||
if (!propertyFilter.test(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ConversionContext propertyContext = context.forProperty(prop);
|
||||
RelationalPropertyValueProvider valueProviderToUse = valueProvider.withContext(propertyContext);
|
||||
|
||||
if (prop.isAssociation()) {
|
||||
|
||||
// TODO: Read AggregateReference
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop.isEmbedded()) {
|
||||
accessor.setProperty(prop, readEmbedded(propertyContext, documentAccessor, prop,
|
||||
getMappingContext().getRequiredPersistentEntity(prop)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!documentAccessor.hasValue(prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
accessor.setProperty(prop, valueProviderToUse.getPropertyValue(prop));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object readEmbedded(ConversionContext context, RowDocumentAccessor documentAccessor,
|
||||
RelationalPersistentProperty prop, RelationalPersistentEntity<?> unwrappedEntity) {
|
||||
|
||||
if (prop.findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
|
||||
return read(context, unwrappedEntity, documentAccessor.getDocument());
|
||||
}
|
||||
|
||||
for (RelationalPersistentProperty persistentProperty : unwrappedEntity) {
|
||||
if (documentAccessor.hasValue(persistentProperty)) {
|
||||
return read(context, unwrappedEntity, documentAccessor.getDocument());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Predicate<RelationalPersistentProperty> isConstructorArgument(PersistentEntity<?, ?> entity) {
|
||||
return entity::isCreatorArgument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversion context holding references to simple {@link ValueConverter} and {@link ContainerValueConverter}.
|
||||
* Entrypoint for recursive conversion of {@link RowDocument} and other types.
|
||||
*
|
||||
* @since 3.2
|
||||
*/
|
||||
protected static class DefaultConversionContext implements ConversionContext {
|
||||
|
||||
final RelationalConverter sourceConverter;
|
||||
final org.springframework.data.convert.CustomConversions conversions;
|
||||
final ObjectPath objectPath;
|
||||
final ContainerValueConverter<RowDocument> documentConverter;
|
||||
final ContainerValueConverter<Collection<?>> collectionConverter;
|
||||
final ContainerValueConverter<Map<?, ?>> mapConverter;
|
||||
final ValueConverter<Object> elementConverter;
|
||||
|
||||
DefaultConversionContext(RelationalConverter sourceConverter,
|
||||
org.springframework.data.convert.CustomConversions customConversions, ObjectPath objectPath,
|
||||
ContainerValueConverter<RowDocument> documentConverter,
|
||||
ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Map<?, ?>> mapConverter,
|
||||
ValueConverter<Object> elementConverter) {
|
||||
|
||||
this.sourceConverter = sourceConverter;
|
||||
this.conversions = customConversions;
|
||||
this.objectPath = objectPath;
|
||||
this.documentConverter = documentConverter;
|
||||
this.collectionConverter = collectionConverter;
|
||||
this.mapConverter = mapConverter;
|
||||
this.elementConverter = elementConverter;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint,
|
||||
ConversionContext context) {
|
||||
|
||||
Assert.notNull(source, "Source must not be null");
|
||||
Assert.notNull(typeHint, "TypeInformation must not be null");
|
||||
|
||||
if (conversions.hasCustomReadTarget(source.getClass(), typeHint.getType())) {
|
||||
return (S) elementConverter.convert(source, typeHint);
|
||||
}
|
||||
|
||||
if (source instanceof Collection<?> collection) {
|
||||
|
||||
if (typeHint.isCollectionLike() || typeHint.getType().isAssignableFrom(Collection.class)) {
|
||||
return (S) collectionConverter.convert(context, collection, typeHint);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeHint.isMap()) {
|
||||
|
||||
if (ClassUtils.isAssignable(RowDocument.class, typeHint.getType())) {
|
||||
return (S) documentConverter.convert(context, (RowDocument) source, typeHint);
|
||||
}
|
||||
|
||||
if (source instanceof Map<?, ?> map) {
|
||||
return (S) mapConverter.convert(context, map, typeHint);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Expected map like structure but found %s", source.getClass()));
|
||||
}
|
||||
|
||||
if (source instanceof RowDocument document) {
|
||||
return (S) documentConverter.convert(context, document, typeHint);
|
||||
}
|
||||
|
||||
return (S) elementConverter.convert(source, typeHint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversionContext withPath(ObjectPath currentPath) {
|
||||
|
||||
Assert.notNull(currentPath, "ObjectPath must not be null");
|
||||
|
||||
return new DefaultConversionContext(sourceConverter, conversions, currentPath, documentConverter,
|
||||
collectionConverter, mapConverter, elementConverter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectPath getPath() {
|
||||
return objectPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomConversions getCustomConversions() {
|
||||
return conversions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelationalConverter getSourceConverter() {
|
||||
return sourceConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a simple {@code source} value into {@link TypeInformation the target type}.
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
interface ValueConverter<T> {
|
||||
|
||||
Object convert(T source, TypeInformation<?> typeHint);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a container {@code source} value into {@link TypeInformation the target type}. Containers may
|
||||
* recursively apply conversions for entities, collections, maps, etc.
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
interface ContainerValueConverter<T> {
|
||||
|
||||
Object convert(ConversionContext context, T source, TypeInformation<?> typeHint);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversion context defining an interface for graph-traversal-based conversion of row documents. Entrypoint for
|
||||
* recursive conversion of {@link RowDocument} and other types.
|
||||
*
|
||||
* @since 3.2
|
||||
*/
|
||||
protected interface ConversionContext {
|
||||
|
||||
/**
|
||||
* Converts a source object into {@link TypeInformation target}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param typeHint must not be {@literal null}.
|
||||
* @return the converted object.
|
||||
*/
|
||||
default <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint) {
|
||||
return convert(source, typeHint, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a source object into {@link TypeInformation target}.
|
||||
*
|
||||
* @param source must not be {@literal null}.
|
||||
* @param typeHint must not be {@literal null}.
|
||||
* @param context must not be {@literal null}.
|
||||
* @return the converted object.
|
||||
*/
|
||||
<S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context);
|
||||
|
||||
/**
|
||||
* Obtain a {@link ConversionContext} for the given property {@code name}.
|
||||
*
|
||||
* @param name must not be {@literal null}.
|
||||
* @return the {@link ConversionContext} to be used for conversion of the given property.
|
||||
*/
|
||||
default ConversionContext forProperty(String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a {@link ConversionContext} for the given {@link RelationalPersistentProperty}.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @return the {@link ConversionContext} to be used for conversion of the given property.
|
||||
*/
|
||||
default ConversionContext forProperty(RelationalPersistentProperty property) {
|
||||
return forProperty(property.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ConversionContext} with {@link ObjectPath currentPath} applied.
|
||||
*
|
||||
* @param currentPath must not be {@literal null}.
|
||||
* @return a new {@link ConversionContext} with {@link ObjectPath currentPath} applied.
|
||||
*/
|
||||
ConversionContext withPath(ObjectPath currentPath);
|
||||
|
||||
ObjectPath getPath();
|
||||
|
||||
CustomConversions getCustomConversions();
|
||||
|
||||
RelationalConverter getSourceConverter();
|
||||
|
||||
}
|
||||
|
||||
enum NoOpParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public <T> T getParameterValue(Parameter<T, RelationalPersistentProperty> parameter) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link PropertyValueProvider} to evaluate a SpEL expression if present on the property or simply accesses the field
|
||||
* of the configured source {@link RowDocument}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
record RelationalPropertyValueProvider(ConversionContext context, RowDocumentAccessor accessor,
|
||||
SpELExpressionEvaluator evaluator,
|
||||
SpELContext spELContext) implements PropertyValueProvider<RelationalPersistentProperty> {
|
||||
|
||||
/**
|
||||
* Creates a new {@link RelationalPropertyValueProvider} for the given source and {@link SpELExpressionEvaluator}.
|
||||
*
|
||||
* @param context must not be {@literal null}.
|
||||
* @param accessor must not be {@literal null}.
|
||||
* @param evaluator must not be {@literal null}.
|
||||
*/
|
||||
RelationalPropertyValueProvider {
|
||||
|
||||
Assert.notNull(context, "ConversionContext must no be null");
|
||||
Assert.notNull(accessor, "DocumentAccessor must no be null");
|
||||
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getPropertyValue(RelationalPersistentProperty property) {
|
||||
|
||||
String expression = property.getSpelExpression();
|
||||
Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property);
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ConversionContext contextToUse = context.forProperty(property);
|
||||
|
||||
return (T) contextToUse.convert(value, property.getTypeInformation());
|
||||
}
|
||||
|
||||
public RelationalPropertyValueProvider withContext(ConversionContext context) {
|
||||
|
||||
return context == this.context ? this
|
||||
: new RelationalPropertyValueProvider(context, accessor, evaluator, spELContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw
|
||||
* resolved SpEL value.
|
||||
*/
|
||||
private static class ConverterAwareSpELExpressionParameterValueProvider
|
||||
extends SpELExpressionParameterValueProvider<RelationalPersistentProperty> {
|
||||
|
||||
private final ConversionContext context;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}.
|
||||
*
|
||||
* @param context must not be {@literal null}.
|
||||
* @param evaluator must not be {@literal null}.
|
||||
* @param conversionService must not be {@literal null}.
|
||||
* @param delegate must not be {@literal null}.
|
||||
*/
|
||||
public ConverterAwareSpELExpressionParameterValueProvider(ConversionContext context,
|
||||
SpELExpressionEvaluator evaluator, ConversionService conversionService,
|
||||
ParameterValueProvider<RelationalPersistentProperty> delegate) {
|
||||
|
||||
super(evaluator, conversionService, delegate);
|
||||
|
||||
Assert.notNull(context, "ConversionContext must no be null");
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, RelationalPersistentProperty> parameter) {
|
||||
return context.convert(object, parameter.getType());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A path of objects nested into each other. The type allows access to all parent objects currently in creation even
|
||||
* when resolving more nested objects. This allows to avoid re-resolving object instances that are logically equivalent
|
||||
* to already resolved ones.
|
||||
* <p>
|
||||
* An immutable ordered set of target objects for {@link org.springframework.data.relational.domain.RowDocument} to
|
||||
* {@link Object} conversions. Object paths can be extended via
|
||||
* {@link #push(Object, org.springframework.data.relational.core.mapping.RelationalPersistentEntity)}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
class ObjectPath {
|
||||
|
||||
static final ObjectPath ROOT = new ObjectPath();
|
||||
|
||||
private final @Nullable ObjectPath parent;
|
||||
private final @Nullable Object object;
|
||||
|
||||
private ObjectPath() {
|
||||
|
||||
this.parent = null;
|
||||
this.object = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ObjectPath} from the given parent {@link ObjectPath} and adding the provided path values.
|
||||
*
|
||||
* @param parent must not be {@literal null}.
|
||||
* @param object must not be {@literal null}.
|
||||
*/
|
||||
private ObjectPath(ObjectPath parent, Object object) {
|
||||
|
||||
this.parent = parent;
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the {@link ObjectPath} with the given {@link Object} as current object.
|
||||
*
|
||||
* @param object must not be {@literal null}.
|
||||
* @param entity must not be {@literal null}.
|
||||
* @return new instance of {@link ObjectPath}.
|
||||
*/
|
||||
ObjectPath push(Object object, RelationalPersistentEntity<?> entity) {
|
||||
|
||||
Assert.notNull(object, "Object must not be null");
|
||||
Assert.notNull(entity, "RelationalPersistentEntity must not be null");
|
||||
|
||||
return new ObjectPath(this, object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current object of the {@link ObjectPath} or {@literal null} if the path is empty.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
Object getCurrentObject() {
|
||||
return getObject();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getObject() {
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
if (parent == null) {
|
||||
return "[empty]";
|
||||
}
|
||||
|
||||
List<String> strings = new ArrayList<>();
|
||||
|
||||
for (ObjectPath current = this; current != null; current = current.parent) {
|
||||
strings.add(ObjectUtils.nullSafeToString(current.getObject()));
|
||||
}
|
||||
|
||||
return StringUtils.collectionToDelimitedString(strings, " -> ");
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import org.springframework.data.mapping.model.EntityInstantiators;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.domain.RowDocument;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -53,6 +54,17 @@ public interface RelationalConverter {
|
||||
*/
|
||||
ConversionService getConversionService();
|
||||
|
||||
/**
|
||||
* Read a {@link RowDocument} into the requested {@link Class aggregate type}.
|
||||
*
|
||||
* @param type target aggregate type.
|
||||
* @param source source {@link RowDocument}.
|
||||
* @return the converted object.
|
||||
* @param <R> aggregate type.
|
||||
* @since 3.2
|
||||
*/
|
||||
<R> R read(Class<R> type, RowDocument source);
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link PersistentEntity} given {@link ParameterValueProvider} to obtain constructor
|
||||
* properties.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.relational.domain.RowDocument;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Wrapper value object for a {@link RowDocument} to be able to access raw values by
|
||||
* {@link RelationalPersistentProperty} references. The accessors will transparently resolve nested document values that
|
||||
* a {@link RelationalPersistentProperty} might refer to through a path expression in field names.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
record RowDocumentAccessor(RowDocument document) {
|
||||
|
||||
/**
|
||||
* Creates a new {@link RowDocumentAccessor} for the given {@link RowDocument}.
|
||||
*
|
||||
* @param document must be a {@link RowDocument} effectively, must not be {@literal null}.
|
||||
*/
|
||||
RowDocumentAccessor {
|
||||
|
||||
Assert.notNull(document, "Document must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the underlying {@link RowDocument document}.
|
||||
*/
|
||||
public RowDocument getDocument() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all mappings from the given {@link RowDocument} to the underlying target {@link RowDocument}. These mappings
|
||||
* will replace any mappings that the target document had for any of the keys currently in the specified map.
|
||||
*
|
||||
* @param source
|
||||
*/
|
||||
public void putAll(RowDocument source) {
|
||||
|
||||
document.putAll(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the given value into the backing {@link RowDocument} based on the coordinates defined through the given
|
||||
* {@link RelationalPersistentProperty}. By default, this will be the plain field name. But field names might also
|
||||
* consist of path traversals so we might need to create intermediate {@link RowDocument}s.
|
||||
*
|
||||
* @param prop must not be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
public void put(RelationalPersistentProperty prop, @Nullable Object value) {
|
||||
|
||||
Assert.notNull(prop, "RelationalPersistentProperty must not be null");
|
||||
String fieldName = getColumnName(prop);
|
||||
|
||||
document.put(fieldName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value the given {@link RelationalPersistentProperty} refers to. By default, this will be a direct field
|
||||
* but the method will also transparently resolve nested values the {@link RelationalPersistentProperty} might refer
|
||||
* to through a path expression in the field name metadata.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public Object get(RelationalPersistentProperty property) {
|
||||
return document.get(getColumnName(property));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the underlying {@link RowDocument} has a value ({@literal null} or non-{@literal null}) for the
|
||||
* given {@link RelationalPersistentProperty}.
|
||||
*
|
||||
* @param property must not be {@literal null}.
|
||||
* @return {@literal true} if no non {@literal null} value present.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean hasValue(RelationalPersistentProperty property) {
|
||||
|
||||
Assert.notNull(property, "Property must not be null");
|
||||
|
||||
return document.containsKey(getColumnName(property));
|
||||
}
|
||||
|
||||
String getColumnName(RelationalPersistentProperty prop) {
|
||||
return prop.getColumnName().getReference();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.mapping;
|
||||
|
||||
/**
|
||||
* Holder for an embedded {@link RelationalPersistentProperty}
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
record EmbeddedContext(RelationalPersistentProperty ownerProperty) {
|
||||
|
||||
EmbeddedContext {
|
||||
}
|
||||
|
||||
public String getEmbeddedPrefix() {
|
||||
return ownerProperty.getEmbeddedPrefix();
|
||||
}
|
||||
|
||||
public String withEmbeddedPrefix(String name) {
|
||||
|
||||
if (ownerProperty.getEmbeddedPrefix() != null) {
|
||||
return ownerProperty.getEmbeddedPrefix() + name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.mapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.data.mapping.*;
|
||||
import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.spel.EvaluationContextProvider;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Embedded entity extension for a {@link Embedded entity}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
class EmbeddedRelationalPersistentEntity<T> implements RelationalPersistentEntity<T> {
|
||||
|
||||
private final RelationalPersistentEntity<T> delegate;
|
||||
|
||||
private final EmbeddedContext context;
|
||||
|
||||
public EmbeddedRelationalPersistentEntity(RelationalPersistentEntity<T> delegate, EmbeddedContext context) {
|
||||
this.delegate = delegate;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlIdentifier getTableName() {
|
||||
throw new MappingException("Cannot map embedded entity to table");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlIdentifier getIdColumn() {
|
||||
throw new MappingException("Embedded entity does not have an id column");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPersistentProperty(RelationalPersistentProperty property) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAssociation(Association<RelationalPersistentProperty> association) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify() throws MappingException {}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<RelationalPersistentProperty> iterator() {
|
||||
|
||||
Iterator<RelationalPersistentProperty> iterator = delegate.iterator();
|
||||
|
||||
return new Iterator<>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return iterator.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelationalPersistentProperty next() {
|
||||
return wrap(iterator.next());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) {
|
||||
delegate.setPersistentPropertyAccessorFactory(factory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEvaluationContextProvider(EvaluationContextProvider provider) {
|
||||
delegate.setEvaluationContextProvider(provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
@Nullable
|
||||
public PreferredConstructor<T, RelationalPersistentProperty> getPersistenceConstructor() {
|
||||
return delegate.getPersistenceConstructor();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public InstanceCreatorMetadata<RelationalPersistentProperty> getInstanceCreatorMetadata() {
|
||||
return delegate.getInstanceCreatorMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCreatorArgument(PersistentProperty<?> property) {
|
||||
return delegate.isCreatorArgument(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdProperty(PersistentProperty<?> property) {
|
||||
return delegate.isIdProperty(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersionProperty(PersistentProperty<?> property) {
|
||||
return delegate.isVersionProperty(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public RelationalPersistentProperty getIdProperty() {
|
||||
return wrap(delegate.getIdProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public RelationalPersistentProperty getVersionProperty() {
|
||||
return wrap(delegate.getVersionProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public RelationalPersistentProperty getPersistentProperty(String name) {
|
||||
return wrap(delegate.getPersistentProperty(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<RelationalPersistentProperty> getPersistentProperties(Class<? extends Annotation> annotationType) {
|
||||
return Streamable.of(delegate.getPersistentProperties(annotationType)).map(this::wrap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasIdProperty() {
|
||||
return delegate.hasIdProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasVersionProperty() {
|
||||
return delegate.hasVersionProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> getType() {
|
||||
return delegate.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getTypeAlias() {
|
||||
return delegate.getTypeAlias();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<T> getTypeInformation() {
|
||||
return delegate.getTypeInformation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doWithProperties(PropertyHandler<RelationalPersistentProperty> handler) {
|
||||
delegate.doWithProperties((PropertyHandler<RelationalPersistentProperty>) persistentProperty -> {
|
||||
handler.doWithPersistentProperty(wrap(persistentProperty));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doWithProperties(SimplePropertyHandler handler) {
|
||||
delegate.doWithProperties((SimplePropertyHandler) property -> handler
|
||||
.doWithPersistentProperty(wrap((RelationalPersistentProperty) property)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doWithAssociations(AssociationHandler<RelationalPersistentProperty> handler) {
|
||||
delegate.doWithAssociations(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doWithAssociations(SimpleAssociationHandler handler) {
|
||||
delegate.doWithAssociations(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
|
||||
return delegate.findAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Annotation> boolean isAnnotationPresent(Class<A> annotationType) {
|
||||
return delegate.isAnnotationPresent(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
|
||||
return delegate.getPropertyAccessor(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <B> PersistentPropertyPathAccessor<B> getPropertyPathAccessor(B bean) {
|
||||
return delegate.getPropertyPathAccessor(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifierAccessor getIdentifierAccessor(Object bean) {
|
||||
return delegate.getIdentifierAccessor(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNew(Object bean) {
|
||||
return delegate.isNew(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImmutable() {
|
||||
return delegate.isImmutable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPropertyPopulation() {
|
||||
return delegate.requiresPropertyPopulation();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RelationalPersistentProperty wrap(@Nullable RelationalPersistentProperty source) {
|
||||
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
return new EmbeddedRelationalPersistentProperty(source, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("EmbeddedRelationalPersistentEntity<%s>", getType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.mapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Embedded property extension to a {@link RelationalPersistentProperty}
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.2
|
||||
*/
|
||||
class EmbeddedRelationalPersistentProperty implements RelationalPersistentProperty {
|
||||
|
||||
private final RelationalPersistentProperty delegate;
|
||||
|
||||
private final EmbeddedContext context;
|
||||
|
||||
public EmbeddedRelationalPersistentProperty(RelationalPersistentProperty delegate, EmbeddedContext context) {
|
||||
|
||||
this.delegate = delegate;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getEmbeddedPrefix() {
|
||||
return context.withEmbeddedPrefix(delegate.getEmbeddedPrefix());
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlIdentifier getColumnName() {
|
||||
return delegate.getColumnName().transform(context::withEmbeddedPrefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelationalPersistentEntity<?> getOwner() {
|
||||
return delegate.getOwner();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated(since = "3.2", forRemoval = true)
|
||||
public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) {
|
||||
return delegate.getReverseColumnName(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlIdentifier getReverseColumnName(RelationalPersistentEntity<?> owner) {
|
||||
return delegate.getReverseColumnName(owner);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public SqlIdentifier getKeyColumn() {
|
||||
return delegate.getKeyColumn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isQualified() {
|
||||
return delegate.isQualified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getQualifierColumnType() {
|
||||
return delegate.getQualifierColumnType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOrdered() {
|
||||
return delegate.isOrdered();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldCreateEmptyEmbedded() {
|
||||
return delegate.shouldCreateEmptyEmbedded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInsertOnly() {
|
||||
return delegate.isInsertOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return delegate.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getType() {
|
||||
return delegate.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeInformation<?> getTypeInformation() {
|
||||
return delegate.getTypeInformation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends TypeInformation<?>> getPersistentEntityTypeInformation() {
|
||||
return delegate.getPersistentEntityTypeInformation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Method getGetter() {
|
||||
return delegate.getGetter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Method getSetter() {
|
||||
return delegate.getSetter();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Method getWither() {
|
||||
return delegate.getWither();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Field getField() {
|
||||
return delegate.getField();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getSpelExpression() {
|
||||
return delegate.getSpelExpression();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Association<RelationalPersistentProperty> getAssociation() {
|
||||
return delegate.getAssociation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Association<RelationalPersistentProperty> getRequiredAssociation() {
|
||||
return delegate.getRequiredAssociation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEntity() {
|
||||
return delegate.isEntity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIdProperty() {
|
||||
return delegate.isIdProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVersionProperty() {
|
||||
return delegate.isVersionProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollectionLike() {
|
||||
return delegate.isCollectionLike();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMap() {
|
||||
return delegate.isMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isArray() {
|
||||
return delegate.isArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTransient() {
|
||||
return delegate.isTransient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable() {
|
||||
return delegate.isWritable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return delegate.isReadable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isImmutable() {
|
||||
return delegate.isImmutable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAssociation() {
|
||||
return delegate.isAssociation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Class<?> getComponentType() {
|
||||
return delegate.getComponentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getRawType() {
|
||||
return delegate.getRawType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Class<?> getMapValueType() {
|
||||
return delegate.getMapValueType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getActualType() {
|
||||
return delegate.getActualType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
|
||||
return delegate.findAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
|
||||
return delegate.findPropertyOrOwnerAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
|
||||
return delegate.isAnnotationPresent(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usePropertyAccess() {
|
||||
return delegate.usePropertyAccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasActualTypeAnnotation(Class<? extends Annotation> annotationType) {
|
||||
return delegate.hasActualTypeAnnotation(annotationType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Class<?> getAssociationTargetType() {
|
||||
return delegate.getAssociationTargetType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public TypeInformation<?> getAssociationTargetTypeInformation() {
|
||||
return delegate.getAssociationTargetTypeInformation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
EmbeddedRelationalPersistentProperty that = (EmbeddedRelationalPersistentProperty) o;
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(delegate, that.delegate)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(context, that.context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = ObjectUtils.nullSafeHashCode(delegate);
|
||||
result = 31 * result + ObjectUtils.nullSafeHashCode(context);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.spel.EvaluationContextProvider;
|
||||
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -110,6 +111,24 @@ public class RelationalMappingContext
|
||||
this.expressionEvaluator.setProvider(new ExtensionAwareEvaluationContextProvider(applicationContext));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RelationalPersistentEntity<?> getPersistentEntity(RelationalPersistentProperty persistentProperty) {
|
||||
|
||||
boolean embeddedDelegation = false;
|
||||
if (persistentProperty instanceof EmbeddedRelationalPersistentProperty) {
|
||||
embeddedDelegation = true;
|
||||
}
|
||||
|
||||
RelationalPersistentEntity<?> entity = super.getPersistentEntity(persistentProperty);
|
||||
|
||||
if (entity != null && (persistentProperty.isEmbedded() || embeddedDelegation)) {
|
||||
return new EmbeddedRelationalPersistentEntity<>(entity, new EmbeddedContext(persistentProperty));
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> RelationalPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
|
||||
|
||||
|
||||
@@ -41,11 +41,15 @@ public class RowDocument implements Map<String, Object> {
|
||||
this.delegate = new LinkedCaseInsensitiveMap<>();
|
||||
}
|
||||
|
||||
public RowDocument(Map<String, Object> map) {
|
||||
public RowDocument(Map<String, ? extends Object> map) {
|
||||
this.delegate = new LinkedCaseInsensitiveMap<>();
|
||||
this.delegate.putAll(delegate);
|
||||
}
|
||||
|
||||
public static Object of(String field, Object value) {
|
||||
return new RowDocument().append(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value at {@code key} as {@link List}.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.convert.ConverterBuilder;
|
||||
import org.springframework.data.convert.ConverterBuilder.ConverterAware;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.convert.CustomConversions.StoreConversions;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.relational.core.mapping.Embedded;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.domain.RowDocument;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MappingRelationalConverter}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class MappingRelationalConverterUnitTests {
|
||||
|
||||
MappingRelationalConverter converter = new MappingRelationalConverter(new RelationalMappingContext());
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldReadSimpleType() {
|
||||
|
||||
RowDocument document = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
|
||||
|
||||
SimpleType result = converter.read(SimpleType.class, document);
|
||||
|
||||
assertThat(result.id).isEqualTo("1");
|
||||
assertThat(result.name).isEqualTo("John");
|
||||
assertThat(result.age).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldReadSimpleRecord() {
|
||||
|
||||
RowDocument document = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
|
||||
|
||||
SimpleRecord result = converter.read(SimpleRecord.class, document);
|
||||
|
||||
assertThat(result.id).isEqualTo("1");
|
||||
assertThat(result.name).isEqualTo("John");
|
||||
assertThat(result.age).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldEvaluateExpression() {
|
||||
|
||||
RowDocument document = new RowDocument().append("myprop", "bar");
|
||||
|
||||
WithExpression result = converter.read(WithExpression.class, document);
|
||||
|
||||
assertThat(result.name).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldReadNonstaticInner() {
|
||||
|
||||
RowDocument document = new RowDocument().append("name", "John").append("child",
|
||||
new RowDocument().append("name", "Johnny"));
|
||||
|
||||
Parent result = converter.read(Parent.class, document);
|
||||
|
||||
assertThat(result.name).isEqualTo("John");
|
||||
assertThat(result.child.name).isEqualTo("Johnny");
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldReadEmbedded() {
|
||||
|
||||
RowDocument document = new RowDocument().append("simple_id", "1").append("simple_name", "John").append("simple_age",
|
||||
30);
|
||||
|
||||
WithEmbedded result = converter.read(WithEmbedded.class, document);
|
||||
|
||||
assertThat(result.simple).isNotNull();
|
||||
assertThat(result.simple.id).isEqualTo("1");
|
||||
assertThat(result.simple.name).isEqualTo("John");
|
||||
assertThat(result.simple.age).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldReadWithLists() {
|
||||
|
||||
RowDocument nested = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
|
||||
|
||||
RowDocument document = new RowDocument().append("strings", List.of("one", "two"))
|
||||
.append("states", List.of("NEW", "USED")).append("entities", List.of(nested));
|
||||
|
||||
WithCollection result = converter.read(WithCollection.class, document);
|
||||
|
||||
assertThat(result.strings).containsExactly("one", "two");
|
||||
assertThat(result.states).containsExactly(State.NEW, State.USED);
|
||||
assertThat(result.entities).hasSize(1);
|
||||
|
||||
assertThat(result.entities.get(0).id).isEqualTo("1");
|
||||
assertThat(result.entities.get(0).name).isEqualTo("John");
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldReadWithMaps() {
|
||||
|
||||
RowDocument nested = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
|
||||
|
||||
RowDocument document = new RowDocument().append("strings", Map.of(1, "one", 2, "two")).append("entities",
|
||||
Map.of(1, nested));
|
||||
|
||||
WithMap result = converter.read(WithMap.class, document);
|
||||
|
||||
assertThat(result.strings).hasSize(2).containsEntry(1, "one").containsEntry(2, "two");
|
||||
|
||||
assertThat(result.entities).hasSize(1);
|
||||
assertThat(result.entities.get(1).id).isEqualTo("1");
|
||||
assertThat(result.entities.get(1).name).isEqualTo("John");
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void shouldApplyConverters() {
|
||||
|
||||
ConverterAware converterAware = ConverterBuilder.reading(String.class, Money.class, s -> {
|
||||
String[] s1 = s.split(" ");
|
||||
return new Money(Integer.parseInt(s1[0]), s1[1]);
|
||||
}).andWriting(money -> money.amount + " " + money.currency);
|
||||
|
||||
CustomConversions conversions = new CustomConversions(StoreConversions.of(SimpleTypeHolder.DEFAULT),
|
||||
List.of(converterAware));
|
||||
RelationalMappingContext mappingContext = new RelationalMappingContext();
|
||||
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
|
||||
mappingContext.afterPropertiesSet();
|
||||
|
||||
MappingRelationalConverter converter = new MappingRelationalConverter(mappingContext, conversions);
|
||||
|
||||
RowDocument document = new RowDocument().append("money", "1 USD");
|
||||
|
||||
WithMoney result = converter.read(WithMoney.class, document);
|
||||
|
||||
assertThat(result.money.amount).isEqualTo(1);
|
||||
assertThat(result.money.currency).isEqualTo("USD");
|
||||
}
|
||||
|
||||
static class SimpleType {
|
||||
|
||||
@Id String id;
|
||||
String name;
|
||||
int age;
|
||||
|
||||
}
|
||||
|
||||
record SimpleRecord(@Id String id, String name, int age) {
|
||||
}
|
||||
|
||||
static class Parent {
|
||||
String name;
|
||||
|
||||
Child child;
|
||||
|
||||
class Child {
|
||||
|
||||
String name;
|
||||
}
|
||||
}
|
||||
|
||||
static class WithCollection {
|
||||
|
||||
List<String> strings;
|
||||
EnumSet<State> states;
|
||||
List<SimpleType> entities;
|
||||
}
|
||||
|
||||
static class WithMap {
|
||||
|
||||
Map<Integer, String> strings;
|
||||
Map<Integer, SimpleType> entities;
|
||||
}
|
||||
|
||||
enum State {
|
||||
NEW, USED, UNKNOWN
|
||||
}
|
||||
|
||||
static class WithMoney {
|
||||
|
||||
Money money;
|
||||
|
||||
}
|
||||
|
||||
static class Money {
|
||||
final int amount;
|
||||
final String currency;
|
||||
|
||||
public Money(int amount, String currency) {
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
}
|
||||
}
|
||||
|
||||
static class WithExpression {
|
||||
|
||||
private final String name;
|
||||
|
||||
public WithExpression(@Value("#root.myprop") String foo) {
|
||||
this.name = foo;
|
||||
}
|
||||
}
|
||||
|
||||
static class WithEmbedded {
|
||||
|
||||
@Embedded.Nullable(prefix = "simple_") SimpleType simple;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.relational.core.sql.SqlIdentifier;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link RelationalMappingContext}.
|
||||
@@ -87,8 +88,37 @@ public class RelationalMappingContextUnitTests {
|
||||
assertThat(one).isSameAs(two);
|
||||
}
|
||||
|
||||
@Test // GH-1586
|
||||
void correctlyCascadesPrefix() {
|
||||
|
||||
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithEmbedded.class);
|
||||
|
||||
RelationalPersistentProperty parent = entity.getRequiredPersistentProperty("parent");
|
||||
RelationalPersistentEntity<?> parentEntity = context.getRequiredPersistentEntity(parent);
|
||||
RelationalPersistentProperty child = parentEntity.getRequiredPersistentProperty("child");
|
||||
RelationalPersistentEntity<?> childEntity = context.getRequiredPersistentEntity(child);
|
||||
RelationalPersistentProperty name = childEntity.getRequiredPersistentProperty("name");
|
||||
|
||||
assertThat(parent.getEmbeddedPrefix()).isEqualTo("prnt_");
|
||||
assertThat(child.getEmbeddedPrefix()).isEqualTo("prnt_chld_");
|
||||
assertThat(name.getColumnName()).isEqualTo(SqlIdentifier.quoted("PRNT_CHLD_NAME"));
|
||||
}
|
||||
|
||||
static class EntityWithUuid {
|
||||
@Id UUID uuid;
|
||||
}
|
||||
|
||||
static class WithEmbedded {
|
||||
@Embedded.Empty(prefix = "prnt_") Parent parent;
|
||||
}
|
||||
|
||||
static class Parent {
|
||||
|
||||
@Embedded.Empty(prefix = "chld_") Child child;
|
||||
}
|
||||
|
||||
static class Child {
|
||||
String name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user