Skip to content

Commit 41b90aa

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 8d551f8 commit 41b90aa

File tree

5 files changed

+144
-80
lines changed

5 files changed

+144
-80
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

+51-31
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,39 +79,44 @@
8379
@Neo4jIntegrationTest
8480
class TypeConversionIT extends Neo4jConversionsITBase {
8581

86-
@Autowired CypherTypesRepository cypherTypesRepository;
82+
private final CypherTypesRepository cypherTypesRepository;
8783

8884
private final AdditionalTypesRepository additionalTypesRepository;
8985

9086
private final SpatialTypesRepository spatialTypesRepository;
9187

9288
private final CustomTypesRepository customTypesRepository;
93-
private final BookmarkCapture bookmarkCapture;
9489

9590
private final DefaultConversionService defaultConversionService;
9691

9792
@Autowired TypeConversionIT(CypherTypesRepository cypherTypesRepository,
9893
AdditionalTypesRepository additionalTypesRepository, SpatialTypesRepository spatialTypesRepository,
9994
CustomTypesRepository customTypesRepository,
100-
Neo4jConversions neo4jConversions, BookmarkCapture bookmarkCapture) {
95+
Neo4jConversions neo4jConversions) {
10196
this.cypherTypesRepository = cypherTypesRepository;
10297
this.additionalTypesRepository = additionalTypesRepository;
10398
this.spatialTypesRepository = spatialTypesRepository;
10499
this.customTypesRepository = customTypesRepository;
105-
this.bookmarkCapture = bookmarkCapture;
106100
this.defaultConversionService = new DefaultConversionService();
107101
neo4jConversions.registerConvertersIn(defaultConversionService);
108102
}
109103

110104
@Test
111-
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+
}
112113

113114
assertThatExceptionOfType(MappingException.class)
114-
.isThrownBy(() -> repository.findById(ID_OF_NON_EXISTING_PRIMITIVES_NODE))
115+
.isThrownBy(() -> cypherTypesRepository.findById(id))
115116
.withMessageMatching(
116-
"Error mapping Record<\\{thingWithNonExistingPrimitives: \\{__internalNeo4jId__: \\d+, id: NULL, someBoolean: NULL, __nodeLabels__: \\[\"NonExistingPrimitives\"\\]\\}\\}>")
117-
.withStackTraceContaining("unboxBoolean")
118-
.withRootCauseInstanceOf(NullPointerException.class);
117+
"Error mapping Record<\\{thingWithAllCypherTypes: \\{__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);
119120
}
120121

121122
@TestFactory
@@ -162,10 +163,10 @@ Stream<DynamicNode> conversionsShouldBeAppliedToEntities() {
162163
() -> assertThat(ReflectionTestUtils.getField(thing, a.getKey()))
163164
.isEqualTo(a.getValue()))));
164165

165-
DynamicContainer writes = DynamicContainer.dynamicContainer("write", entry.getValue().entrySet().stream()
166-
.map(a -> DynamicTest
167-
.dynamicTest(a.getKey(),
168-
() -> 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))));
169170

170171
return DynamicContainer.dynamicContainer(entry.getKey(), Arrays.asList(reads, writes));
171172
});
@@ -176,8 +177,6 @@ void assertWrite(Object thing, String fieldName, ConversionService conversionSer
176177
long id = (long) ReflectionTestUtils.getField(thing, "id");
177178
Object domainValue = ReflectionTestUtils.getField(thing, fieldName);
178179

179-
Field field = ReflectionUtils.findField(thing.getClass(), fieldName);
180-
Optional<ConvertWith> annotation = AnnotationUtils.findAnnotation(field, ConvertWith.class);
181180
Function<Object, Value> conversion;
182181
if (fieldName.equals("dateAsLong")) {
183182
conversion = o -> Values.value(((Date) o).getTime());
@@ -189,8 +188,7 @@ void assertWrite(Object thing, String fieldName, ConversionService conversionSer
189188
Value driverValue;
190189
if (domainValue != null && Collection.class.isAssignableFrom(domainValue.getClass())) {
191190
Collection<?> sourceCollection = (Collection<?>) domainValue;
192-
Object[] targetCollection = (sourceCollection).stream()
193-
.map(element -> conversion.apply(element)).toArray();
191+
Object[] targetCollection = (sourceCollection).stream().map(conversion).toArray();
194192
driverValue = Values.value(targetCollection);
195193
} else {
196194
driverValue = conversion.apply(domainValue);
@@ -240,6 +238,35 @@ void parametersTargetingConvertedAttributesMustBeConverted(@Autowired CustomType
240238
.hasSizeGreaterThan(0);
241239
}
242240

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+
243270
public interface ConvertedIDsRepository extends Neo4jRepository<ThingWithUUIDID, UUID> {
244271
}
245272

@@ -252,18 +279,11 @@ public interface AdditionalTypesRepository extends Neo4jRepository<ThingWithAllA
252279
public interface SpatialTypesRepository extends Neo4jRepository<ThingWithAllSpatialTypes, Long> {
253280
}
254281

255-
public interface NonExistingPrimitivesRepository extends Neo4jRepository<ThingWithNonExistingPrimitives, Long> {
256-
}
257-
258282
public interface CustomTypesRepository extends Neo4jRepository<ThingWithCustomTypes, Long> {
259283

260284
List<ThingWithCustomTypes> findAllByDateAsString(Date theDate);
261285
}
262286

263-
public interface ThingWithCompositePropertiesRepository
264-
extends Neo4jRepository<ThingWithCompositeProperties, Long> {
265-
}
266-
267287
@Configuration
268288
@EnableNeo4jRepositories(considerNestedRepositories = true)
269289
@EnableTransactionManagement
@@ -281,7 +301,7 @@ public Neo4jConversions neo4jConversions() {
281301

282302
@Bean
283303
public BookmarkCapture bookmarkCapture() {
284-
return new BookmarkCapture();
304+
return Neo4jConversionsITBase.bookmarkCapture;
285305
}
286306

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