Skip to content

Commit 687236b

Browse files
GH-2280 - Allow custom composite converters to delete decomposed properties.
Custom composite converters must be notified when a property to be decomposed is `null`. This means we must remove the `nullSafeWrite` decorator for all composite properties first. To be compatible with existing converters that trust us not to get a `null`, we must check for any NPE raising from such a converter. A custom `Neo4jPersistentPropertyToMapConverter` needs to set _all_ properties it normally writes when decomposing a property to `Values.NULL`, so that they are removed from the node or relationship in question. This fixes #2280.
1 parent ce65892 commit 687236b

File tree

8 files changed

+65
-10
lines changed

8 files changed

+65
-10
lines changed

src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyConverter.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import org.neo4j.driver.Value;
2020

2121
/**
22-
* This interface represents a pair of methods capable of converting values of type {@code T} to and from {@link Value values}.
22+
* This interface represents a pair of methods capable of converting values of type {@code T} to and from {@link Value values}.
2323
*
2424
* @param <T> The type of the property to convert (the type of the actual attribute).
2525
* @author Michael J. Simons

src/main/java/org/springframework/data/neo4j/core/convert/Neo4jPersistentPropertyToMapConverter.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.apiguardian.api.API;
2121
import org.neo4j.driver.Value;
22+
import org.springframework.lang.Nullable;
2223

2324
/**
2425
* You need to provide an implementation of this interface in case you want to store a property of an entity as separate
@@ -43,7 +44,7 @@ public interface Neo4jPersistentPropertyToMapConverter<K, P> {
4344
* @param neo4jConversionService The conversion service to delegate to if necessary
4445
* @return The decomposed object.
4546
*/
46-
Map<K, Value> decompose(P property, Neo4jConversionService neo4jConversionService);
47+
Map<K, Value> decompose(@Nullable P property, Neo4jConversionService neo4jConversionService);
4748

