DATAJDBC-131 - Basic support for Maps
Aggregate roots with properties of type java.util.Map get properly inserted, updated and deleted.
Known limitations:
- Naming strategy does not allow for multiple references via Set, Map or directly to the same entity.
- The table for the referenced Entity contains the column for the map key.
A workaround for that would be to manipulate the DbActions in the AggregateChange yourself.
This commit is contained in:
committed by
Greg Turnquist
parent
482f330f02
commit
b385f7f24f
@@ -29,7 +29,7 @@ public interface DataAccessStrategy {
|
||||
|
||||
<T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters);
|
||||
|
||||
<S> void update(S instance, Class<S> domainType);
|
||||
<T> void update(T instance, Class<T> domainType);
|
||||
|
||||
void delete(Object id, Class<?> domainType);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.data.jdbc.support.JdbcUtil;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.repository.core.EntityInformation;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
|
||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||
@@ -71,7 +72,6 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
|
||||
/**
|
||||
* Creates a {@link DefaultDataAccessStrategy} which references it self for resolution of recursive data accesses.
|
||||
*
|
||||
* Only suitable if this is the only access strategy in use.
|
||||
*/
|
||||
public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations,
|
||||
@@ -149,7 +149,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void deleteAll(PropertyPath propertyPath) {
|
||||
public void deleteAll(PropertyPath propertyPath) {
|
||||
operations.getJdbcOperations().update(sql(propertyPath.getOwningType().getType()).createDeleteAllSql(propertyPath));
|
||||
}
|
||||
|
||||
@@ -193,14 +193,18 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
|
||||
public Iterable findAllByProperty(Object rootId, JdbcPersistentProperty property) {
|
||||
|
||||
Class<?> actualType = property.getActualType();
|
||||
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName());
|
||||
boolean isMap = property.getTypeInformation().isMap();
|
||||
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName(),
|
||||
property.getKeyColumn());
|
||||
|
||||
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), rootId);
|
||||
|
||||
return (Iterable<T>) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType));
|
||||
return operations.query(findAllByProperty, parameter, isMap //
|
||||
? getMapEntityRowMapper(property) //
|
||||
: getEntityRowMapper(actualType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -287,7 +291,12 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, accessStrategy);
|
||||
}
|
||||
|
||||
private RowMapper getMapEntityRowMapper(JdbcPersistentProperty property) {
|
||||
return new MapEntityRowMapper(getEntityRowMapper(property.getActualType()), property.getKeyColumn());
|
||||
}
|
||||
|
||||
private <T> MapSqlParameterSource createIdParameterSource(Object id, Class<T> domainType) {
|
||||
|
||||
return new MapSqlParameterSource("id",
|
||||
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
|
||||
}
|
||||
@@ -313,5 +322,4 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy {
|
||||
private SqlGenerator sql(Class<?> domainType) {
|
||||
return sqlGeneratorSource.getSqlGenerator(domainType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -77,6 +77,13 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
private <T> Map<String, Object> createAdditionalColumnValues(Insert<T> insert) {
|
||||
|
||||
Map<String, Object> additionalColumnValues = new HashMap<>();
|
||||
addDependingOnInformation(insert, additionalColumnValues);
|
||||
additionalColumnValues.putAll(insert.getAdditionalValues());
|
||||
|
||||
return additionalColumnValues;
|
||||
}
|
||||
|
||||
private <T> void addDependingOnInformation(Insert<T> insert, Map<String, Object> additionalColumnValues) {
|
||||
DbAction dependingOn = insert.getDependingOn();
|
||||
|
||||
if (dependingOn != null) {
|
||||
@@ -88,7 +95,5 @@ class DefaultJdbcInterpreter implements Interpreter {
|
||||
|
||||
additionalColumnValues.put(columnName, identifier);
|
||||
}
|
||||
return additionalColumnValues;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import lombok.NonNull;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
@@ -80,6 +81,11 @@ class EntityRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
if (Set.class.isAssignableFrom(property.getType())) {
|
||||
propertyAccessor.setProperty(property, accessStrategy.findAllByProperty(id, property));
|
||||
} else if (Map.class.isAssignableFrom(property.getType())) {
|
||||
|
||||
Iterable<Object> allByProperty = accessStrategy.findAllByProperty(id, property);
|
||||
IterableOfEntryToMapConverter converter = new IterableOfEntryToMapConverter();
|
||||
propertyAccessor.setProperty(property, converter.convert(allByProperty));
|
||||
} else {
|
||||
propertyAccessor.setProperty(property, readFrom(resultSet, property, ""));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConditionalConverter;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A converter for creating a {@link Map} from an {@link Iterable<Map.Entry>}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class IterableOfEntryToMapConverter implements ConditionalConverter, Converter<Iterable, Map> {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map convert(Iterable source) {
|
||||
|
||||
HashMap result = new HashMap();
|
||||
|
||||
source.forEach(element -> {
|
||||
|
||||
if (!(element instanceof Entry)) {
|
||||
throw new IllegalArgumentException(String.format("Cannot convert %s to Map.Entry", element.getClass()));
|
||||
}
|
||||
|
||||
Entry entry = (Entry) element;
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to determine if the {@literal sourceType} can be converted to a {@link Map}. If this can not be determined,
|
||||
* because the sourceTyppe does not contain information about the element type it returns {@literal true}.
|
||||
*
|
||||
* @param sourceType {@link TypeDescriptor} to convert from.
|
||||
* @param targetType {@link TypeDescriptor} to convert to.
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
|
||||
Assert.notNull(sourceType, "Source type must not be null.");
|
||||
Assert.notNull(targetType, "Target type must not be null.");
|
||||
|
||||
if (!sourceType.isAssignableTo(TypeDescriptor.valueOf(Iterable.class)))
|
||||
return false;
|
||||
|
||||
TypeDescriptor elementDescriptor = sourceType.getElementTypeDescriptor();
|
||||
return elementDescriptor == null || elementDescriptor.isAssignableTo(TypeDescriptor.valueOf(Entry.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link RowMapper} that maps a row to a {@link Map.Entry} so an {@link Iterable} of those can be converted to a
|
||||
* {@link Map} using an {@link IterableOfEntryToMapConverter}. Creation of the {@literal value} part of the resulting
|
||||
* {@link Map.Entry} is delegated to a {@link RowMapper} provided in the constructor.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
class MapEntityRowMapper<T> implements RowMapper<Map.Entry<Object, T>> {
|
||||
|
||||
private final RowMapper<T> delegate;
|
||||
private final String keyColumn;
|
||||
|
||||
MapEntityRowMapper(RowMapper<T> delegate, String keyColumn) {
|
||||
|
||||
this.delegate = delegate;
|
||||
this.keyColumn = keyColumn;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map.Entry<Object, T> mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
return new HashMap.SimpleEntry<>(rs.getObject(keyColumn), delegate.mapRow(rs, rowNum));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.data.jdbc.core;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
@@ -88,8 +89,24 @@ class SqlGenerator {
|
||||
return findAllSql.get();
|
||||
}
|
||||
|
||||
String getFindAllByProperty(String columnName) {
|
||||
return String.format("%s WHERE %s = :%s", findAllSql.get(), columnName, columnName);
|
||||
/**
|
||||
* Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships.
|
||||
* Results are limited to those rows referencing some other entity using the column specified by
|
||||
* {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on
|
||||
* a referencing entity.
|
||||
*
|
||||
* @param columnName name of the column of the FK back to the referencing entity.
|
||||
* @param keyColumn if the property is of type {@link Map} this column contains the map key.
|
||||
* @return a SQL String.
|
||||
*/
|
||||
String getFindAllByProperty(String columnName, String keyColumn) {
|
||||
|
||||
String baseSelect = (keyColumn != null) //
|
||||
? createSelectBuilder().column(cb -> cb.tableAlias(entity.getTableName()).column(keyColumn).as(keyColumn))
|
||||
.build()
|
||||
: getFindAll();
|
||||
|
||||
return String.format("%s WHERE %s = :%s", baseSelect, columnName, columnName);
|
||||
}
|
||||
|
||||
String getExists() {
|
||||
@@ -136,10 +153,19 @@ class SqlGenerator {
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the columns to the provided {@link SelectBuilder} representing simplem properties, including those from
|
||||
* one-to-one relationships.
|
||||
*
|
||||
* @param builder The {@link SelectBuilder} to be modified.
|
||||
*/
|
||||
private void addColumnsAndJoinsForOneToOneReferences(SelectBuilder builder) {
|
||||
|
||||
for (JdbcPersistentProperty property : entity) {
|
||||
if (!property.isEntity() || Collection.class.isAssignableFrom(property.getType())) {
|
||||
if (!property.isEntity() //
|
||||
|| Collection.class.isAssignableFrom(property.getType()) //
|
||||
|| Map.class.isAssignableFrom(property.getType()) //
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ package org.springframework.data.jdbc.core.conversion;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -40,6 +43,13 @@ public abstract class DbAction<T> {
|
||||
*/
|
||||
private final T entity;
|
||||
|
||||
/**
|
||||
* Key-value-pairs to specify additional values to be used with the statement which can't be obtained from the entity,
|
||||
* nor from {@link DbAction}s {@literal this} depends on. A used case are map keys, which need to be persisted with
|
||||
* the map value but aren't part of the value.
|
||||
*/
|
||||
private final Map<String, Object> additionalValues = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Another action, this action depends on. For example the insert for one entity might need the id of another entity,
|
||||
* which gets insert before this one. That action would be referenced by this property, so that the id becomes
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
*/
|
||||
package org.springframework.data.jdbc.core.conversion;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.data.jdbc.core.conversion.DbAction.Insert;
|
||||
@@ -56,7 +60,7 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
Insert<Object> insert = DbAction.insert(o, dependingOn);
|
||||
aggregateChange.addAction(insert);
|
||||
|
||||
referencedEntities(o).forEach(e -> saveReferencedEntities(e, aggregateChange, insert));
|
||||
referencedEntities(o).forEach(propertyAndValue -> saveReferencedEntities(propertyAndValue, aggregateChange, insert));
|
||||
} else {
|
||||
|
||||
deleteReferencedEntities(entityInformation.getRequiredId(o), aggregateChange);
|
||||
@@ -64,32 +68,35 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
Update<Object> update = DbAction.update(o, dependingOn);
|
||||
aggregateChange.addAction(update);
|
||||
|
||||
referencedEntities(o).forEach(e -> insertReferencedEntities(e, aggregateChange, update));
|
||||
referencedEntities(o).forEach(propertyAndValue -> insertReferencedEntities(propertyAndValue, aggregateChange, update));
|
||||
}
|
||||
}
|
||||
|
||||
private void saveReferencedEntities(Object o, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
private void saveReferencedEntities(PropertyAndValue propertyAndValue, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
|
||||
saveActions(o, dependingOn).forEach(a -> {
|
||||
saveActions(propertyAndValue, dependingOn).forEach(a -> {
|
||||
|
||||
aggregateChange.addAction(a);
|
||||
referencedEntities(o).forEach(e -> saveReferencedEntities(e, aggregateChange, a));
|
||||
referencedEntities(propertyAndValue.value).forEach(pav -> saveReferencedEntities(pav, aggregateChange, a));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private <T> Stream<DbAction> saveActions(T t, DbAction dependingOn) {
|
||||
private Stream<DbAction> saveActions(PropertyAndValue propertyAndValue, DbAction dependingOn) {
|
||||
|
||||
if (Collection.class.isAssignableFrom(ClassUtils.getUserClass(t))) {
|
||||
return collectionSaveAction((Collection) t, dependingOn);
|
||||
if (Map.Entry.class.isAssignableFrom(ClassUtils.getUserClass(propertyAndValue.value))) {
|
||||
return mapEntrySaveAction(propertyAndValue, dependingOn);
|
||||
}
|
||||
|
||||
return Stream.of(singleSaveAction(t, dependingOn));
|
||||
return Stream.of(singleSaveAction(propertyAndValue.value, dependingOn));
|
||||
}
|
||||
|
||||
private Stream<DbAction> collectionSaveAction(Collection collection, DbAction dependingOn) {
|
||||
private Stream<DbAction> mapEntrySaveAction(PropertyAndValue propertyAndValue, DbAction dependingOn) {
|
||||
|
||||
return collection.stream().map(e -> singleSaveAction(e, dependingOn));
|
||||
Map.Entry<Object, Object> entry = (Map.Entry) propertyAndValue.value;
|
||||
|
||||
DbAction action = singleSaveAction(entry.getValue(), dependingOn);
|
||||
action.getAdditionalValues().put(propertyAndValue.property.getKeyColumn(), entry.getKey());
|
||||
return Stream.of(action);
|
||||
}
|
||||
|
||||
private <T> DbAction singleSaveAction(T t, DbAction dependingOn) {
|
||||
@@ -100,22 +107,36 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
return entityInformation.isNew(t) ? DbAction.insert(t, dependingOn) : DbAction.update(t, dependingOn);
|
||||
}
|
||||
|
||||
private void insertReferencedEntities(Object o, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
private void insertReferencedEntities(PropertyAndValue propertyAndValue, AggregateChange aggregateChange, DbAction dependingOn) {
|
||||
|
||||
aggregateChange.addAction(DbAction.insert(o, dependingOn));
|
||||
referencedEntities(o).forEach(e -> insertReferencedEntities(e, aggregateChange, dependingOn));
|
||||
Insert<Object> insert;
|
||||
if (propertyAndValue.property.isMap()) {
|
||||
|
||||
Entry<Object, Object> valueAsEntry = (Entry<Object, Object>) propertyAndValue.value;
|
||||
insert = DbAction.insert(valueAsEntry.getValue(), dependingOn);
|
||||
insert.getAdditionalValues().put(propertyAndValue.property.getKeyColumn(), valueAsEntry.getKey());
|
||||
} else {
|
||||
insert = DbAction.insert(propertyAndValue.value, dependingOn);
|
||||
}
|
||||
|
||||
aggregateChange.addAction(insert);
|
||||
referencedEntities(insert.getEntity())
|
||||
.forEach(pav -> insertReferencedEntities(pav, aggregateChange, dependingOn));
|
||||
}
|
||||
|
||||
private Stream<Object> referencedEntities(Object o) {
|
||||
private Stream<PropertyAndValue> referencedEntities(Object o) {
|
||||
|
||||
JdbcPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(o.getClass());
|
||||
|
||||
return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) //
|
||||
.filter(PersistentProperty::isEntity)
|
||||
.flatMap(p -> referencedEntity(p, persistentEntity.getPropertyAccessor(o)));
|
||||
.filter(PersistentProperty::isEntity) //
|
||||
.flatMap( //
|
||||
p -> referencedEntity(p, persistentEntity.getPropertyAccessor(o)) //
|
||||
.map(e -> new PropertyAndValue(p, e)) //
|
||||
);
|
||||
}
|
||||
|
||||
private Stream<?> referencedEntity(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
private Stream<Object> referencedEntity(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Class<?> actualType = p.getActualType();
|
||||
JdbcPersistentEntity<?> persistentEntity = context //
|
||||
@@ -127,9 +148,15 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
|
||||
Class<?> type = p.getType();
|
||||
|
||||
return Collection.class.isAssignableFrom(type) //
|
||||
? collectionPropertyAsStream(p, propertyAccessor) //
|
||||
: singlePropertyAsStream(p, propertyAccessor);
|
||||
if (Collection.class.isAssignableFrom(type)) {
|
||||
return collectionPropertyAsStream(p, propertyAccessor);
|
||||
}
|
||||
|
||||
if (Map.class.isAssignableFrom(type)) {
|
||||
return mapPropertyAsStream(p, propertyAccessor);
|
||||
}
|
||||
|
||||
return singlePropertyAsStream(p, propertyAccessor);
|
||||
}
|
||||
|
||||
private Stream<Object> collectionPropertyAsStream(JdbcPersistentProperty p,
|
||||
@@ -142,6 +169,15 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
: ((Collection<Object>) property).stream();
|
||||
}
|
||||
|
||||
private Stream<Object> mapPropertyAsStream(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Object property = propertyAccessor.getProperty(p);
|
||||
|
||||
return property == null //
|
||||
? Stream.empty() //
|
||||
: ((Map<Object, Object>) property).entrySet().stream().map(e -> (Object) e);
|
||||
}
|
||||
|
||||
private Stream<Object> singlePropertyAsStream(JdbcPersistentProperty p, PersistentPropertyAccessor propertyAccessor) {
|
||||
|
||||
Object property = propertyAccessor.getProperty(p);
|
||||
@@ -151,4 +187,11 @@ public class JdbcEntityWriter extends JdbcEntityWriterSupport {
|
||||
|
||||
return Stream.of(property);
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class PropertyAndValue {
|
||||
|
||||
private final JdbcPersistentProperty property;
|
||||
private final Object value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper
|
||||
* @see org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty#getColumnName()
|
||||
*/
|
||||
public String getColumnName() {
|
||||
return this.context.getNamingStrategy().getColumnName(this);
|
||||
return context.getNamingStrategy().getColumnName(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +104,12 @@ public class BasicJdbcPersistentProperty extends AnnotationBasedPersistentProper
|
||||
|
||||
@Override
|
||||
public String getReverseColumnName() {
|
||||
return getOwner().getTableName();
|
||||
return context.getNamingStrategy().getReverseColumnName(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyColumn() {
|
||||
return isMap() ? context.getNamingStrategy().getKeyColumn(this) : null;
|
||||
}
|
||||
|
||||
private Class columnTypeIfEntity(Class type) {
|
||||
|
||||
@@ -50,4 +50,14 @@ public class DefaultNamingStrategy implements NamingStrategy {
|
||||
public String getColumnName(JdbcPersistentProperty property) {
|
||||
return property.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReverseColumnName(JdbcPersistentProperty property) {
|
||||
return property.getOwner().getTableName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyColumn(JdbcPersistentProperty property) {
|
||||
return getReverseColumnName(property) + "_key";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,6 @@ public interface JdbcPersistentProperty extends PersistentProperty<JdbcPersisten
|
||||
JdbcPersistentEntity<?> getOwner();
|
||||
|
||||
String getReverseColumnName();
|
||||
|
||||
String getKeyColumn();
|
||||
}
|
||||
|
||||
@@ -30,4 +30,17 @@ public interface NamingStrategy {
|
||||
return this.getSchema() + (this.getSchema().equals("") ? "" : ".") + this.getTableName(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* For a reference A -> B this is the name in the table for B which references A.
|
||||
*
|
||||
* @return a column name.
|
||||
*/
|
||||
String getReverseColumnName(JdbcPersistentProperty property);
|
||||
|
||||
/**
|
||||
* For a map valued reference A -> Map>X,B< this is the name of the column in the tabel for B holding the key of the map.
|
||||
* @return
|
||||
*/
|
||||
String getKeyColumn(JdbcPersistentProperty property);
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -36,7 +37,9 @@ import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.convert.Jsr310Converters;
|
||||
import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
|
||||
import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity;
|
||||
@@ -50,11 +53,14 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class EntityRowMapperUnitTests {
|
||||
|
||||
public static final long ID_FOR_ENTITY_REFERENCING_MAP = 42L;
|
||||
public static final long ID_FOR_ENTITY_NOT_REFERENCING_MAP = 23L;
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void simpleEntitiesGetProperlyExtracted() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name"), //
|
||||
23L, "alpha");
|
||||
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha");
|
||||
rs.next();
|
||||
|
||||
Trivial extracted = createRowMapper(Trivial.class).mapRow(rs, 1);
|
||||
@@ -62,15 +68,14 @@ public class EntityRowMapperUnitTests {
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name) //
|
||||
.containsExactly(23L, "alpha");
|
||||
|
||||
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void simpleOneToOneGetsProperlyExtracted() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name", "child_id", "child_name"), //
|
||||
23L, "alpha", 42L, "beta");
|
||||
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24L, "beta");
|
||||
rs.next();
|
||||
|
||||
OneToOne extracted = createRowMapper(OneToOne.class).mapRow(rs, 1);
|
||||
@@ -78,14 +83,14 @@ public class EntityRowMapperUnitTests {
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name, e -> e.child.id, e -> e.child.name) //
|
||||
.containsExactly(23L, "alpha", 42L, "beta");
|
||||
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 24L, "beta");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-113
|
||||
public void collectionReferenceGetsLoadedWithAdditionalSelect() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name"), //
|
||||
23L, "alpha");
|
||||
ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha");
|
||||
rs.next();
|
||||
|
||||
OneToSet extracted = createRowMapper(OneToSet.class).mapRow(rs, 1);
|
||||
@@ -93,19 +98,45 @@ public class EntityRowMapperUnitTests {
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name, e -> e.children.size()) //
|
||||
.containsExactly(23L, "alpha", 2);
|
||||
.containsExactly(ID_FOR_ENTITY_NOT_REFERENCING_MAP, "alpha", 2);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void mapReferenceGetsLoadedWithAdditionalSelect() throws SQLException {
|
||||
|
||||
ResultSet rs = mockResultSet(asList("id", "name"), //
|
||||
ID_FOR_ENTITY_REFERENCING_MAP, "alpha");
|
||||
rs.next();
|
||||
|
||||
OneToMap extracted = createRowMapper(OneToMap.class).mapRow(rs, 1);
|
||||
|
||||
assertThat(extracted) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id, e -> e.name, e -> e.children.size()) //
|
||||
.containsExactly(ID_FOR_ENTITY_REFERENCING_MAP, "alpha", 2);
|
||||
}
|
||||
|
||||
private <T> EntityRowMapper<T> createRowMapper(Class<T> type) {
|
||||
|
||||
JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy());
|
||||
DataAccessStrategy template = mock(DataAccessStrategy.class);
|
||||
DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class);
|
||||
|
||||
doReturn(new HashSet<>(asList(new Trivial(), new Trivial()))).when(template).findAllByProperty(eq(23L),
|
||||
// the ID of the entity is used to determin what kind of resultset is needed for subsequent selects.
|
||||
doReturn(new HashSet<>(asList(new Trivial(), new Trivial()))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_NOT_REFERENCING_MAP),
|
||||
any(JdbcPersistentProperty.class));
|
||||
|
||||
doReturn(new HashSet<>(asList( //
|
||||
new SimpleEntry("one", new Trivial()), //
|
||||
new SimpleEntry("two", new Trivial()) //
|
||||
))).when(accessStrategy).findAllByProperty(eq(ID_FOR_ENTITY_REFERENCING_MAP), any(JdbcPersistentProperty.class));
|
||||
|
||||
GenericConversionService conversionService = new GenericConversionService();
|
||||
conversionService.addConverter(new IterableOfEntryToMapConverter());
|
||||
DefaultConversionService.addDefaultConverters(conversionService);
|
||||
Jsr310Converters.getConvertersToRegister().forEach(conversionService::addConverter);
|
||||
|
||||
return new EntityRowMapper<>((JdbcPersistentEntity<T>) context.getRequiredPersistentEntity(type),
|
||||
new DefaultConversionService(), context, template);
|
||||
conversionService, context, accessStrategy);
|
||||
}
|
||||
|
||||
private static ResultSet mockResultSet(List<String> columns, Object... values) {
|
||||
@@ -223,4 +254,11 @@ public class EntityRowMapperUnitTests {
|
||||
Set<Trivial> children;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class OneToMap {
|
||||
|
||||
@Id Long id;
|
||||
String name;
|
||||
Map<String, Trivial> children;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static java.util.Collections.*;
|
||||
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link IterableOfEntryToMapConverter}.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
public class IterableOfEntryToMapConverterUnitTests {
|
||||
|
||||
IterableOfEntryToMapConverter converter = new IterableOfEntryToMapConverter();
|
||||
|
||||
@Test
|
||||
public void testConversions() {
|
||||
|
||||
HashMap<Object, Object> map = new HashMap<>();
|
||||
map.put("key", "value");
|
||||
List<Object[]> testValues = asList( //
|
||||
new Object[] { emptySet(), emptyMap() }, //
|
||||
new Object[] { emptyList(), emptyMap() }, //
|
||||
new Object[] { "string", false }, //
|
||||
new Object[] { asList(new SimpleEntry<>("key", "value")), map }, //
|
||||
new Object[] { asList("string"), IllegalArgumentException.class } //
|
||||
);
|
||||
|
||||
SoftAssertions softly = new SoftAssertions();
|
||||
testValues.forEach(array -> softly.assertThat(tryToConvert(array[0])).isEqualTo(array[1]));
|
||||
softly.assertAll();
|
||||
}
|
||||
|
||||
private Object tryToConvert(Object input) {
|
||||
|
||||
try {
|
||||
if (converter.matches( //
|
||||
TypeDescriptor.valueOf(input.getClass()), //
|
||||
TypeDescriptor.valueOf(Map.class)) //
|
||||
) {
|
||||
return converter.convert((Iterable) input);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
return e.getClass();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.jdbc.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.SoftAssertions;
|
||||
@@ -108,6 +109,31 @@ public class SqlGeneratorUnitTests {
|
||||
"DELETE FROM SecondLevelReferencedEntity WHERE ReferencedEntity IN (SELECT x_l1id FROM ReferencedEntity WHERE DummyEntity IS NOT NULL)");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void findAllByProperty() {
|
||||
|
||||
// this would get called when DummyEntity is the element type of a Set
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", null);
|
||||
|
||||
assertThat(sql).isEqualTo("SELECT DummyEntity.id AS id, DummyEntity.name AS name, "
|
||||
+ "ref.l1id AS ref_l1id, ref.content AS ref_content, ref.further AS ref_further "
|
||||
+ "FROM DummyEntity LEFT OUTER JOIN ReferencedEntity AS ref ON ref.DummyEntity = DummyEntity.id "
|
||||
+ "WHERE back-ref = :back-ref");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void findAllByPropertyWithKey() {
|
||||
|
||||
// this would get called when DummyEntity is th element type of a Map
|
||||
String sql = sqlGenerator.getFindAllByProperty("back-ref", "key-column");
|
||||
|
||||
assertThat(sql).isEqualTo("SELECT DummyEntity.id AS id, DummyEntity.name AS name, "
|
||||
+ "ref.l1id AS ref_l1id, ref.content AS ref_content, ref.further AS ref_further, "
|
||||
+ "DummyEntity.key-column AS key-column "
|
||||
+ "FROM DummyEntity LEFT OUTER JOIN ReferencedEntity AS ref ON ref.DummyEntity = DummyEntity.id "
|
||||
+ "WHERE back-ref = :back-ref");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class DummyEntity {
|
||||
|
||||
@@ -115,6 +141,7 @@ public class SqlGeneratorUnitTests {
|
||||
String name;
|
||||
ReferencedEntity ref;
|
||||
Set<Element> elements;
|
||||
Map<Integer, Element> mappedElements;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
|
||||
@@ -19,7 +19,9 @@ import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Test;
|
||||
@@ -41,6 +43,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class JdbcEntityWriterUnitTests {
|
||||
|
||||
public static final long SOME_ENTITY_ID = 23L;
|
||||
JdbcEntityWriter converter = new JdbcEntityWriter(new JdbcMappingContext(new DefaultNamingStrategy()));
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
@@ -62,7 +65,7 @@ public class JdbcEntityWriterUnitTests {
|
||||
@Test // DATAJDBC-112
|
||||
public void existingEntityGetsConvertedToUpdate() {
|
||||
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(23L);
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = //
|
||||
new AggregateChange(Kind.SAVE, SingleReferenceEntity.class, entity);
|
||||
|
||||
@@ -79,7 +82,7 @@ public class JdbcEntityWriterUnitTests {
|
||||
@Test // DATAJDBC-112
|
||||
public void referenceTriggersDeletePlusInsert() {
|
||||
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(23L);
|
||||
SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID);
|
||||
entity.other = new Element(null);
|
||||
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SingleReferenceEntity.class,
|
||||
@@ -158,6 +161,63 @@ public class JdbcEntityWriterUnitTests {
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void newEntityWithEmptyMapResultsInSingleInsert() {
|
||||
|
||||
MapContainer entity = new MapContainer(null);
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, MapContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()).extracting(DbAction::getClass, DbAction::getEntityType) //
|
||||
.containsExactly( //
|
||||
tuple(Insert.class, MapContainer.class));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void newEntityWithMapResultsInAdditionalInsertPerElement() {
|
||||
|
||||
MapContainer entity = new MapContainer(null);
|
||||
entity.elements.put("one", new Element(null));
|
||||
entity.elements.put("two", new Element(null));
|
||||
|
||||
AggregateChange<SingleReferenceEntity> aggregateChange = new AggregateChange(Kind.SAVE, SetContainer.class, entity);
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions())
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType, JdbcEntityWriterUnitTests::getMapKey) //
|
||||
.containsExactlyInAnyOrder( //
|
||||
tuple(Insert.class, MapContainer.class, null), //
|
||||
tuple(Insert.class, Element.class, "one"), //
|
||||
tuple(Insert.class, Element.class, "two") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, MapContainer.class, null), //
|
||||
tuple(Insert.class, Element.class, "two") //
|
||||
).containsSubsequence( // container comes before the elements
|
||||
tuple(Insert.class, MapContainer.class, null), //
|
||||
tuple(Insert.class, Element.class, "one") //
|
||||
);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-112
|
||||
public void mapTriggersDeletePlusInsert() {
|
||||
|
||||
MapContainer entity = new MapContainer(SOME_ENTITY_ID);
|
||||
entity.elements.put("one", new Element(null));
|
||||
|
||||
AggregateChange<MapContainer> aggregateChange = new AggregateChange(Kind.SAVE, MapContainer.class, entity);
|
||||
|
||||
converter.write(entity, aggregateChange);
|
||||
|
||||
assertThat(aggregateChange.getActions()) //
|
||||
.extracting(DbAction::getClass, DbAction::getEntityType, JdbcEntityWriterUnitTests::getMapKey) //
|
||||
.containsExactly( //
|
||||
tuple(Delete.class, Element.class, null), //
|
||||
tuple(Update.class, MapContainer.class, null), //
|
||||
tuple(Insert.class, Element.class, "one") //
|
||||
);
|
||||
}
|
||||
|
||||
private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) {
|
||||
|
||||
CascadingReferenceMiddleElement middleElement1 = new CascadingReferenceMiddleElement(null);
|
||||
@@ -166,6 +226,10 @@ public class JdbcEntityWriterUnitTests {
|
||||
return middleElement1;
|
||||
}
|
||||
|
||||
private static Object getMapKey(DbAction a) {
|
||||
return a.getAdditionalValues().get("MapContainer_key");
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class SingleReferenceEntity {
|
||||
|
||||
@@ -196,6 +260,13 @@ public class JdbcEntityWriterUnitTests {
|
||||
Set<Element> elements = new HashSet<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class MapContainer {
|
||||
|
||||
@Id final Long id;
|
||||
Map<String, Element> elements = new HashMap<>();
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
private static class Element {
|
||||
@Id final Long id;
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
|
||||
import org.springframework.data.jdbc.testing.TestConfiguration;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.rules.SpringClassRule;
|
||||
import org.springframework.test.context.junit4.rules.SpringMethodRule;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Very simple use cases for creation and usage of JdbcRepositories for Entities that contain {@link java.util.Map}s.
|
||||
*
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@Transactional
|
||||
public class JdbcRepositoryWithMapsIntegrationTests {
|
||||
|
||||
@Configuration
|
||||
@Import(TestConfiguration.class)
|
||||
static class Config {
|
||||
|
||||
@Autowired JdbcRepositoryFactory factory;
|
||||
|
||||
@Bean
|
||||
Class<?> testClass() {
|
||||
return JdbcRepositoryWithMapsIntegrationTests.class;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DummyEntityRepository dummyEntityRepository() {
|
||||
return factory.getRepository(DummyEntityRepository.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
|
||||
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
|
||||
|
||||
@Autowired NamedParameterJdbcTemplate template;
|
||||
@Autowired DummyEntityRepository repository;
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void saveAndLoadEmptyMap() {
|
||||
|
||||
DummyEntity entity = repository.save(createDummyEntity());
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull() //
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void saveAndLoadNonEmptyMap() {
|
||||
|
||||
Element element1 = new Element();
|
||||
Element element2 = new Element();
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.put("one", element1);
|
||||
entity.content.put("two", element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content.values()).allMatch(v -> v.id != null);
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
assertThat(reloaded.content.values()) //
|
||||
.isNotNull() //
|
||||
.extracting(e -> e.id) //
|
||||
.containsExactlyInAnyOrder(element1.id, element2.id);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void findAllLoadsMap() {
|
||||
|
||||
Element element1 = new Element();
|
||||
Element element2 = new Element();
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.put("one", element1);
|
||||
entity.content.put("two", element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content.values()).allMatch(v -> v.id != null);
|
||||
|
||||
Iterable<DummyEntity> reloaded = repository.findAll();
|
||||
|
||||
reloaded.forEach(de -> System.out.println("id " + de.id + " content " + de.content.values().iterator().next().content));
|
||||
|
||||
assertThat(reloaded) //
|
||||
.extracting(e -> e.id, e -> e.content.size()) //
|
||||
.containsExactly(tuple(entity.id, entity.content.size()));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void updateMap() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
Element element3 = createElement("three");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.put("one", element1);
|
||||
entity.content.put("two", element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
entity.content.remove("one");
|
||||
element2.content = "two changed";
|
||||
entity.content.put("three", element3);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
assertThat(entity.id).isNotNull();
|
||||
assertThat(entity.content.values()).allMatch(v -> v.id != null);
|
||||
|
||||
DummyEntity reloaded = repository.findById(entity.id).orElseThrow(AssertionFailedError::new);
|
||||
|
||||
// the elements got properly updated and reloaded
|
||||
assertThat(reloaded.content) //
|
||||
.isNotNull();
|
||||
|
||||
assertThat(reloaded.content.entrySet()) //
|
||||
.extracting(e -> e.getKey(), e -> e.getValue().id, e -> e.getValue().content) //
|
||||
.containsExactlyInAnyOrder( //
|
||||
tuple("two", element2.id, "two changed"), //
|
||||
tuple("three", element3.id, "three") //
|
||||
);
|
||||
|
||||
Long count = template.queryForObject("select count(1) from Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-131
|
||||
public void deletingWithMap() {
|
||||
|
||||
Element element1 = createElement("one");
|
||||
Element element2 = createElement("two");
|
||||
|
||||
DummyEntity entity = createDummyEntity();
|
||||
entity.content.put("one", element1);
|
||||
entity.content.put("two", element2);
|
||||
|
||||
entity = repository.save(entity);
|
||||
|
||||
repository.deleteById(entity.id);
|
||||
|
||||
assertThat(repository.findById(entity.id)).isEmpty();
|
||||
|
||||
Long count = template.queryForObject("select count(1) from Element", new HashMap<>(), Long.class);
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
private Element createElement(String content) {
|
||||
|
||||
Element element = new Element();
|
||||
element.content = content;
|
||||
return element;
|
||||
}
|
||||
|
||||
private static DummyEntity createDummyEntity() {
|
||||
|
||||
DummyEntity entity = new DummyEntity();
|
||||
entity.setName("Entity Name");
|
||||
return entity;
|
||||
}
|
||||
|
||||
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {}
|
||||
|
||||
@Data
|
||||
static class DummyEntity {
|
||||
|
||||
@Id private Long id;
|
||||
String name;
|
||||
Map<String, Element> content = new HashMap<>();
|
||||
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static class Element {
|
||||
|
||||
@Id private Long id;
|
||||
String content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE dummyentity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, content VARCHAR(100), DummyEntity_key VARCHAR(100), dummyentity BIGINT);
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE dummyentity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id BIGINT AUTO_INCREMENT PRIMARY KEY, content VARCHAR(100), DummyEntity_key VARCHAR(100),dummyentity BIGINT);
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP TABLE element;
|
||||
DROP TABLE dummyentity;
|
||||
CREATE TABLE dummyentity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
|
||||
CREATE TABLE element (id SERIAL PRIMARY KEY, content VARCHAR(100),dummyentity_key VARCHAR(100), dummyentity BIGINT);
|
||||
Reference in New Issue
Block a user