Enable Single Query Loading for simple aggregates.
Single Query Loading loads as the name suggests complete aggregates using a single select. While the ultimate goal is to support this for all aggregates, this commit enables it only for simple aggregate and also only for some operations. A simple aggregate is an aggregate that only reference up to one other entity and does not have embedded entities. The supported operations are those available via `CrudRepository`: `findAll`, `findById`, and `findAllByIds`. Single Query Loading does NOT work with the supported in memory databases H2 and HSQLDB, since these do not properly support windowing functions, which are essential for Single Query Loading. To turn on Single Query Loading call `RelationalMappingContext.setSingleQueryLoadingEnabled(true)`. Closes #1446 See #1450 See #1445 Original pull request: #1572
This commit is contained in:
committed by
Mark Paluch
parent
dffde372f9
commit
93821f5f42
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.sqlgeneration.AliasFactory;
|
||||
import org.springframework.data.relational.core.sqlgeneration.CachingSqlGenerator;
|
||||
import org.springframework.data.relational.core.sqlgeneration.SingleQuerySqlGenerator;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Reads complete Aggregates from the database, by generating appropriate SQL using a {@link SingleQuerySqlGenerator}
|
||||
* and a matching {@link AggregateResultSetExtractor} and invoking a
|
||||
* {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}
|
||||
*
|
||||
* @param <T> the type of aggregate produced by this reader.
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AggregateReader<T> {
|
||||
|
||||
private final RelationalMappingContext mappingContext;
|
||||
private final RelationalPersistentEntity<T> aggregate;
|
||||
private final AliasFactory aliasFactory;
|
||||
private final org.springframework.data.relational.core.sqlgeneration.SqlGenerator sqlGenerator;
|
||||
private final JdbcConverter converter;
|
||||
private final NamedParameterJdbcOperations jdbcTemplate;
|
||||
|
||||
AggregateReader(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter,
|
||||
NamedParameterJdbcOperations jdbcTemplate, RelationalPersistentEntity<T> aggregate) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
|
||||
this.aggregate = aggregate;
|
||||
this.converter = converter;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
|
||||
this.sqlGenerator = new CachingSqlGenerator(new SingleQuerySqlGenerator(mappingContext, dialect, aggregate));
|
||||
this.aliasFactory = sqlGenerator.getAliasFactory();
|
||||
}
|
||||
|
||||
public List<T> findAll() {
|
||||
|
||||
String sql = sqlGenerator.findAll();
|
||||
|
||||
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
|
||||
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
|
||||
pathToColumn);
|
||||
|
||||
Iterable<T> result = jdbcTemplate.query(sql, extractor);
|
||||
|
||||
Assert.state(result != null, "result is null");
|
||||
|
||||
return (List<T>) result;
|
||||
}
|
||||
|
||||
public T findById(Object id) {
|
||||
|
||||
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
|
||||
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
|
||||
pathToColumn);
|
||||
|
||||
String sql = sqlGenerator.findById();
|
||||
|
||||
id = converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation());
|
||||
|
||||
Iterator<T> result = jdbcTemplate.query(sql, Map.of("id", id), extractor).iterator();
|
||||
|
||||
T returnValue = result.hasNext() ? result.next() : null;
|
||||
|
||||
if (result.hasNext()) {
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public Iterable<T> findAllById(Iterable<?> ids) {
|
||||
|
||||
PathToColumnMapping pathToColumn = createPathToColumnMapping(aliasFactory);
|
||||
AggregateResultSetExtractor<T> extractor = new AggregateResultSetExtractor<>(mappingContext, aggregate, converter,
|
||||
pathToColumn);
|
||||
|
||||
String sql = sqlGenerator.findAllById();
|
||||
|
||||
List<Object> convertedIds = new ArrayList<>();
|
||||
for (Object id : ids) {
|
||||
convertedIds.add(converter.writeValue(id, aggregate.getRequiredIdProperty().getTypeInformation()));
|
||||
}
|
||||
|
||||
return jdbcTemplate.query(sql, Map.of("ids", convertedIds), extractor);
|
||||
}
|
||||
|
||||
private PathToColumnMapping createPathToColumnMapping(AliasFactory aliasFactory) {
|
||||
return new PathToColumnMapping() {
|
||||
@Override
|
||||
public String column(AggregatePath path) {
|
||||
|
||||
String alias = aliasFactory.getColumnAlias(path);
|
||||
Assert.notNull(alias, () -> "alias for >" + path + "<must not be null");
|
||||
return alias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String keyColumn(AggregatePath path) {
|
||||
return aliasFactory.getKeyAlias(path);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
|
||||
/**
|
||||
* Creates {@link AggregateReader} instances.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AggregateReaderFactory {
|
||||
|
||||
private final RelationalMappingContext mappingContext;
|
||||
private final Dialect dialect;
|
||||
private final JdbcConverter converter;
|
||||
private final NamedParameterJdbcOperations jdbcTemplate;
|
||||
|
||||
public AggregateReaderFactory(RelationalMappingContext mappingContext, Dialect dialect, JdbcConverter converter,
|
||||
NamedParameterJdbcOperations jdbcTemplate) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
this.dialect = dialect;
|
||||
this.converter = converter;
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
<T> AggregateReader<T> createAggregateReaderFor(RelationalPersistentEntity<T> entity) {
|
||||
return new AggregateReader<>(mappingContext, dialect, converter, jdbcTemplate, entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.util.AbstractCollection;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.mapping.Parameter;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.EntityInstantiator;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.jdbc.core.ResultSetExtractor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Extracts complete aggregates from a {@link ResultSet}. The {@literal ResultSet} must have a very special structure
|
||||
* which looks somewhat how one would represent an aggregate in a single excel table. The first row contains data of the
|
||||
* aggregate root, any single valued reference and the first element of any collection. Following rows do NOT repeat the
|
||||
* aggregate root data but contain data of second elements of any collections. For details see accompanying unit tests.
|
||||
*
|
||||
* @param <T> the type of aggregates to extract
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class AggregateResultSetExtractor<T> implements ResultSetExtractor<Iterable<T>> {
|
||||
|
||||
private final RelationalMappingContext context;
|
||||
private final RelationalPersistentEntity<T> rootEntity;
|
||||
private final JdbcConverter converter;
|
||||
private final PathToColumnMapping propertyToColumn;
|
||||
|
||||
/**
|
||||
* @param context the {@link org.springframework.data.mapping.context.MappingContext} providing the metadata for the
|
||||
* aggregate and its entity. Must not be {@literal null}.
|
||||
* @param rootEntity the aggregate root. Must not be {@literal null}.
|
||||
* @param converter Used for converting objects from the database to whatever is required by the aggregate. Must not
|
||||
* be {@literal null}.
|
||||
* @param pathToColumn a mapping from {@link org.springframework.data.relational.core.mapping.AggregatePath} to the
|
||||
* column of the {@link ResultSet} that holds the data for that
|
||||
* {@link org.springframework.data.relational.core.mapping.AggregatePath}.
|
||||
*/
|
||||
AggregateResultSetExtractor(RelationalMappingContext context, RelationalPersistentEntity<T> rootEntity,
|
||||
JdbcConverter converter, PathToColumnMapping pathToColumn) {
|
||||
|
||||
Assert.notNull(context, "context must not be null");
|
||||
Assert.notNull(rootEntity, "rootEntity must not be null");
|
||||
Assert.notNull(converter, "converter must not be null");
|
||||
Assert.notNull(pathToColumn, "propertyToColumn must not be null");
|
||||
|
||||
this.context = context;
|
||||
this.rootEntity = rootEntity;
|
||||
this.converter = converter;
|
||||
this.propertyToColumn = pathToColumn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<T> extractData(ResultSet resultSet) throws DataAccessException {
|
||||
|
||||
CachingResultSet crs = new CachingResultSet(resultSet);
|
||||
|
||||
CollectionReader reader = new CollectionReader(crs);
|
||||
|
||||
while (crs.next()) {
|
||||
reader.read();
|
||||
}
|
||||
|
||||
return (Iterable<T>) reader.getResultAndReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* create an instance and populate all its properties
|
||||
*/
|
||||
@Nullable
|
||||
private Object hydrateInstance(EntityInstantiator instantiator, ResultSetParameterValueProvider valueProvider,
|
||||
RelationalPersistentEntity<?> entity) {
|
||||
|
||||
if (!valueProvider.basePath.isRoot() && // this is a nested ValueProvider
|
||||
valueProvider.basePath.getRequiredLeafProperty().isEmbedded() && // it's an embedded
|
||||
!valueProvider.basePath.getRequiredLeafProperty().shouldCreateEmptyEmbedded() && // it's embedded
|
||||
!valueProvider.hasValue()) { // all values have been null
|
||||
return null;
|
||||
}
|
||||
|
||||
Object instance = instantiator.createInstance(entity, valueProvider);
|
||||
|
||||
PersistentPropertyAccessor<?> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
|
||||
converter.getConversionService());
|
||||
|
||||
if (entity.requiresPropertyPopulation()) {
|
||||
|
||||
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) p -> {
|
||||
|
||||
if (!entity.isCreatorArgument(p)) {
|
||||
accessor.setProperty(p, valueProvider.getValue(p));
|
||||
}
|
||||
});
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} is responsible for reading a single entity or collection of entities from a set of columns
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private interface Reader {
|
||||
|
||||
/**
|
||||
* read the data needed for creating the result of this {@literal Reader}
|
||||
*/
|
||||
void read();
|
||||
|
||||
/**
|
||||
* Checks if this {@literal Reader} has all the data needed for a complete result, or if it needs to read further
|
||||
* rows.
|
||||
*
|
||||
* @return the result of the check.
|
||||
*/
|
||||
boolean hasResult();
|
||||
|
||||
/**
|
||||
* Constructs the result, returns it and resets the state of the reader to read the next instance.
|
||||
*
|
||||
* @return an instance of whatever this {@literal Reader} is supposed to read.
|
||||
*/
|
||||
@Nullable
|
||||
Object getResultAndReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a {@link Map} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private static class MapAdapter extends AbstractCollection<Map.Entry<Object, Object>> {
|
||||
|
||||
private final Map<Object, Object> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<Object, Object>> iterator() {
|
||||
return map.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Map.Entry<Object, Object> entry) {
|
||||
|
||||
map.put(entry.getKey(), entry.getValue());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a {@link List} to the interface of a {@literal Collection<Map.Entry<Object, Object>>}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private static class ListAdapter extends AbstractCollection<Map.Entry<Object, Object>> {
|
||||
|
||||
private final List<Object> list = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<Object, Object>> iterator() {
|
||||
throw new UnsupportedOperationException("Do we need this?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(Map.Entry<Object, Object> entry) {
|
||||
|
||||
Integer index = (Integer) entry.getKey();
|
||||
while (index >= list.size()) {
|
||||
list.add(null);
|
||||
}
|
||||
list.set(index, entry.getValue());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} for reading entities.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private class EntityReader implements Reader {
|
||||
|
||||
/**
|
||||
* Debugging the recursive structure of {@link Reader} instances can become a little mind bending. Giving each
|
||||
* {@literal Reader} a descriptive name helps with that.
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
private final AggregatePath basePath;
|
||||
private final CachingResultSet crs;
|
||||
|
||||
private final EntityInstantiator instantiator;
|
||||
@Nullable private final String idColumn;
|
||||
|
||||
private ResultSetParameterValueProvider valueProvider;
|
||||
private boolean result;
|
||||
|
||||
Object oldId = null;
|
||||
|
||||
private EntityReader(AggregatePath basePath, CachingResultSet crs) {
|
||||
this(basePath, crs, null);
|
||||
}
|
||||
|
||||
private EntityReader(AggregatePath basePath, CachingResultSet crs, @Nullable String keyColumn) {
|
||||
|
||||
this.basePath = basePath;
|
||||
this.crs = crs;
|
||||
|
||||
RelationalPersistentEntity<?> entity = basePath.isRoot() ? rootEntity : basePath.getRequiredLeafEntity();
|
||||
instantiator = converter.getEntityInstantiators().getInstantiatorFor(entity);
|
||||
|
||||
idColumn = entity.hasIdProperty() ? propertyToColumn.column(basePath.append(entity.getRequiredIdProperty()))
|
||||
: keyColumn;
|
||||
|
||||
reset();
|
||||
|
||||
name = "EntityReader for " + (basePath.isRoot() ? "<root>" : basePath.toDotPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read() {
|
||||
|
||||
if (idColumn != null && oldId == null) {
|
||||
oldId = crs.getObject(idColumn);
|
||||
}
|
||||
|
||||
valueProvider.readValues();
|
||||
if (idColumn == null) {
|
||||
result = true;
|
||||
} else {
|
||||
Object peekedId = crs.peek(idColumn);
|
||||
if (peekedId == null || !peekedId.equals(oldId)) {
|
||||
|
||||
result = true;
|
||||
oldId = peekedId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object getResultAndReset() {
|
||||
|
||||
try {
|
||||
return hydrateInstance(instantiator, valueProvider, valueProvider.baseEntity);
|
||||
} finally {
|
||||
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
|
||||
valueProvider = new ResultSetParameterValueProvider(crs, basePath);
|
||||
result = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} for reading collections of entities.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class CollectionReader implements Reader {
|
||||
|
||||
// debugging only
|
||||
private final String name;
|
||||
|
||||
private final Supplier<Collection> collectionInitializer;
|
||||
private final Reader entityReader;
|
||||
|
||||
private Collection result;
|
||||
|
||||
private static Supplier<Collection> collectionInitializerFor(AggregatePath path) {
|
||||
|
||||
RelationalPersistentProperty property = path.getRequiredLeafProperty();
|
||||
if (List.class.isAssignableFrom(property.getType())) {
|
||||
return ListAdapter::new;
|
||||
} else if (property.isMap()) {
|
||||
return MapAdapter::new;
|
||||
} else {
|
||||
return HashSet::new;
|
||||
}
|
||||
}
|
||||
|
||||
private CollectionReader(AggregatePath basePath, CachingResultSet crs) {
|
||||
|
||||
this.collectionInitializer = collectionInitializerFor(basePath);
|
||||
|
||||
String keyColumn = null;
|
||||
final RelationalPersistentProperty property = basePath.getRequiredLeafProperty();
|
||||
if (property.isMap() || List.class.isAssignableFrom(basePath.getRequiredLeafProperty().getType())) {
|
||||
keyColumn = propertyToColumn.keyColumn(basePath);
|
||||
}
|
||||
|
||||
if (property.isQualified()) {
|
||||
this.entityReader = new EntryReader(basePath, crs, keyColumn, property.getQualifierColumnType());
|
||||
} else {
|
||||
this.entityReader = new EntityReader(basePath, crs, keyColumn);
|
||||
}
|
||||
reset();
|
||||
name = "Reader for " + basePath.toDotPath();
|
||||
}
|
||||
|
||||
private CollectionReader(CachingResultSet crs) {
|
||||
|
||||
this.collectionInitializer = ArrayList::new;
|
||||
this.entityReader = new EntityReader(context.getAggregatePath(rootEntity), crs);
|
||||
reset();
|
||||
|
||||
name = "Collectionreader for <root>";
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read() {
|
||||
|
||||
entityReader.read();
|
||||
if (entityReader.hasResult()) {
|
||||
result.add(entityReader.getResultAndReset());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResultAndReset() {
|
||||
|
||||
try {
|
||||
if (result instanceof MapAdapter) {
|
||||
return ((MapAdapter) result).map;
|
||||
}
|
||||
if (result instanceof ListAdapter) {
|
||||
return ((ListAdapter) result).list;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
result = collectionInitializer.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link Reader} for reading collection entries. Most of the work is done by an {@link EntityReader}, but a
|
||||
* additional key column might get read. The result is
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private class EntryReader implements Reader {
|
||||
|
||||
final EntityReader delegate;
|
||||
final String keyColumn;
|
||||
private final TypeInformation<?> keyColumnType;
|
||||
|
||||
Object key;
|
||||
|
||||
EntryReader(AggregatePath basePath, CachingResultSet crs, String keyColumn, Class<?> keyColumnType) {
|
||||
|
||||
this.keyColumnType = TypeInformation.of(keyColumnType);
|
||||
this.delegate = new EntityReader(basePath, crs, keyColumn);
|
||||
this.keyColumn = keyColumn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read() {
|
||||
|
||||
if (key == null) {
|
||||
Object unconvertedKeyObject = delegate.crs.getObject(keyColumn);
|
||||
key = converter.readValue(unconvertedKeyObject, keyColumnType);
|
||||
}
|
||||
delegate.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return delegate.hasResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getResultAndReset() {
|
||||
|
||||
try {
|
||||
return new AbstractMap.SimpleEntry<>(key, delegate.getResultAndReset());
|
||||
} finally {
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ParameterValueProvider} that provided the values for an entity from a continues set of rows in a {@link ResultSet}. These might be referenced entities or collections of such entities. {@link ResultSet}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
private class ResultSetParameterValueProvider implements ParameterValueProvider<RelationalPersistentProperty> {
|
||||
|
||||
private final CachingResultSet rs;
|
||||
/**
|
||||
* The path which is used to determine columnNames
|
||||
*/
|
||||
private final AggregatePath basePath;
|
||||
private final RelationalPersistentEntity<?> baseEntity;
|
||||
|
||||
/**
|
||||
* Holds all the values for the entity, either directly or in the form of an appropriate {@link Reader}.
|
||||
*/
|
||||
private final Map<RelationalPersistentProperty, Object> aggregatedValues = new HashMap<>();
|
||||
|
||||
ResultSetParameterValueProvider(CachingResultSet rs, AggregatePath basePath) {
|
||||
|
||||
this.rs = rs;
|
||||
this.basePath = basePath;
|
||||
this.baseEntity = basePath.isRoot() ? rootEntity
|
||||
: context.getRequiredPersistentEntity(basePath.getRequiredLeafProperty().getActualType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
@Nullable
|
||||
public <S> S getParameterValue(Parameter<S, RelationalPersistentProperty> parameter) {
|
||||
|
||||
return (S) getValue(baseEntity.getRequiredPersistentProperty(parameter.getName()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getValue(RelationalPersistentProperty property) {
|
||||
|
||||
Object value = aggregatedValues.get(property);
|
||||
|
||||
if (value instanceof Reader) {
|
||||
return ((Reader) value).getResultAndReset();
|
||||
}
|
||||
|
||||
value = converter.readValue(value, property.getTypeInformation());
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* read values for all collection like properties and aggregate them in a collection.
|
||||
*/
|
||||
void readValues() {
|
||||
baseEntity.forEach(this::readValue);
|
||||
}
|
||||
|
||||
private void readValue(RelationalPersistentProperty p) {
|
||||
|
||||
if (p.isEntity()) {
|
||||
|
||||
Reader reader = null;
|
||||
|
||||
if (p.isCollectionLike() || p.isMap()) { // even when there are no values we still want a (empty) collection.
|
||||
|
||||
reader = (Reader) aggregatedValues.computeIfAbsent(p, pp -> new CollectionReader(basePath.append(pp), rs));
|
||||
}
|
||||
if (getIndicatorOf(p) != null) {
|
||||
|
||||
if (!(p.isCollectionLike() || p.isMap())) { // for single entities we want a null entity instead of on filled
|
||||
// with null values.
|
||||
|
||||
reader = (Reader) aggregatedValues.computeIfAbsent(p, pp -> new EntityReader(basePath.append(pp), rs));
|
||||
}
|
||||
|
||||
Assert.state(reader != null, "reader must not be null");
|
||||
|
||||
reader.read();
|
||||
}
|
||||
} else {
|
||||
aggregatedValues.computeIfAbsent(p, this::getObject);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getIndicatorOf(RelationalPersistentProperty p) {
|
||||
if (p.isMap() || List.class.isAssignableFrom(p.getType())) {
|
||||
return rs.getObject(getKeyName(p));
|
||||
}
|
||||
|
||||
if (p.isEmbedded()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return rs.getObject(getColumnName(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a single columnValue from the resultset without throwing an exception. If the column does not exist a null
|
||||
* value is returned. Does not instantiate complex objects.
|
||||
*
|
||||
* @param property
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private Object getObject(RelationalPersistentProperty property) {
|
||||
return rs.getObject(getColumnName(property));
|
||||
}
|
||||
|
||||
/**
|
||||
* converts a property into a column name representing that property.
|
||||
*
|
||||
* @param property
|
||||
* @return
|
||||
*/
|
||||
private String getColumnName(RelationalPersistentProperty property) {
|
||||
|
||||
return propertyToColumn.column(basePath.append(property));
|
||||
}
|
||||
|
||||
private String getKeyName(RelationalPersistentProperty property) {
|
||||
|
||||
return propertyToColumn.keyColumn(basePath.append(property));
|
||||
}
|
||||
|
||||
private boolean hasValue() {
|
||||
|
||||
for (Object value : aggregatedValues.values()) {
|
||||
if (value != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,6 +394,7 @@ public class BasicJdbcConverter extends BasicRelationalConverter implements Jdbc
|
||||
}
|
||||
|
||||
private <S> ReadingContext<S> extendBy(RelationalPersistentProperty property) {
|
||||
|
||||
return new ReadingContext<>(
|
||||
(RelationalPersistentEntity<S>) getMappingContext().getRequiredPersistentEntity(property.getActualType()),
|
||||
rootPath.append(property), path.append(property), identifier, key,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Despite its name not really a {@link ResultSet}, but it offers the part of the {@literal ResultSet} API that is used
|
||||
* by {@link AggregateReader}. It allows peeking in the next row of a ResultSet by caching one row of the ResultSet.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class CachingResultSet {
|
||||
|
||||
private final ResultSetAccessor accessor;
|
||||
private final ResultSet resultSet;
|
||||
private Cache cache;
|
||||
|
||||
CachingResultSet(ResultSet resultSet) {
|
||||
|
||||
this.accessor = new ResultSetAccessor(resultSet);
|
||||
this.resultSet = resultSet;
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
|
||||
if (isPeeking()) {
|
||||
|
||||
final boolean next = cache.next;
|
||||
cache = null;
|
||||
return next;
|
||||
}
|
||||
|
||||
try {
|
||||
return resultSet.next();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to advance CachingResultSet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Object getObject(String columnLabel) {
|
||||
|
||||
Object returnValue;
|
||||
if (isPeeking()) {
|
||||
returnValue = cache.values.get(columnLabel);
|
||||
} else {
|
||||
returnValue = safeGetFromDelegate(columnLabel);
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Object peek(String columnLabel) {
|
||||
|
||||
if (!isPeeking()) {
|
||||
createCache();
|
||||
}
|
||||
|
||||
if (!cache.next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return safeGetFromDelegate(columnLabel);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object safeGetFromDelegate(String columnLabel) {
|
||||
return accessor.getObject(columnLabel);
|
||||
}
|
||||
|
||||
private void createCache() {
|
||||
cache = new Cache();
|
||||
|
||||
try {
|
||||
int columnCount = resultSet.getMetaData().getColumnCount();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
// at least some databases return lower case labels although rs.getObject(UPPERCASE_LABEL) returns the expected
|
||||
// value. The aliases we use happen to be uppercase. So we transform everything to upper case.
|
||||
cache.add(resultSet.getMetaData().getColumnLabel(i).toLowerCase(),
|
||||
accessor.getObject(resultSet.getMetaData().getColumnLabel(i)));
|
||||
}
|
||||
|
||||
cache.next = resultSet.next();
|
||||
} catch (SQLException se) {
|
||||
throw new RuntimeException("Can't cache result set data", se);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isPeeking() {
|
||||
return cache != null;
|
||||
}
|
||||
|
||||
private static class Cache {
|
||||
|
||||
boolean next;
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
|
||||
void add(String columnName, Object value) {
|
||||
values.put(columnName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.conversion.IdValueSource;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
@@ -69,6 +68,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
private final NamedParameterJdbcOperations operations;
|
||||
private final SqlParametersFactory sqlParametersFactory;
|
||||
private final InsertStrategyFactory insertStrategyFactory;
|
||||
private final ReadingDataAccessStrategy singleSelectDelegate;
|
||||
|
||||
/**
|
||||
* Creates a {@link DefaultDataAccessStrategy}
|
||||
@@ -96,6 +96,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
this.operations = operations;
|
||||
this.sqlParametersFactory = sqlParametersFactory;
|
||||
this.insertStrategyFactory = insertStrategyFactory;
|
||||
this.singleSelectDelegate = new SingleQueryDataAccessStrategy(context, sqlGeneratorSource.getDialect(), converter, operations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -260,6 +261,10 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
@Override
|
||||
public <T> T findById(Object id, Class<T> domainType) {
|
||||
|
||||
if (isSingleSelectQuerySupported(domainType)) {
|
||||
return singleSelectDelegate.findById(id, domainType);
|
||||
}
|
||||
|
||||
String findOneSql = sql(domainType).getFindOne();
|
||||
SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER);
|
||||
|
||||
@@ -272,6 +277,11 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType) {
|
||||
|
||||
if (isSingleSelectQuerySupported(domainType)){
|
||||
return singleSelectDelegate.findAll(domainType);
|
||||
}
|
||||
|
||||
return operations.query(sql(domainType).getFindAll(), getEntityRowMapper(domainType));
|
||||
}
|
||||
|
||||
@@ -282,10 +292,12 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if (isSingleSelectQuerySupported(domainType)){
|
||||
return singleSelectDelegate.findAllById(ids, domainType);
|
||||
}
|
||||
|
||||
SqlParameterSource parameterSource = sqlParametersFactory.forQueryByIds(ids, domainType);
|
||||
|
||||
String findAllInListSql = sql(domainType).getFindAllInList();
|
||||
|
||||
return operations.query(findAllInListSql, parameterSource, getEntityRowMapper(domainType));
|
||||
}
|
||||
|
||||
@@ -430,4 +442,40 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
return baseProperty.getOwner().getType();
|
||||
}
|
||||
|
||||
private boolean isSingleSelectQuerySupported(Class<?> entityType) {
|
||||
|
||||
return context.isSingleQueryLoadingEnabled() && sqlGeneratorSource.getDialect().supportsSingleQueryLoading()//
|
||||
&& entityQualifiesForSingleSelectQuery(entityType);
|
||||
}
|
||||
|
||||
private boolean entityQualifiesForSingleSelectQuery(Class<?> entityType) {
|
||||
|
||||
boolean referenceFound = false;
|
||||
for (PersistentPropertyPath<RelationalPersistentProperty> path : context.findPersistentPropertyPaths(entityType, __ -> true)) {
|
||||
RelationalPersistentProperty property = path.getLeafProperty();
|
||||
if (property.isEntity()) {
|
||||
|
||||
// embedded entities are currently not supported
|
||||
if (property.isEmbedded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only a single reference is currently supported
|
||||
if (referenceFound) {
|
||||
return false;
|
||||
}
|
||||
|
||||
referenceFound = true;
|
||||
}
|
||||
|
||||
// AggregateReferences aren't supported yet
|
||||
if (property.isAssociation()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
|
||||
/**
|
||||
* A mapping between {@link PersistentPropertyPath} and column names of a query. Column names are intentionally
|
||||
* represented by {@link String} values, since this is what a {@link java.sql.ResultSet} uses, and since all the query
|
||||
* columns should be aliases there is no need for quoting or similar as provided by
|
||||
* {@link org.springframework.data.relational.core.sql.SqlIdentifier}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public interface PathToColumnMapping {
|
||||
|
||||
String column(AggregatePath path);
|
||||
|
||||
String keyColumn(AggregatePath path);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.relational.core.query.Query;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* The finding methods of a {@link DataAccessStrategy}.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
interface ReadingDataAccessStrategy {
|
||||
/**
|
||||
* Loads a single entity identified by type and id.
|
||||
*
|
||||
* @param id the id of the entity to load. Must not be {@code null}.
|
||||
* @param domainType the domain type of the entity. Must not be {@code null}.
|
||||
* @param <T> the type of the entity.
|
||||
* @return Might return {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
<T> T findById(Object id, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Loads all entities of the given type.
|
||||
*
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> the type of entities to load.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Iterable<T> findAll(Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Loads all entities that match one of the ids passed as an argument. It is not guaranteed that the number of ids
|
||||
* passed in matches the number of entities returned.
|
||||
*
|
||||
* @param ids the Ids of the entities to load. Must not be {@code null}.
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> type of entities to load.
|
||||
* @return the loaded entities. Guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Loads all entities of the given type, sorted.
|
||||
*
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> the type of entities to load.
|
||||
* @param sort the sorting information. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @since 2.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Class<T> domainType, Sort sort);
|
||||
|
||||
/**
|
||||
* Loads all entities of the given type, paged and sorted.
|
||||
*
|
||||
* @param domainType the type of entities to load. Must not be {@code null}.
|
||||
* @param <T> the type of entities to load.
|
||||
* @param pageable the pagination information. Must not be {@code null}.
|
||||
* @return Guaranteed to be not {@code null}.
|
||||
* @since 2.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Class<T> domainType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting item to an entity ensuring exactly one result.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param domainType the type of entities. Must not be {@code null}.
|
||||
* @return exactly one result or {@link Optional#empty()} if no match found.
|
||||
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
|
||||
* @since 3.0
|
||||
*/
|
||||
<T> Optional<T> findOne(Query query, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param domainType the type of entities. Must not be {@code null}.
|
||||
* @return a non-null list with all the matching results.
|
||||
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
|
||||
* @since 3.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Query query, Class<T> domainType);
|
||||
|
||||
/**
|
||||
* Execute a {@code SELECT} query and convert the resulting items to a {@link Iterable}. Applies the {@link Pageable}
|
||||
* to the result.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param domainType the type of entities. Must not be {@literal null}.
|
||||
* @param pageable the pagination that should be applied. Must not be {@literal null}.
|
||||
* @return a non-null list with all the matching results.
|
||||
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
|
||||
* @since 3.0
|
||||
*/
|
||||
<T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.relational.core.dialect.Dialect;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.query.Query;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
|
||||
/**
|
||||
* A {@link ReadingDataAccessStrategy} that uses an {@link AggregateReader} to load entities with a single query.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class SingleQueryDataAccessStrategy implements ReadingDataAccessStrategy {
|
||||
private final AggregateReaderFactory readerFactory;
|
||||
private final RelationalMappingContext mappingContext;
|
||||
|
||||
public SingleQueryDataAccessStrategy(RelationalMappingContext mappingContext, Dialect dialect,
|
||||
JdbcConverter converter, NamedParameterJdbcOperations jdbcTemplate) {
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
this.readerFactory = new AggregateReaderFactory(mappingContext, dialect, converter, jdbcTemplate);
|
||||
;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findById(Object id, Class<T> domainType) {
|
||||
return getReader(domainType).findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType) {
|
||||
return getReader(domainType).findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
|
||||
return getReader(domainType).findAllById(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType, Sort sort) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Class<T> domainType, Pageable pageable) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Optional<T> findOne(Query query, Class<T> domainType) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Query query, Class<T> domainType) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAll(Query query, Class<T> domainType, Pageable pageable) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private <T> AggregateReader<T> getReader(Class<T> domainType) {
|
||||
|
||||
RelationalPersistentEntity<T> persistentEntity = (RelationalPersistentEntity<T>) mappingContext
|
||||
.getRequiredPersistentEntity(domainType);
|
||||
return readerFactory.createAggregateReaderFor(persistentEntity);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import java.util.function.Function;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -90,13 +91,21 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Transactional
|
||||
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
class JdbcAggregateTemplateIntegrationTests {
|
||||
abstract class AbstractJdbcAggregateTemplateIntegrationTests {
|
||||
|
||||
@Autowired JdbcAggregateOperations template;
|
||||
@Autowired NamedParameterJdbcOperations jdbcTemplate;
|
||||
@Autowired RelationalMappingContext mappingContext;
|
||||
|
||||
LegoSet legoSet = createLegoSet("Star Destroyer");
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach(){
|
||||
mappingContext.setSingleQueryLoadingEnabled(useSingleQuery());
|
||||
}
|
||||
|
||||
abstract boolean useSingleQuery();
|
||||
|
||||
/**
|
||||
* creates an instance of {@link NoIdListChain4} with the following properties:
|
||||
* <ul>
|
||||
@@ -193,6 +202,42 @@ class JdbcAggregateTemplateIntegrationTests {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void findById() {
|
||||
|
||||
WithInsertOnly entity = new WithInsertOnly();
|
||||
entity.insertOnly = "entity";
|
||||
entity = template.save(entity);
|
||||
|
||||
WithInsertOnly other = new WithInsertOnly();
|
||||
other.insertOnly = "other";
|
||||
other = template.save(other);
|
||||
|
||||
assertThat(template.findById(entity.id, WithInsertOnly.class).insertOnly).isEqualTo("entity");
|
||||
assertThat(template.findById(other.id, WithInsertOnly.class).insertOnly).isEqualTo("other");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void findAllById() {
|
||||
|
||||
WithInsertOnly entity = new WithInsertOnly();
|
||||
entity.insertOnly = "entity";
|
||||
entity = template.save(entity);
|
||||
|
||||
WithInsertOnly other = new WithInsertOnly();
|
||||
other.insertOnly = "other";
|
||||
other = template.save(other);
|
||||
|
||||
WithInsertOnly yetAnother = new WithInsertOnly();
|
||||
yetAnother.insertOnly = "yetAnother";
|
||||
yetAnother = template.save(yetAnother);
|
||||
|
||||
Iterable<WithInsertOnly> reloadedById = template.findAllById(asList(entity.id, yetAnother.id),
|
||||
WithInsertOnly.class);
|
||||
assertThat(reloadedById).extracting(e -> e.id, e -> e.insertOnly)
|
||||
.containsExactlyInAnyOrder(tuple(entity.id, "entity"), tuple(yetAnother.id, "yetAnother"));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
|
||||
void saveAndLoadAnEntityWithReferencedEntityById() {
|
||||
@@ -1833,4 +1878,17 @@ class JdbcAggregateTemplateIntegrationTests {
|
||||
return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
static class JdbcAggregateTemplateIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {
|
||||
@Override
|
||||
boolean useSingleQuery() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static class JdbcAggregateTemplateSqlIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests {
|
||||
@Override
|
||||
boolean useSingleQuery() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.jdbc.core.convert.AggregateResultSetExtractorUnitTests.ColumnType.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.AggregatePath;
|
||||
import org.springframework.data.relational.core.mapping.DefaultNamingStrategy;
|
||||
import org.springframework.data.relational.core.mapping.Embedded;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
|
||||
/**
|
||||
* Unit tests for the {@link AggregateResultSetExtractor}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class AggregateResultSetExtractorUnitTests {
|
||||
|
||||
RelationalMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
|
||||
private final JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class));
|
||||
|
||||
private final PathToColumnMapping column = new PathToColumnMapping() {
|
||||
@Override
|
||||
public String column(AggregatePath path) {
|
||||
return AggregateResultSetExtractorUnitTests.this.column(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String keyColumn(AggregatePath path) {
|
||||
return column(path) + "_key";
|
||||
}
|
||||
};
|
||||
|
||||
AggregateResultSetExtractor<SimpleEntity> extractor = getExtractor(SimpleEntity.class);
|
||||
|
||||
@Test // GH-1446
|
||||
void emptyResultSetYieldsEmptyResult() throws SQLException {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList("T0_C0_ID1", "T0_C1_NAME"));
|
||||
assertThat(extractor.extractData(resultSet)).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void singleSimpleEntityGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
1, "Alfred");
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
|
||||
.containsExactly(tuple(1L, "Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void multipleSimpleEntitiesGetExtractedFromMultipleRows() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
1, "Alfred", //
|
||||
2, "Bertram" //
|
||||
);
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name).containsExactly( //
|
||||
tuple(1L, "Alfred"), //
|
||||
tuple(2L, "Bertram") //
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Conversions {
|
||||
|
||||
@Test // GH-1446
|
||||
void appliesConversionToProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
new BigDecimal(1), "Alfred");
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
|
||||
.containsExactly(tuple(1L, "Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void appliesConversionToConstructorValue() {
|
||||
|
||||
AggregateResultSetExtractor<DummyRecord> extractor = getExtractor(DummyRecord.class);
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name")), //
|
||||
new BigDecimal(1), "Alfred");
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.name)
|
||||
.containsExactly(tuple(1L, "Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void appliesConversionToKeyValue() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, new BigDecimal(0), "Dummy Alfred", //
|
||||
1, new BigDecimal(1), "Dummy Berta", //
|
||||
1, new BigDecimal(2), "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> AggregateResultSetExtractor<T> getExtractor(Class<T> type) {
|
||||
return (AggregateResultSetExtractor<T>) new AggregateResultSetExtractor<>(context,
|
||||
(RelationalPersistentEntity<DummyRecord>) context.getPersistentEntity(type), converter, column);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class EmbeddedReference {
|
||||
@Test // GH-1446
|
||||
void embeddedGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNullable.dummyName")), //
|
||||
1, "Imani");
|
||||
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.embeddedNullable.dummyName)
|
||||
.containsExactly(tuple(1L, "Imani"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void nullEmbeddedGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNullable.dummyName")), //
|
||||
1, null);
|
||||
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.embeddedNullable)
|
||||
.containsExactly(tuple(1L, null));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void emptyEmbeddedGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("embeddedNonNull.dummyName")), //
|
||||
1, null);
|
||||
|
||||
assertThat(extractor.extractData(resultSet)) //
|
||||
.extracting(e -> e.id1, e -> e.embeddedNonNull.dummyName) //
|
||||
.containsExactly(tuple(1L, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ToOneRelationships {
|
||||
@Test // GH-1446
|
||||
void entityReferenceGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummy"), column("dummy.dummyName")), //
|
||||
1, 1, "Dummy Alfred");
|
||||
|
||||
assertThat(extractor.extractData(resultSet)) //
|
||||
.extracting(e -> e.id1, e -> e.dummy.dummyName) //
|
||||
.containsExactly(tuple(1L, "Dummy Alfred"));
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void nullEntityReferenceGetsExtractedFromSingleRow() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummy"), column("dummy.dummyName")), //
|
||||
1, null, "Dummy Alfred");
|
||||
|
||||
assertThat(extractor.extractData(resultSet)).extracting(e -> e.id1, e -> e.dummy)
|
||||
.containsExactly(tuple(1L, null));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Sets {
|
||||
|
||||
@Test // GH-1446
|
||||
void extractEmptySetReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummies"), column("dummies.dummyName")), //
|
||||
1, null, null, //
|
||||
1, null, null, //
|
||||
1, null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummies).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleSetReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummies"), column("dummies.dummyName")), //
|
||||
1, 1, "Dummy Alfred", //
|
||||
1, 1, "Dummy Berta", //
|
||||
1, 1, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSetReferenceAndSimpleProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("name"), column("dummies"), column("dummies.dummyName")), //
|
||||
1, "Simplicissimus", 1, "Dummy Alfred", //
|
||||
1, null, 1, "Dummy Berta", //
|
||||
1, null, 1, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
|
||||
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMultipleSetReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
|
||||
column("dummies"), column("dummies.dummyName"), //
|
||||
column("otherDummies"), column("otherDummies.dummyName")), //
|
||||
1, 1, "Dummy Alfred", 1, "Other Ephraim", //
|
||||
1, 1, "Dummy Berta", 1, "Other Zeno", //
|
||||
1, 1, "Dummy Carl", null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedSetsWithId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediates"), column("intermediates.iId"), column("intermediates.intermediateName"), //
|
||||
column("intermediates.dummies"), column("intermediates.dummies.dummyName")), //
|
||||
1, "Alfred", 1, 23, "Inami", 23, "Dustin", //
|
||||
1, null, 1, 23, null, 23, "Dora", //
|
||||
1, null, 1, 24, "Ina", 24, "Dotty", //
|
||||
1, null, 1, 25, "Ion", null, null, //
|
||||
2, "Bon Jovi", 2, 26, "Judith", 26, "Ephraim", //
|
||||
2, null, 2, 26, null, 26, "Erin", //
|
||||
2, null, 2, 27, "Joel", 27, "Erika", //
|
||||
2, null, 2, 28, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediates.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediates.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
assertThat(alfred.intermediates).extracting(d -> d.intermediateName).containsExactlyInAnyOrder("Inami", "Ina",
|
||||
"Ion");
|
||||
|
||||
assertThat(alfred.findInIntermediates("Inami").dummies).extracting(d -> d.dummyName)
|
||||
.containsExactlyInAnyOrder("Dustin", "Dora");
|
||||
assertThat(alfred.findInIntermediates("Ina").dummies).extracting(d -> d.dummyName)
|
||||
.containsExactlyInAnyOrder("Dotty");
|
||||
assertThat(alfred.findInIntermediates("Ion").dummies).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
|
||||
assertThat(bonJovy.intermediates).extracting(d -> d.intermediateName).containsExactlyInAnyOrder("Judith", "Joel",
|
||||
"Justin");
|
||||
assertThat(bonJovy.findInIntermediates("Judith").dummies).extracting(d -> d.dummyName)
|
||||
.containsExactlyInAnyOrder("Ephraim", "Erin");
|
||||
assertThat(bonJovy.findInIntermediates("Joel").dummies).extracting(d -> d.dummyName).containsExactly("Erika");
|
||||
assertThat(bonJovy.findInIntermediates("Justin").dummyList).isEmpty();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Lists {
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleListReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, 0, "Dummy Alfred", //
|
||||
1, 1, "Dummy Berta", //
|
||||
1, 2, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleUnorderedListReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, 0, "Dummy Alfred", //
|
||||
1, 2, "Dummy Carl", 1, 1, "Dummy Berta" //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractListReferenceAndSimpleProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("name"), column("dummyList", KEY), column("dummyList.dummyName")), //
|
||||
1, "Simplicissimus", 0, "Dummy Alfred", //
|
||||
1, null, 1, "Dummy Berta", //
|
||||
1, null, 2, "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMultipleCollectionReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
|
||||
column("dummyList", KEY), column("dummyList.dummyName"), //
|
||||
column("otherDummies"), column("otherDummies.dummyName")), //
|
||||
1, 0, "Dummy Alfred", 1, "Other Ephraim", //
|
||||
1, 1, "Dummy Berta", 1, "Other Zeno", //
|
||||
1, 2, "Dummy Carl", null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
assertThat(result.iterator().next().dummyList).extracting(d -> d.dummyName) //
|
||||
.containsExactly("Dummy Alfred", "Dummy Berta", "Dummy Carl");
|
||||
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedListsWithId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateList", KEY), column("intermediateList.iId"), column("intermediateList.intermediateName"), //
|
||||
column("intermediateList.dummyList", KEY), column("intermediateList.dummyList.dummyName")), //
|
||||
1, "Alfred", 0, 23, "Inami", 0, "Dustin", //
|
||||
1, null, 0, 23, null, 1, "Dora", //
|
||||
1, null, 1, 24, "Ina", 0, "Dotty", //
|
||||
1, null, 2, 25, "Ion", null, null, //
|
||||
2, "Bon Jovi", 0, 26, "Judith", 0, "Ephraim", //
|
||||
2, null, 0, 26, null, 1, "Erin", //
|
||||
2, null, 1, 27, "Joel", 0, "Erika", //
|
||||
2, null, 2, 28, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateList.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
assertThat(alfred.intermediateList).extracting(d -> d.intermediateName).containsExactly("Inami", "Ina", "Ion");
|
||||
|
||||
assertThat(alfred.findInIntermediateList("Inami").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Dustin", "Dora");
|
||||
assertThat(alfred.findInIntermediateList("Ina").dummyList).extracting(d -> d.dummyName).containsExactly("Dotty");
|
||||
assertThat(alfred.findInIntermediateList("Ion").dummyList).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
|
||||
assertThat(bonJovy.intermediateList).extracting(d -> d.intermediateName).containsExactly("Judith", "Joel",
|
||||
"Justin");
|
||||
assertThat(bonJovy.findInIntermediateList("Judith").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Ephraim", "Erin");
|
||||
assertThat(bonJovy.findInIntermediateList("Joel").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Erika");
|
||||
assertThat(bonJovy.findInIntermediateList("Justin").dummyList).isEmpty();
|
||||
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedListsWithOutId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateListNoId", KEY), column("intermediateListNoId.intermediateName"), //
|
||||
column("intermediateListNoId.dummyList", KEY), column("intermediateListNoId.dummyList.dummyName")), //
|
||||
1, "Alfred", 0, "Inami", 0, "Dustin", //
|
||||
1, null, 0, null, 1, "Dora", //
|
||||
1, null, 1, "Ina", 0, "Dotty", //
|
||||
1, null, 2, "Ion", null, null, //
|
||||
2, "Bon Jovi", 0, "Judith", 0, "Ephraim", //
|
||||
2, null, 0, null, 1, "Erin", //
|
||||
2, null, 1, "Joel", 0, "Erika", //
|
||||
2, null, 2, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateListNoId.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
assertThat(alfred.intermediateListNoId).extracting(d -> d.intermediateName).containsExactly("Inami", "Ina",
|
||||
"Ion");
|
||||
|
||||
assertThat(alfred.findInIntermediateListNoId("Inami").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Dustin", "Dora");
|
||||
assertThat(alfred.findInIntermediateListNoId("Ina").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Dotty");
|
||||
assertThat(alfred.findInIntermediateListNoId("Ion").dummyList).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
assertThat(bonJovy).extracting("id1", "name").containsExactly(2L, "Bon Jovi");
|
||||
assertThat(bonJovy.intermediateListNoId).extracting(d -> d.intermediateName).containsExactly("Judith", "Joel",
|
||||
"Justin");
|
||||
|
||||
assertThat(bonJovy.findInIntermediateListNoId("Judith").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Ephraim", "Erin");
|
||||
assertThat(bonJovy.findInIntermediateListNoId("Joel").dummyList).extracting(d -> d.dummyName)
|
||||
.containsExactly("Erika");
|
||||
assertThat(bonJovy.findInIntermediateListNoId("Justin").dummyList).isEmpty();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Maps {
|
||||
|
||||
@Test // GH-1446
|
||||
void extractSingleMapReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("dummyMap", KEY), column("dummyMap.dummyName")), //
|
||||
1, "alpha", "Dummy Alfred", //
|
||||
1, "beta", "Dummy Berta", //
|
||||
1, "gamma", "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
|
||||
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
|
||||
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
|
||||
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMapReferenceAndSimpleProperty() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(
|
||||
asList(column("id1"), column("name"), column("dummyMap", KEY), column("dummyMap.dummyName")), //
|
||||
1, "Simplicissimus", "alpha", "Dummy Alfred", //
|
||||
1, null, "beta", "Dummy Berta", //
|
||||
1, null, "gamma", "Dummy Carl");
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name).containsExactly(tuple(1L, "Simplicissimus"));
|
||||
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
|
||||
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
|
||||
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
|
||||
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractMultipleCollectionReference() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), //
|
||||
column("dummyMap", KEY), column("dummyMap.dummyName"), //
|
||||
column("otherDummies"), column("otherDummies.dummyName")), //
|
||||
1, "alpha", "Dummy Alfred", 1, "Other Ephraim", //
|
||||
1, "beta", "Dummy Berta", 1, "Other Zeno", //
|
||||
1, "gamma", "Dummy Carl", null, null);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1).containsExactly(1L);
|
||||
Map<String, DummyEntity> dummyMap = result.iterator().next().dummyMap;
|
||||
assertThat(dummyMap).extracting("alpha").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Alfred");
|
||||
assertThat(dummyMap).extracting("beta").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Berta");
|
||||
assertThat(dummyMap).extracting("gamma").extracting(d -> ((DummyEntity) d).dummyName).isEqualTo("Dummy Carl");
|
||||
|
||||
assertThat(result.iterator().next().otherDummies).extracting(d -> d.dummyName) //
|
||||
.containsExactlyInAnyOrder("Other Ephraim", "Other Zeno");
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedMapsWithId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateMap", KEY), column("intermediateMap.iId"), column("intermediateMap.intermediateName"), //
|
||||
column("intermediateMap.dummyMap", KEY), column("intermediateMap.dummyMap.dummyName")), //
|
||||
1, "Alfred", "alpha", 23, "Inami", "omega", "Dustin", //
|
||||
1, null, "alpha", 23, null, "zeta", "Dora", //
|
||||
1, null, "beta", 24, "Ina", "eta", "Dotty", //
|
||||
1, null, "gamma", 25, "Ion", null, null, //
|
||||
2, "Bon Jovi", "phi", 26, "Judith", "theta", "Ephraim", //
|
||||
2, null, "phi", 26, null, "jota", "Erin", //
|
||||
2, null, "chi", 27, "Joel", "sigma", "Erika", //
|
||||
2, null, "psi", 28, "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateMap.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
|
||||
assertThat(alfred.intermediateMap.get("alpha").dummyMap.get("omega").dummyName).isEqualTo("Dustin");
|
||||
assertThat(alfred.intermediateMap.get("alpha").dummyMap.get("zeta").dummyName).isEqualTo("Dora");
|
||||
assertThat(alfred.intermediateMap.get("beta").dummyMap.get("eta").dummyName).isEqualTo("Dotty");
|
||||
assertThat(alfred.intermediateMap.get("gamma").dummyMap).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
|
||||
assertThat(bonJovy.intermediateMap.get("phi").dummyMap.get("theta").dummyName).isEqualTo("Ephraim");
|
||||
assertThat(bonJovy.intermediateMap.get("phi").dummyMap.get("jota").dummyName).isEqualTo("Erin");
|
||||
assertThat(bonJovy.intermediateMap.get("chi").dummyMap.get("sigma").dummyName).isEqualTo("Erika");
|
||||
assertThat(bonJovy.intermediateMap.get("psi").dummyMap).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-1446
|
||||
void extractNestedMapsWithOutId() {
|
||||
|
||||
ResultSet resultSet = ResultSetTestUtil.mockResultSet(asList(column("id1"), column("name"), //
|
||||
column("intermediateMapNoId", KEY), column("intermediateMapNoId.intermediateName"), //
|
||||
column("intermediateMapNoId.dummyMap", KEY), column("intermediateMapNoId.dummyMap.dummyName")), //
|
||||
1, "Alfred", "alpha", "Inami", "omega", "Dustin", //
|
||||
1, null, "alpha", null, "zeta", "Dora", //
|
||||
1, null, "beta", "Ina", "eta", "Dotty", //
|
||||
1, null, "gamma", "Ion", null, null, //
|
||||
2, "Bon Jovi", "phi", "Judith", "theta", "Ephraim", //
|
||||
2, null, "phi", null, "jota", "Erin", //
|
||||
2, null, "chi", "Joel", "sigma", "Erika", //
|
||||
2, null, "psi", "Justin", null, null //
|
||||
);
|
||||
|
||||
Iterable<SimpleEntity> result = extractor.extractData(resultSet);
|
||||
|
||||
assertThat(result).extracting(e -> e.id1, e -> e.name, e -> e.intermediateMapNoId.size())
|
||||
.containsExactlyInAnyOrder(tuple(1L, "Alfred", 3), tuple(2L, "Bon Jovi", 3));
|
||||
|
||||
final Iterator<SimpleEntity> iter = result.iterator();
|
||||
SimpleEntity alfred = iter.next();
|
||||
assertThat(alfred).extracting("id1", "name").containsExactly(1L, "Alfred");
|
||||
|
||||
assertThat(alfred.intermediateMapNoId.get("alpha").dummyMap.get("omega").dummyName).isEqualTo("Dustin");
|
||||
assertThat(alfred.intermediateMapNoId.get("alpha").dummyMap.get("zeta").dummyName).isEqualTo("Dora");
|
||||
assertThat(alfred.intermediateMapNoId.get("beta").dummyMap.get("eta").dummyName).isEqualTo("Dotty");
|
||||
assertThat(alfred.intermediateMapNoId.get("gamma").dummyMap).isEmpty();
|
||||
|
||||
SimpleEntity bonJovy = iter.next();
|
||||
|
||||
assertThat(bonJovy.intermediateMapNoId.get("phi").dummyMap.get("theta").dummyName).isEqualTo("Ephraim");
|
||||
assertThat(bonJovy.intermediateMapNoId.get("phi").dummyMap.get("jota").dummyName).isEqualTo("Erin");
|
||||
assertThat(bonJovy.intermediateMapNoId.get("chi").dummyMap.get("sigma").dummyName).isEqualTo("Erika");
|
||||
assertThat(bonJovy.intermediateMapNoId.get("psi").dummyMap).isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String column(String path) {
|
||||
return column(path, NORMAL);
|
||||
}
|
||||
|
||||
private String column(String path, ColumnType columnType) {
|
||||
|
||||
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
|
||||
SimpleEntity.class);
|
||||
|
||||
return column(context.getAggregatePath(propertyPath)) + (columnType == KEY ? "_key" : "");
|
||||
}
|
||||
|
||||
private String column(AggregatePath path) {
|
||||
return path.toDotPath();
|
||||
}
|
||||
|
||||
enum ColumnType {
|
||||
NORMAL, KEY
|
||||
}
|
||||
|
||||
private static class SimpleEntity {
|
||||
|
||||
@Id long id1;
|
||||
String name;
|
||||
DummyEntity dummy;
|
||||
@Embedded.Nullable DummyEntity embeddedNullable;
|
||||
@Embedded.Empty DummyEntity embeddedNonNull;
|
||||
|
||||
Set<Intermediate> intermediates;
|
||||
|
||||
Set<DummyEntity> dummies;
|
||||
Set<DummyEntity> otherDummies;
|
||||
|
||||
List<DummyEntity> dummyList;
|
||||
List<Intermediate> intermediateList;
|
||||
List<IntermediateNoId> intermediateListNoId;
|
||||
|
||||
Map<String, DummyEntity> dummyMap;
|
||||
Map<String, Intermediate> intermediateMap;
|
||||
Map<String, IntermediateNoId> intermediateMapNoId;
|
||||
|
||||
Intermediate findInIntermediates(String name) {
|
||||
for (Intermediate intermediate : intermediates) {
|
||||
if (intermediate.intermediateName.equals(name)) {
|
||||
return intermediate;
|
||||
}
|
||||
}
|
||||
fail("No intermediate with name " + name + " found in intermediates.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Intermediate findInIntermediateList(String name) {
|
||||
for (Intermediate intermediate : intermediateList) {
|
||||
if (intermediate.intermediateName.equals(name)) {
|
||||
return intermediate;
|
||||
}
|
||||
}
|
||||
fail("No intermediate with name " + name + " found in intermediateList.");
|
||||
return null;
|
||||
}
|
||||
|
||||
IntermediateNoId findInIntermediateListNoId(String name) {
|
||||
for (IntermediateNoId intermediate : intermediateListNoId) {
|
||||
if (intermediate.intermediateName.equals(name)) {
|
||||
return intermediate;
|
||||
}
|
||||
}
|
||||
fail("No intermediates with name " + name + " found in intermediateListNoId.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Intermediate {
|
||||
|
||||
@Id long iId;
|
||||
String intermediateName;
|
||||
|
||||
Set<DummyEntity> dummies;
|
||||
List<DummyEntity> dummyList;
|
||||
Map<String, DummyEntity> dummyMap;
|
||||
}
|
||||
|
||||
private static class IntermediateNoId {
|
||||
|
||||
String intermediateName;
|
||||
|
||||
Set<DummyEntity> dummies;
|
||||
List<DummyEntity> dummyList;
|
||||
Map<String, DummyEntity> dummyMap;
|
||||
}
|
||||
|
||||
private static class DummyEntity {
|
||||
String dummyName;
|
||||
Long longValue;
|
||||
}
|
||||
|
||||
private record DummyRecord(Long id1, String name) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.convert;
|
||||
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import javax.naming.OperationNotSupportedException;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Utility for mocking ResultSets for tests.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class ResultSetTestUtil {
|
||||
|
||||
static ResultSet mockResultSet(List<String> columns, Object... values) {
|
||||
|
||||
Assert.isTrue( //
|
||||
values.length % columns.size() == 0, //
|
||||
String //
|
||||
.format( //
|
||||
"Number of values [%d] must be a multiple of the number of columns [%d]", //
|
||||
values.length, //
|
||||
columns.size() //
|
||||
) //
|
||||
);
|
||||
|
||||
List<Map<String, Object>> result = convertValues(columns, values);
|
||||
|
||||
return mock(ResultSet.class, new ResultSetAnswer(columns, result));
|
||||
}
|
||||
|
||||
|
||||
private static List<Map<String, Object>> convertValues(List<String> columns, Object[] values) {
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
|
||||
int index = 0;
|
||||
while (index < values.length) {
|
||||
|
||||
Map<String, Object> row = new LinkedCaseInsensitiveMap<>();
|
||||
result.add(row);
|
||||
for (String column : columns) {
|
||||
|
||||
row.put(column, values[index]);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class ResultSetAnswer implements Answer<Object> {
|
||||
|
||||
private final List<String> names;
|
||||
private final List<Map<String, Object>> values;
|
||||
private int index = -1;
|
||||
|
||||
ResultSetAnswer(List<String> names, List<Map<String, Object>> values) {
|
||||
|
||||
this.names = names;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
|
||||
switch (invocation.getMethod().getName()) {
|
||||
case "next" -> {
|
||||
return next();
|
||||
}
|
||||
case "getObject" -> {
|
||||
Object argument = invocation.getArgument(0);
|
||||
String name = argument instanceof Integer ? names.get(((Integer) argument) - 1) : (String) argument;
|
||||
return getObject(name);
|
||||
}
|
||||
case "isAfterLast" -> {
|
||||
return isAfterLast();
|
||||
}
|
||||
case "isBeforeFirst" -> {
|
||||
return isBeforeFirst();
|
||||
}
|
||||
case "getRow" -> {
|
||||
return isAfterLast() || isBeforeFirst() ? 0 : index + 1;
|
||||
}
|
||||
case "toString" -> {
|
||||
return this.toString();
|
||||
}
|
||||
case "findColumn" -> {
|
||||
return isThereAColumnNamed(invocation.getArgument(0));
|
||||
}
|
||||
case "getMetaData" -> {
|
||||
return new MockedMetaData();
|
||||
}
|
||||
default -> throw new OperationNotSupportedException(invocation.getMethod().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private int isThereAColumnNamed(String name) {
|
||||
throw new UnsupportedOperationException("duh");
|
||||
// Optional<Map<String, Object>> first = values.stream().filter(s -> s.equals(name)).findFirst();
|
||||
// return (first.isPresent()) ? 1 : 0;
|
||||
}
|
||||
|
||||
private boolean isAfterLast() {
|
||||
return index >= values.size() && !values.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isBeforeFirst() {
|
||||
return index < 0 && !values.isEmpty();
|
||||
}
|
||||
|
||||
private Object getObject(String column) throws SQLException {
|
||||
|
||||
Map<String, Object> rowMap = values.get(index);
|
||||
|
||||
if (!rowMap.containsKey(column)) {
|
||||
throw new SQLException(String.format("Trying to access a column (%s) that does not exist", column));
|
||||
}
|
||||
|
||||
return rowMap.get(column);
|
||||
}
|
||||
|
||||
private boolean next() {
|
||||
|
||||
index++;
|
||||
return index < values.size();
|
||||
}
|
||||
|
||||
private class MockedMetaData implements ResultSetMetaData {
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return names.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoIncrement(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCaseSensitive(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSearchable(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrency(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int isNullable(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSigned(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnDisplaySize(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnLabel(int i) {
|
||||
return names.get(i - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSchemaName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrecision(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScale(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTableName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCatalogName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnType(int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnTypeName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefinitelyWritable(int i) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnClassName(int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unwrap(Class<T> aClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWrapperFor(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -160,8 +160,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
|
||||
repository.saveAll(asList(entityA, entityB, entityC));
|
||||
|
||||
assertThat(repository.findByEnumTypeIn(Set.of(Direction.LEFT, Direction.RIGHT)))
|
||||
.extracting(entity -> entity.direction)
|
||||
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
|
||||
.extracting(entity -> entity.direction).containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
|
||||
}
|
||||
|
||||
@Test // GH-1212
|
||||
@@ -175,8 +174,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
|
||||
entityC.direction = Direction.RIGHT;
|
||||
repository.saveAll(asList(entityA, entityB, entityC));
|
||||
|
||||
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER)))
|
||||
.extracting(entity -> entity.direction)
|
||||
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER))).extracting(entity -> entity.direction)
|
||||
.containsExactly(Direction.CENTER);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,11 @@ import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.testcontainers.containers.MSSQLServerContainer;
|
||||
|
||||
import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
|
||||
|
||||
|
||||
/**
|
||||
* {@link DataSource} setup for PostgreSQL.
|
||||
* <p>
|
||||
@@ -36,14 +34,14 @@ import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
|
||||
* @see <a href="https://github.com/testcontainers/testcontainers-java/tree/master/modules/mssqlserver"></a>
|
||||
*/
|
||||
@Configuration
|
||||
@Profile({"mssql"})
|
||||
@Profile({ "mssql" })
|
||||
public class MsSqlDataSourceConfiguration extends DataSourceConfiguration {
|
||||
|
||||
public static final String MS_SQL_SERVER_VERSION = "mcr.microsoft.com/mssql/server:2019-CU16-ubuntu-20.04";
|
||||
public static final String MS_SQL_SERVER_VERSION = "mcr.microsoft.com/mssql/server:2022-CU5-ubuntu-20.04";
|
||||
private static MSSQLServerContainer<?> MSSQL_CONTAINER;
|
||||
|
||||
@Override
|
||||
protected DataSource createDataSource() {
|
||||
@Override
|
||||
protected DataSource createDataSource() {
|
||||
|
||||
if (MSSQL_CONTAINER == null) {
|
||||
|
||||
@@ -54,14 +52,13 @@ public class MsSqlDataSourceConfiguration extends DataSourceConfiguration {
|
||||
MSSQL_CONTAINER = container;
|
||||
}
|
||||
|
||||
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
|
||||
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
|
||||
sqlServerDataSource.setURL(MSSQL_CONTAINER.getJdbcUrl());
|
||||
sqlServerDataSource.setUser(MSSQL_CONTAINER.getUsername());
|
||||
sqlServerDataSource.setPassword(MSSQL_CONTAINER.getPassword());
|
||||
|
||||
return sqlServerDataSource;
|
||||
}
|
||||
|
||||
return sqlServerDataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void customizePopulator(ResourceDatabasePopulator populator) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
DROP TABLE DUMMY_ENTITY;
|
||||
DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS;
|
||||
|
||||
CREATE TABLE DUMMY_ENTITY ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY);
|
||||
@@ -1,3 +1,3 @@
|
||||
DROP TABLE DUMMY_ENTITY;
|
||||
DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS;
|
||||
|
||||
CREATE TABLE DUMMY_ENTITY ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY);
|
||||
Reference in New Issue
Block a user