4849
/**
4950
* Composes the object back from the map. The map contains the raw driver values, as SDN cannot know how you want to

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jConversionService.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,20 @@ public Value writeValue(@Nullable Object value, TypeInformation<?> sourceType,
103103

104104
Function<Object, Value> conversion =
105105
writingConverter == null ? v -> conversionService.convert(v, Value.class) : writingConverter;
106+
106107
return writeValueImpl(value, sourceType, conversion);
107108
}
108109

109110
private Value writeValueImpl(@Nullable Object value, TypeInformation<?> type,
110111
Function<Object, Value> conversion) {
111112

112113
if (value == null) {
113-
return Values.NULL;
114+
try {
115+
// Some conversion services may treat null special, so we pass it anyway and ask for forgiveness
116+
return conversion.apply(null);
117+
} catch (NullPointerException e) {
118+
return Values.NULL;
119+
}
114120
}
115121

116122
if (isCollection(type)) {

src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentProperty.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,12 @@ private static Function<Object, Value> nullSafeWrite(Function<Object, Value> del
215215

216216
@Override
217217
public Function<Object, Value> getOptionalWritingConverter() {
218-
return customConversion.getOptional().map(c -> nullSafeWrite(c::write)).orElse(null);
218+
return customConversion.getOptional()
219+
.map(c -> {
220+
Function<Object, Value> originalConversion = c::write;
221+
return this.isComposite() ? originalConversion : nullSafeWrite(originalConversion);
222+
})
223+
.orElse(null);
219224
}
220225

221226
private static Function<Value, Object> nullSafeRead(Function<Value, Object> delegate) {

src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.data.mapping.PersistentProperty;
2424
import org.springframework.data.neo4j.core.schema.CompositeProperty;
2525
import org.springframework.data.neo4j.core.schema.DynamicLabels;
26+
import org.springframework.lang.Nullable;
2627

2728
/**
2829
* A {@link org.springframework.data.mapping.PersistentProperty} interface with additional methods for metadata related
@@ -67,8 +68,10 @@ default boolean isDynamicLabels() {
6768
return this.isAnnotationPresent(DynamicLabels.class) && this.isCollectionLike();
6869
}
6970

71+
@Nullable
7072
Function<Object, Value> getOptionalWritingConverter();
7173

74+
@Nullable
7275
Function<Value, Object> getOptionalReadingConverter();
7376

7477
/**

src/main/java/org/springframework/data/neo4j/core/schema/CompositeProperty.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323
import java.lang.reflect.Type;
24+
import java.util.Collections;
2425
import java.util.HashMap;
2526
import java.util.Map;
2627
import java.util.function.BiFunction;
@@ -128,7 +129,12 @@ final class DefaultToMapConverter<K> implements Neo4jPersistentPropertyToMapConv
128129
}
129130

130131
@Override
131-
public Map<K, Value> decompose(Map<K, Object> property, Neo4jConversionService conversionService) {
132+
public Map<K, Value> decompose(@Nullable Map<K, Object> property, Neo4jConversionService conversionService) {
133+
134+
if (property == null) {
135+
return Collections.emptyMap();
136+
}
137+
132138
Map<K, Value> decomposed = new HashMap<>(property.size());
133139
property.forEach(
134140
(k, v) -> decomposed.put(k, conversionService.writeValue(v, typeInformationForValues, null)));

src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/ImperativeCompositePropertiesIT.java

+25
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
import org.junit.jupiter.api.Test;
2121
import org.neo4j.driver.Driver;
22+
import org.neo4j.driver.Record;
23+
import org.neo4j.driver.Session;
24+
import org.neo4j.driver.types.Node;
2225
import org.springframework.beans.factory.annotation.Autowired;
2326
import org.springframework.context.annotation.Bean;
2427
import org.springframework.context.annotation.Configuration;
@@ -81,6 +84,28 @@ void compositePropertiesOnNodesShouldBeRead(@Autowired Repository repository) {
8184
assertThat(repository.findById(id)).isPresent().hasValueSatisfying(this::assertNodePropertiesOn);
8285
}
8386

87+
@Test // GH-2280
88+
void compositePropertiesOnNodesShouldBeDeleted(@Autowired Repository repository) {
89+
90+
Long id = createNodeWithCompositeProperties();
91+
ThingWithCompositeProperties thing = repository.findById(id).get();
92+
thing.setDatesWithTransformedKey(Collections.singletonMap("Test", null));
93+
thing.setSomeDatesByEnumA(Collections.singletonMap(ThingWithCompositeProperties.EnumA.VALUE_AA, null));
94+
thing.setSomeOtherDTO(null);
95+
repository.save(thing);
96+
97+
try (Session session = driver.session()) {
98+
Record r = session.readTransaction(tx -> tx.run("MATCH (t:CompositeProperties) WHERE id(t) = $id RETURN t",
99+
Collections.singletonMap("id", id)).single());
100+
Node n = r.get("t").asNode();
101+
assertThat(n.asMap()).doesNotContainKeys(
102+
"someDatesByEnumA.VALUE_AA",
103+
"datesWithTransformedKey.test",
104+
"dto.x", "dto.y", "dto.z"
105+
);
106+
}
107+
}
108+
84109
public interface Repository extends Neo4jRepository<ThingWithCompositeProperties, Long> {
85110
}
86111

src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithCompositeProperties.java

+14-5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.data.neo4j.core.schema.Id;
3232
import org.springframework.data.neo4j.core.schema.Node;
3333
import org.springframework.data.neo4j.core.schema.Relationship;
34+
import org.springframework.lang.NonNull;
35+
import org.springframework.lang.Nullable;
3436

3537
/**
3638
* Test class verifying composite properties behaviour.
@@ -235,12 +237,19 @@ public String apply(CompositeProperty.Phase phase, String s) {
235237
*/
236238
static class SomeOtherDTOToMapConverter implements Neo4jPersistentPropertyToMapConverter<String, SomeOtherDTO> {
237239

238-
@Override
239-
public Map<String, Value> decompose(SomeOtherDTO property, Neo4jConversionService conversionService) {
240+
@NonNull @Override
241+
public Map<String, Value> decompose(@Nullable SomeOtherDTO property, Neo4jConversionService conversionService) {
242+
240243
final HashMap<String, Value> decomposed = new HashMap<>();
241-
decomposed.put("x", Values.value(property.x));
242-
decomposed.put("y", Values.value(property.y));
243-
decomposed.put("z", Values.value(property.z));
244+
if (property == null) {
245+
decomposed.put("x", Values.NULL);
246+
decomposed.put("y", Values.NULL);
247+
decomposed.put("z", Values.NULL);
248+
} else {
249+
decomposed.put("x", Values.value(property.x));
250+
decomposed.put("y", Values.value(property.y));
251+
decomposed.put("z", Values.value(property.z));
252+
}
244253
return decomposed;
245254
}
246255

0 commit comments

Comments
 (0)