Skip to content

Commit 66e691f

Browse files
GH-2348 - Use default values for primitive attributes when Graph properties are unset.
This changes the behaviour in regards of default values for primitives attributes of domain objects: They have well known default values on the JVM and we should use them, even if the property is non-existent in the graph (materializing as literal null). People migrating from SDN+OGM rely on the behaviour. This change does not apply to attributes assigned via constructor arguments. Constructor argument don't have default values (in Java) and we try to avoid guessing as much as possible. In addition, it would also clash with actually possible default values in Kotlin data classes. The change cannot be done in `DefaultNeo4jConversionService` as originally planned, as the conversion service is of course unaware about the target of a converted value. This fixes #2348.
1 parent 96165a8 commit 66e691f

File tree

5 files changed

+145
-84
lines changed

5 files changed

+145
-84
lines changed

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

+16-6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.neo4j.driver.types.Type;
4444
import org.neo4j.driver.types.TypeSystem;
4545
import org.springframework.core.CollectionFactory;
46+
import org.springframework.core.KotlinDetector;
4647
import org.springframework.data.annotation.ReadOnlyProperty;
4748
import org.springframework.data.mapping.AssociationHandler;
4849
import org.springframework.data.mapping.MappingException;
@@ -53,6 +54,7 @@
5354
import org.springframework.data.mapping.model.ParameterValueProvider;
5455
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
5556
import org.springframework.data.neo4j.core.schema.TargetNode;
57+
import org.springframework.data.util.ReflectionUtils;
5658
import org.springframework.data.util.TypeInformation;
5759
import org.springframework.lang.NonNull;
5860
import org.springframework.lang.Nullable;
@@ -108,7 +110,7 @@ public <R> R read(Class<R> targetType, MapAccessor mapAccessor) {
108110
try {
109111
return map(queryRoot, queryRoot, rootNodeDescription);
110112
} catch (Exception e) {
111-
throw new MappingException("Error mapping " + mapAccessor.toString(), e);
113+
throw new MappingException("Error mapping " + mapAccessor, e);
112114
}
113115
}
114116

@@ -270,6 +272,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
270272
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
271273
.getNodeDescription();
272274

275+
boolean isKotlinType = KotlinDetector.isKotlinType(concreteNodeDescription.getType());
273276
ET instance = instantiate(concreteNodeDescription, queryResult,
274277
nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, relationshipsFromResult, nodesFromResult);
275278

@@ -282,7 +285,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
282285
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
283286
.getPersistenceConstructor()::isConstructorParameter;
284287
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
285-
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity);
288+
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, isKotlinType);
286289
concreteNodeDescription.doWithProperties(handler);
287290

288291
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
@@ -396,26 +399,33 @@ public Object getParameterValue(PreferredConstructor.Parameter parameter) {
396399

397400
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
398401
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
399-
Collection<String> surplusLabels, Object targetNode) {
402+
Collection<String> surplusLabels, Object targetNode, boolean ownerIsKotlinType) {
400403
return property -> {
401404
if (isConstructorParameter.test(property)) {
402405
return;
403406
}
404407

408+
TypeInformation<?> typeInformation = property.getTypeInformation();
405409
if (property.isDynamicLabels()) {
406410
propertyAccessor.setProperty(property,
407-
createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels));
411+
createDynamicLabelsProperty(typeInformation, surplusLabels));
408412
} else if (property.isAnnotationPresent(TargetNode.class)) {
409413
if (queryResult instanceof Relationship) {
410414
propertyAccessor.setProperty(property, targetNode);
411415
}
412416
} else {
413-
propertyAccessor.setProperty(property,
414-
conversionService.readValue(extractValueOf(property, queryResult), property.getTypeInformation(), property.getOptionalReadingConverter()));
417+
Object value = conversionService.readValue(extractValueOf(property, queryResult), typeInformation, property.getOptionalReadingConverter());
418+
Class<?> rawType = typeInformation.getType();
419+
propertyAccessor.setProperty(property, getValueOrDefault(ownerIsKotlinType, rawType, value));
415420
}
416421
};
417422
}
418423

