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:
Jens Schauder
2017-10-09 07:55:58 +02:00
committed by Greg Turnquist
parent 482f330f02
commit b385f7f24f
21 changed files with 744 additions and 48 deletions

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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, ""));
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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";
}
}

View File

@@ -44,4 +44,6 @@ public interface JdbcPersistentProperty extends PersistentProperty<JdbcPersisten
JdbcPersistentEntity<?> getOwner();
String getReverseColumnName();
String getKeyColumn();
}

View File

@@ -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&gt;X,B&lt; this is the name of the column in the tabel for B holding the key of the map.
* @return
*/
String getKeyColumn(JdbcPersistentProperty property);
}