Skip to content

Commit b385f7f

Browse files
schaudergregturn
authored andcommitted
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.
1 parent 482f330 commit b385f7f

21 files changed

+744
-48
lines changed

src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public interface DataAccessStrategy {
2929

3030
<T> void insert(T instance, Class<T> domainType, Map<String, Object> additionalParameters);
3131

32-
<S> void update(S instance, Class<S> domainType);
32+
<T> void update(T instance, Class<T> domainType);
3333

3434
void delete(Object id, Class<?> domainType);
3535

src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.data.mapping.PropertyHandler;
3838
import org.springframework.data.mapping.PropertyPath;
3939
import org.springframework.data.repository.core.EntityInformation;
40+
import org.springframework.jdbc.core.RowMapper;
4041
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
4142
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
4243
import org.springframework.jdbc.support.GeneratedKeyHolder;
@@ -71,7 +72,6 @@ public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedPar
7172

7273
/**
7374
* Creates a {@link DefaultDataAccessStrategy} which references it self for resolution of recursive data accesses.
74-
*
7575
* Only suitable if this is the only access strategy in use.
7676
*/
7777
public DefaultDataAccessStrategy(SqlGeneratorSource sqlGeneratorSource, NamedParameterJdbcOperations operations,
@@ -149,7 +149,7 @@ public <T> void deleteAll(Class<T> domainType) {
149149
}
150150

151151
@Override
152-
public <T> void deleteAll(PropertyPath propertyPath) {
152+
public void deleteAll(PropertyPath propertyPath) {
153153
operations.getJdbcOperations().update(sql(propertyPath.getOwningType().getType()).createDeleteAllSql(propertyPath));
154154
}
155155

@@ -193,14 +193,18 @@ public <T> Iterable<T> findAllById(Iterable<?> ids, Class<T> domainType) {
193193
}
194194

195195
@Override
196-
public <T> Iterable<T> findAllByProperty(Object rootId, JdbcPersistentProperty property) {
196+
public Iterable findAllByProperty(Object rootId, JdbcPersistentProperty property) {
197197

198198
Class<?> actualType = property.getActualType();
199-
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName());
199+
boolean isMap = property.getTypeInformation().isMap();
200+
String findAllByProperty = sql(actualType).getFindAllByProperty(property.getReverseColumnName(),
201+
property.getKeyColumn());
200202

201203
MapSqlParameterSource parameter = new MapSqlParameterSource(property.getReverseColumnName(), rootId);
202204

203-
return (Iterable<T>) operations.query(findAllByProperty, parameter, getEntityRowMapper(actualType));
205+
return operations.query(findAllByProperty, parameter, isMap //
206+
? getMapEntityRowMapper(property) //
207+
: getEntityRowMapper(actualType));
204208
}
205209

206210
@Override
@@ -287,7 +291,12 @@ private <T> EntityRowMapper<T> getEntityRowMapper(Class<T> domainType) {
287291
return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), conversions, context, accessStrategy);
288292
}
289293

294+
private RowMapper getMapEntityRowMapper(JdbcPersistentProperty property) {
295+
return new MapEntityRowMapper(getEntityRowMapper(property.getActualType()), property.getKeyColumn());
296+
}
297+
290298
private <T> MapSqlParameterSource createIdParameterSource(Object id, Class<T> domainType) {
299+
291300
return new MapSqlParameterSource("id",
292301
convert(id, getRequiredPersistentEntity(domainType).getRequiredIdProperty().getColumnType()));
293302
}
@@ -313,5 +322,4 @@ private <V> V convert(Object from, Class<V> to) {
313322
private SqlGenerator sql(Class<?> domainType) {
314323
return sqlGeneratorSource.getSqlGenerator(domainType);
315324
}
316-
317325
}

src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ public <T> void interpret(DeleteAll<T> delete) {
7777
private <T> Map<String, Object> createAdditionalColumnValues(Insert<T> insert) {
7878

7979
Map<String, Object> additionalColumnValues = new HashMap<>();
80+
addDependingOnInformation(insert, additionalColumnValues);
81+
additionalColumnValues.putAll(insert.getAdditionalValues());
82+
83+
return additionalColumnValues;
84+
}
85+
86+
private <T> void addDependingOnInformation(Insert<T> insert, Map<String, Object> additionalColumnValues) {
8087
DbAction dependingOn = insert.getDependingOn();
8188

8289
if (dependingOn != null) {
@@ -88,7 +95,5 @@ private <T> Map<String, Object> createAdditionalColumnValues(Insert<T> insert) {
8895

8996
additionalColumnValues.put(columnName, identifier);
9097
}
91-
return additionalColumnValues;
9298
}
93-
9499
}

src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.sql.ResultSet;
2121
import java.sql.SQLException;
22+
import java.util.Map;
2223
import java.util.Set;
2324

2425
import org.springframework.core.convert.ConversionService;
@@ -80,6 +81,11 @@ public T mapRow(ResultSet resultSet, int rowNumber) throws SQLException {
8081

8182
if (Set.class.isAssignableFrom(property.getType())) {
8283
propertyAccessor.setProperty(property, accessStrategy.findAllByProperty(id, property));
84+
} else if (Map.class.isAssignableFrom(property.getType())) {
85+
86+
Iterable<Object> allByProperty = accessStrategy.findAllByProperty(id, property);
87+
IterableOfEntryToMapConverter converter = new IterableOfEntryToMapConverter();
88+
propertyAccessor.setProperty(property, converter.convert(allByProperty));
8389
} else {
8490
propertyAccessor.setProperty(property, readFrom(resultSet, property, ""));
8591
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.Map.Entry;
21+
22+
import org.springframework.core.convert.TypeDescriptor;
23+
import org.springframework.core.convert.converter.ConditionalConverter;
24+
import org.springframework.core.convert.converter.Converter;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* A converter for creating a {@link Map} from an {@link Iterable<Map.Entry>}.
30+
*
31+
* @author Jens Schauder
32+
*/
33+
class IterableOfEntryToMapConverter implements ConditionalConverter, Converter<Iterable, Map> {
34+
35+
@Nullable
36+
@Override
37+
public Map convert(Iterable source) {
38+
39+
HashMap result = new HashMap();
40+
41+
source.forEach(element -> {
42+
43+
if (!(element instanceof Entry)) {
44+
throw new IllegalArgumentException(String.format("Cannot convert %s to Map.Entry", element.getClass()));
45+
}
46+
47+
Entry entry = (Entry) element;
48+
result.put(entry.getKey(), entry.getValue());
49+
});
50+
51+
return result;
52+
}
53+
54+
/**
55+
* Tries to determine if the {@literal sourceType} can be converted to a {@link Map}. If this can not be determined,
56+
* because the sourceTyppe does not contain information about the element type it returns {@literal true}.
57+
*
58+
* @param sourceType {@link TypeDescriptor} to convert from.
59+
* @param targetType {@link TypeDescriptor} to convert to.
60+
* @return
61+
*/
62+
@Override
63+
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
64+
65+
Assert.notNull(sourceType, "Source type must not be null.");
66+
Assert.notNull(targetType, "Target type must not be null.");
67+
68+
if (!sourceType.isAssignableTo(TypeDescriptor.valueOf(Iterable.class)))
69+
return false;
70+
71+
TypeDescriptor elementDescriptor = sourceType.getElementTypeDescriptor();
72+
return elementDescriptor == null || elementDescriptor.isAssignableTo(TypeDescriptor.valueOf(Entry.class));
73+
}
74+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core;
17+
18+
import java.sql.ResultSet;
19+
import java.sql.SQLException;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import org.springframework.jdbc.core.RowMapper;
24+
import org.springframework.lang.Nullable;
25+
26+
/**
27+
* A {@link RowMapper} that maps a row to a {@link Map.Entry} so an {@link Iterable} of those can be converted to a
28+
* {@link Map} using an {@link IterableOfEntryToMapConverter}. Creation of the {@literal value} part of the resulting
29+
* {@link Map.Entry} is delegated to a {@link RowMapper} provided in the constructor.
30+
*
31+
* @author Jens Schauder
32+
*/
33+
class MapEntityRowMapper<T> implements RowMapper<Map.Entry<Object, T>> {
34+
35+
private final RowMapper<T> delegate;
36+
private final String keyColumn;
37+
38+
MapEntityRowMapper(RowMapper<T> delegate, String keyColumn) {
39+
40+
this.delegate = delegate;
41+
this.keyColumn = keyColumn;
42+
}
43+
44+
@Nullable
45+
@Override
46+
public Map.Entry<Object, T> mapRow(ResultSet rs, int rowNum) throws SQLException {
47+
return new HashMap.SimpleEntry<>(rs.getObject(keyColumn), delegate.mapRow(rs, rowNum));
48+
}
49+
}

src/main/java/org/springframework/data/jdbc/core/SqlGenerator.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.ArrayList;
1919
import java.util.Collection;
2020
import java.util.List;
21+
import java.util.Map;
2122
import java.util.Set;
2223
import java.util.stream.Collectors;
2324
import java.util.stream.Stream;
@@ -88,8 +89,24 @@ String getFindAll() {
8889
return findAllSql.get();
8990
}
9091

91-
String getFindAllByProperty(String columnName) {
92-
return String.format("%s WHERE %s = :%s", findAllSql.get(), columnName, columnName);
92+
/**
93+
* Returns a query for selecting all simple properties of an entity, including those for one-to-one relationships.
94+
* Results are limited to those rows referencing some other entity using the column specified by
95+
* {@literal columnName}. This is used to select values for a complex property ({@link Set}, {@link Map} ...) based on
96+
* a referencing entity.
97+
*
98+
* @param columnName name of the column of the FK back to the referencing entity.
99+
* @param keyColumn if the property is of type {@link Map} this column contains the map key.
100+
* @return a SQL String.
101+
*/
102+
String getFindAllByProperty(String columnName, String keyColumn) {
103+
104+
String baseSelect = (keyColumn != null) //
105+
? createSelectBuilder().column(cb -> cb.tableAlias(entity.getTableName()).column(keyColumn).as(keyColumn))
106+
.build()
107+
: getFindAll();
108+
109+
return String.format("%s WHERE %s = :%s", baseSelect, columnName, columnName);
93110
}
94111

95112
String getExists() {
@@ -136,10 +153,19 @@ private SelectBuilder createSelectBuilder() {
136153
return builder;
137154
}
138155

156+
/**
157+
* Adds the columns to the provided {@link SelectBuilder} representing simplem properties, including those from
158+
* one-to-one relationships.
159+
*
160+
* @param builder The {@link SelectBuilder} to be modified.
161+
*/
139162
private void addColumnsAndJoinsForOneToOneReferences(SelectBuilder builder) {
140163

141164
for (JdbcPersistentProperty property : entity) {
142-
if (!property.isEntity() || Collection.class.isAssignableFrom(property.getType())) {
165+
if (!property.isEntity() //
166+
|| Collection.class.isAssignableFrom(property.getType()) //
167+
|| Map.class.isAssignableFrom(property.getType()) //
168+
) {
143169
continue;
144170
}
145171

src/main/java/org/springframework/data/jdbc/core/conversion/DbAction.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import lombok.Getter;
1919
import lombok.ToString;
2020

21+
import java.util.HashMap;
22+
import java.util.Map;
23+
2124
import org.springframework.data.mapping.PropertyPath;
2225
import org.springframework.util.Assert;
2326

@@ -40,6 +43,13 @@ public abstract class DbAction<T> {
4043
*/
4144
private final T entity;
4245

46+
/**
47+
* Key-value-pairs to specify additional values to be used with the statement which can't be obtained from the entity,
48+
* nor from {@link DbAction}s {@literal this} depends on. A used case are map keys, which need to be persisted with
49+
* the map value but aren't part of the value.
50+
*/
51+
private final Map<String, Object> additionalValues = new HashMap<>();
52+
4353
/**
4454
* Another action, this action depends on. For example the insert for one entity might need the id of another entity,
4555
* which gets insert before this one. That action would be referenced by this property, so that the id becomes

0 commit comments

Comments
 (0)