424+
private static Object getValueOrDefault(boolean ownerIsKotlinType, Class<?> rawType, @Nullable Object value) {
425+
426+
return value == null && !ownerIsKotlinType && rawType.isPrimitive() ? ReflectionUtils.getPrimitiveDefault(rawType) : value;
427+
}
428+
419429
private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
420430
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter, Collection<Relationship> relationshipsFromResult, Collection<Node> nodesFromResult) {
421431
return association -> {

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

+52-35
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2020

21-
import java.lang.reflect.Field;
2221
import java.text.SimpleDateFormat;
2322
import java.time.ZoneId;
2423
import java.time.ZonedDateTime;
@@ -41,7 +40,6 @@
4140
import org.junit.jupiter.api.DynamicTest;
4241
import org.junit.jupiter.api.Test;
4342
import org.junit.jupiter.api.TestFactory;
44-
import org.junit.platform.commons.util.AnnotationUtils;
4543
import org.neo4j.driver.Driver;
4644
import org.neo4j.driver.Session;
4745
import org.neo4j.driver.Value;
@@ -54,17 +52,16 @@
5452
import org.springframework.data.mapping.MappingException;
5553
import org.springframework.data.neo4j.config.AbstractNeo4jConfig;
5654
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
57-
import org.springframework.data.neo4j.core.convert.ConvertWith;
55+
import org.springframework.data.neo4j.core.Neo4jTemplate;
5856
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
5957
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
6058
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
59+
import org.springframework.data.neo4j.integration.shared.common.ThingWithAllCypherTypes2;
6160
import org.springframework.data.neo4j.integration.shared.conversion.Neo4jConversionsITBase;
6261
import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes;
6362
import org.springframework.data.neo4j.integration.shared.common.ThingWithAllCypherTypes;
6463
import org.springframework.data.neo4j.integration.shared.common.ThingWithAllSpatialTypes;
65-
import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCompositeProperties;
6664
import org.springframework.data.neo4j.integration.shared.conversion.ThingWithCustomTypes;
67-
import org.springframework.data.neo4j.integration.shared.common.ThingWithNonExistingPrimitives;
6865
import org.springframework.data.neo4j.integration.shared.common.ThingWithUUIDID;
6966
import org.springframework.data.neo4j.repository.Neo4jRepository;
7067
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
@@ -73,7 +70,6 @@
7370
import org.springframework.test.util.ReflectionTestUtils;
7471
import org.springframework.transaction.PlatformTransactionManager;
7572
import org.springframework.transaction.annotation.EnableTransactionManagement;
76-
import org.springframework.util.ReflectionUtils;
7773

7874
/**
7975
* @author Michael J. Simons
@@ -83,42 +79,44 @@
8379
@Neo4jIntegrationTest
8480
class TypeConversionIT extends Neo4jConversionsITBase {
8581

86-
private final Driver driver;
87-
88-
@Autowired CypherTypesRepository cypherTypesRepository;
82+
private final CypherTypesRepository cypherTypesRepository;
8983

9084
private final AdditionalTypesRepository additionalTypesRepository;
9185

9286
private final SpatialTypesRepository spatialTypesRepository;
9387

9488
private final CustomTypesRepository customTypesRepository;
95-
private final BookmarkCapture bookmarkCapture;
9689

9790
private final DefaultConversionService defaultConversionService;
9891

99-
@Autowired TypeConversionIT(Driver driver, CypherTypesRepository cypherTypesRepository,
92+
@Autowired TypeConversionIT(CypherTypesRepository cypherTypesRepository,
10093
AdditionalTypesRepository additionalTypesRepository, SpatialTypesRepository spatialTypesRepository,
10194
CustomTypesRepository customTypesRepository,
102-
Neo4jConversions neo4jConversions, BookmarkCapture bookmarkCapture) {
103-
this.driver = driver;
95+
Neo4jConversions neo4jConversions) {
10496
this.cypherTypesRepository = cypherTypesRepository;
10597
this.additionalTypesRepository = additionalTypesRepository;
10698
this.spatialTypesRepository = spatialTypesRepository;
10799
this.customTypesRepository = customTypesRepository;
108-
this.bookmarkCapture = bookmarkCapture;
109100
this.defaultConversionService = new DefaultConversionService();
110101
neo4jConversions.registerConvertersIn(defaultConversionService);
111102
}
112103

113104
@Test
114-
void thereShallBeNoDefaultValuesForNonExistingAttributes(@Autowired NonExistingPrimitivesRepository repository) {
105+
void thereShallBeNoDefaultValuesForNonExistingAttributes() {
106+
107+
Long id;
108+
try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) {
109+
110+
id = session.writeTransaction(tx -> tx.run("CREATE (n:CypherTypes) RETURN id(n)").single().get(0).asLong());
111+
bookmarkCapture.seedWith(session.lastBookmark());
112+
}
115113

116114
assertThatExceptionOfType(MappingException.class)
117-
.isThrownBy(() -> repository.findById(ID_OF_NON_EXISTING_PRIMITIVES_NODE))
115+
.isThrownBy(() -> cypherTypesRepository.findById(id))
118116
.withMessageMatching(
119-
"Error mapping Record<\\{n: \\{__internalNeo4jId__: \\d+, id: NULL, someBoolean: NULL, __nodeLabels__: \\[\"NonExistingPrimitives\"\\]\\}\\}>")
120-
.withStackTraceContaining("unboxBoolean")
121-
.withRootCauseInstanceOf(NullPointerException.class);
117+
"Error mapping Record<\\{n: \\{__internalNeo4jId__: \\d+, aBoolean: NULL, aString: NULL, aLong: NULL, anOffsetTime: NULL, aLocalDateTime: NULL, aDouble: NULL, aByteArray: NULL, aPoint: NULL, aZeroDuration: NULL, aZoneDateTime: NULL, __nodeLabels__: \\[\"CypherTypes\"], aLocalDate: NULL, aZeroPeriod: NULL, anIsoDuration: NULL, aLocalTime: NULL, id: NULL}}>")
118+
.withStackTraceContaining("Illegal arguments for constructor")
119+
.withRootCauseInstanceOf(IllegalArgumentException.class);
122120
}
123121

124122
@TestFactory
@@ -165,10 +163,10 @@ Stream<DynamicNode> conversionsShouldBeAppliedToEntities() {
165163
() -> assertThat(ReflectionTestUtils.getField(thing, a.getKey()))
166164
.isEqualTo(a.getValue()))));
167165

168-
DynamicContainer writes = DynamicContainer.dynamicContainer("write", entry.getValue().entrySet().stream()
169-
.map(a -> DynamicTest
170-
.dynamicTest(a.getKey(),
171-
() -> assertWrite(copyOfThing, a.getKey(), defaultConversionService))));
166+
DynamicContainer writes = DynamicContainer.dynamicContainer("write", entry.getValue().keySet().stream()
167+
.map(o -> DynamicTest
168+
.dynamicTest(o,
169+
() -> assertWrite(copyOfThing, o, defaultConversionService))));
172170

173171
return DynamicContainer.dynamicContainer(entry.getKey(), Arrays.asList(reads, writes));
174172
});
@@ -179,8 +177,6 @@ void assertWrite(Object thing, String fieldName, ConversionService conversionSer
179177
long id = (long) ReflectionTestUtils.getField(thing, "id");
180178
Object domainValue = ReflectionTestUtils.getField(thing, fieldName);
181179

182-
Field field = ReflectionUtils.findField(thing.getClass(), fieldName);
183-
Optional<ConvertWith> annotation = AnnotationUtils.findAnnotation(field, ConvertWith.class);
184180
Function<Object, Value> conversion;
185181
if (fieldName.equals("dateAsLong")) {
186182
conversion = o -> Values.value(((Date) o).getTime());
@@ -192,8 +188,7 @@ void assertWrite(Object thing, String fieldName, ConversionService conversionSer
192188
Value driverValue;
193189
if (domainValue != null && Collection.class.isAssignableFrom(domainValue.getClass())) {
194190
Collection<?> sourceCollection = (Collection<?>) domainValue;
195-
Object[] targetCollection = (sourceCollection).stream()
196-
.map(element -> conversion.apply(element)).toArray();
191+
Object[] targetCollection = (sourceCollection).stream().map(conversion).toArray();
197192
driverValue = Values.value(targetCollection);
198193
} else {
199194
driverValue = conversion.apply(domainValue);
@@ -243,6 +238,35 @@ void parametersTargetingConvertedAttributesMustBeConverted(@Autowired CustomType
243238
.hasSizeGreaterThan(0);
244239
}
245240

241+
@Test // GH-2348
242+
void nonExistingPrimitivesShouldNotFailWithFieldAccess(@Autowired Neo4jTemplate template) {
243+
Long id;
244+
try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) {
245+
246+
id = session.writeTransaction(tx -> tx.run("CREATE (n:ThingWithAllCypherTypes2) RETURN id(n)").single().get(0).asLong());
247+
bookmarkCapture.seedWith(session.lastBookmark());
248+
}
249+
250+
Optional<ThingWithAllCypherTypes2> optionalResult = template.findById(id, ThingWithAllCypherTypes2.class);
251+
assertThat(optionalResult).hasValueSatisfying(result -> {
252+
assertThat(result.isABoolean()).isFalse();
253+
assertThat(result.getALong()).isEqualTo(0L);
254+
assertThat(result.getAnInt()).isEqualTo(0);
255+
assertThat(result.getADouble()).isEqualTo(0.0);
256+
assertThat(result.getAString()).isNull();
257+
assertThat(result.getAByteArray()).isNull();
258+
assertThat(result.getALocalDate()).isNull();
259+
assertThat(result.getAnOffsetTime()).isNull();
260+
assertThat(result.getALocalTime()).isNull();
261+
assertThat(result.getAZoneDateTime()).isNull();
262+
assertThat(result.getALocalDateTime()).isNull();
263+
assertThat(result.getAnIsoDuration()).isNull();
264+
assertThat(result.getAPoint()).isNull();
265+
assertThat(result.getAZeroPeriod()).isNull();
266+
assertThat(result.getAZeroDuration()).isNull();
267+
});
268+
}
269+
246270
public interface ConvertedIDsRepository extends Neo4jRepository<ThingWithUUIDID, UUID> {
247271
}
248272

@@ -255,18 +279,11 @@ public interface AdditionalTypesRepository extends Neo4jRepository<ThingWithAllA
255279
public interface SpatialTypesRepository extends Neo4jRepository<ThingWithAllSpatialTypes, Long> {
256280
}
257281

258-
public interface NonExistingPrimitivesRepository extends Neo4jRepository<ThingWithNonExistingPrimitives, Long> {
259-
}
260-
261282
public interface CustomTypesRepository extends Neo4jRepository<ThingWithCustomTypes, Long> {
262283

263284
List<ThingWithCustomTypes> findAllByDateAsString(Date theDate);
264285
}
265286

266-
public interface ThingWithCompositePropertiesRepository
267-
extends Neo4jRepository<ThingWithCompositeProperties, Long> {
268-
}
269-
270287
@Configuration
271288
@EnableNeo4jRepositories(considerNestedRepositories = true)
272289
@EnableTransactionManagement
@@ -284,7 +301,7 @@ public Neo4jConversions neo4jConversions() {
284301

285302
@Bean
286303
public BookmarkCapture bookmarkCapture() {
287-
return new BookmarkCapture();
304+
return Neo4jConversionsITBase.bookmarkCapture;
288305
}
289306

290307
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2011-2021 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+
* https://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.neo4j.integration.shared.common;
17+
18+
import lombok.Getter;
19+
import lombok.RequiredArgsConstructor;
20+
import lombok.Setter;
21+
22+
import java.time.Duration;
23+
import java.time.LocalDate;
24+
import java.time.LocalDateTime;
25+
import java.time.LocalTime;
26+
import java.time.OffsetTime;
27+
import java.time.Period;
28+
import java.time.ZonedDateTime;
29+
30+
import org.neo4j.driver.types.IsoDuration;
31+
import org.neo4j.driver.types.Point;
32+
import org.springframework.data.neo4j.core.schema.GeneratedValue;
33+
import org.springframework.data.neo4j.core.schema.Id;
34+
import org.springframework.data.neo4j.core.schema.Node;
35+
36+
/**
37+
* Similar to {@link ThingWithAllCypherTypes} but using field access.
38+
* @author Michael J. Simons
39+
*/
40+
@Node
41+
@Getter
42+
@Setter
43+
@RequiredArgsConstructor
44+
public class ThingWithAllCypherTypes2 {
45+
46+
@Id @GeneratedValue private final Long id;
47+
48+
private boolean aBoolean;
49+
50+
private long aLong;
51+
52+
private int anInt;
53+
54+
private double aDouble;
55+
56+
private String aString;
57+
58+
private byte[] aByteArray;
59+
60+
private LocalDate aLocalDate;
61+
62+
private OffsetTime anOffsetTime;
63+
64+
private LocalTime aLocalTime;
65+
66+
private ZonedDateTime aZoneDateTime;
67+
68+
private LocalDateTime aLocalDateTime;
69+
70+
private IsoDuration anIsoDuration;
71+
72+
private Point aPoint;
73+
74+
private Period aZeroPeriod;
75+
76+
private Duration aZeroDuration;
77+
}

src/test/java/org/springframework/data/neo4j/integration/shared/common/ThingWithNonExistingPrimitives.java

-39
This file was deleted.

0 commit comments

Comments
 (0)