@@ -102,6 +102,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
102
102
public <R > R read (Class <R > targetType , MapAccessor mapAccessor ) {
103
103
104
104
Neo4jPersistentEntity <R > rootNodeDescription = (Neo4jPersistentEntity ) nodeDescriptionStore .getNodeDescription (targetType );
105
+ knownObjects .nextRecord ();
106
+
105
107
MapAccessor queryRoot = determineQueryRoot (mapAccessor , rootNodeDescription );
106
108
if (queryRoot == null ) {
107
109
throw new NoRootNodeMappingException (String .format ("Could not find mappable nodes or relationships inside %s for %s" , mapAccessor , rootNodeDescription ));
@@ -272,45 +274,77 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
272
274
Neo4jPersistentEntity <ET > concreteNodeDescription = (Neo4jPersistentEntity <ET >) nodeDescriptionAndLabels
273
275
.getNodeDescription ();
274
276
275
- boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
276
277
ET instance = instantiate (concreteNodeDescription , queryResult ,
277
278
nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , relationshipsFromResult , nodesFromResult );
278
279
279
280
knownObjects .removeFromInCreation (internalId );
280
- PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (instance );
281
281
282
- if (concreteNodeDescription .requiresPropertyPopulation ()) {
283
-
284
- // Fill simple properties
285
- Predicate <Neo4jPersistentProperty > isConstructorParameter = concreteNodeDescription
286
- .getPersistenceConstructor ()::isConstructorParameter ;
287
- PropertyHandler <Neo4jPersistentProperty > handler = populateFrom (queryResult , propertyAccessor ,
288
- isConstructorParameter , nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , isKotlinType );
289
- concreteNodeDescription .doWithProperties (handler );
290
-
291
- // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
292
- // reference the start again. Because it is getting still constructed, it won't be in the knownObjects
293
- // store unless we temporarily put it there.
294
- knownObjects .storeObject (internalId , instance );
295
- // Fill associations
296
- concreteNodeDescription .doWithAssociations (
297
- populateFrom (queryResult , propertyAccessor , isConstructorParameter , relationshipsFromResult , nodesFromResult ));
298
- }
282
+ populateProperties (queryResult , nodeDescription , internalId , instance , lastMappedEntity , relationshipsFromResult , nodesFromResult , false );
283
+
284
+ PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (instance );
299
285
ET bean = propertyAccessor .getBean ();
300
286
301
287
// save final state of the bean
302
288
knownObjects .storeObject (internalId , bean );
303
289
return bean ;
304
290
};
305
291
306
- Object mappedObject = knownObjects .getObject (internalId );
292
+ ET mappedObject = ( ET ) knownObjects .getObject (internalId );
307
293
if (mappedObject == null ) {
308
- mappedObject = mappedObjectSupplier .get ();
294
+ mappedObject = ( ET ) mappedObjectSupplier .get ();
309
295
knownObjects .storeObject (internalId , mappedObject );
296
+ } else if (knownObjects .alreadyMappedInPreviousRecord (internalId )) {
297
+ // If the object were created in a run before, it _could_ have missing relationships
298
+ // (e.g. due to incomplete fetching by a custom query)
299
+ // in such cases we will add the additional data from the next record.
300
+ // This can and should only work for
301
+ // 1. mutable owning types
302
+ // AND (!!!)
303
+ // 2. mutable target types
304
+ // because we cannot just create new instances
305
+ populateProperties (queryResult , nodeDescription , internalId , mappedObject , lastMappedEntity , relationshipsFromResult , nodesFromResult , true );
310
306
}
311
307
return (ET ) mappedObject ;
312
308
}
313
309
310
+
311
+ private <ET > void populateProperties (MapAccessor queryResult , Neo4jPersistentEntity <ET > nodeDescription , Long internalId ,
312
+ ET mappedObject , @ Nullable Object lastMappedEntity ,
313
+ Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult , boolean objectAlreadyMapped ) {
314
+
315
+ List <String > allLabels = getLabels (queryResult , nodeDescription );
316
+ NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
317
+ .deriveConcreteNodeDescription (nodeDescription , allLabels );
318
+
319
+ @ SuppressWarnings ("unchecked" )
320
+ Neo4jPersistentEntity <ET > concreteNodeDescription = (Neo4jPersistentEntity <ET >) nodeDescriptionAndLabels
321
+ .getNodeDescription ();
322
+
323
+ if (!concreteNodeDescription .requiresPropertyPopulation ()) {
324
+ return ;
325
+ }
326
+
327
+ PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (mappedObject );
328
+ Predicate <Neo4jPersistentProperty > isConstructorParameter = concreteNodeDescription
329
+ .getPersistenceConstructor ()::isConstructorParameter ;
330
+
331
+ // if the object were mapped before, we assume that at least all properties are populated
332
+ if (!objectAlreadyMapped ) {
333
+ boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
334
+ // Fill simple properties
335
+ PropertyHandler <Neo4jPersistentProperty > handler = populateFrom (queryResult , propertyAccessor ,
336
+ isConstructorParameter , nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , isKotlinType );
337
+ concreteNodeDescription .doWithProperties (handler );
338
+ }
339
+ // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
340
+ // reference the start again. Because it is getting still constructed, it won't be in the knownObjects
341
+ // store unless we temporarily put it there.
342
+ knownObjects .storeObject (internalId , mappedObject );
343
+ // Fill associations
344
+ concreteNodeDescription .doWithAssociations (
345
+ populateFrom (queryResult , propertyAccessor , isConstructorParameter , objectAlreadyMapped , relationshipsFromResult , nodesFromResult ));
346
+ }
347
+
314
348
@ Nullable
315
349
private Long getInternalId (@ NonNull MapAccessor queryResult ) {
316
350
return queryResult instanceof Node
@@ -398,8 +432,9 @@ public Object getParameterValue(PreferredConstructor.Parameter parameter) {
398
432
}
399
433
400
434
private PropertyHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult ,
401
- PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
402
- Collection <String > surplusLabels , Object targetNode , boolean ownerIsKotlinType ) {
435
+ PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
436
+ Collection <String > surplusLabels , @ Nullable Object targetNode , boolean ownerIsKotlinType ) {
437
+
403
438
return property -> {
404
439
if (isConstructorParameter .test (property )) {
405
440
return ;
@@ -421,20 +456,55 @@ private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryR
421
456
};
422
457
}
423
458
459
+ @ Nullable
424
460
private static Object getValueOrDefault (boolean ownerIsKotlinType , Class <?> rawType , @ Nullable Object value ) {
425
461
426
462
return value == null && !ownerIsKotlinType && rawType .isPrimitive () ? ReflectionUtils .getPrimitiveDefault (rawType ) : value ;
427
463
}
428
464
429
465
private AssociationHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult ,
430
- PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
466
+ PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
467
+ boolean objectAlreadyMapped , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
468
+
431
469
return association -> {
432
470
433
471
Neo4jPersistentProperty persistentProperty = association .getInverse ();
472
+
434
473
if (isConstructorParameter .test (persistentProperty )) {
435
474
return ;
436
475
}
437
476
477
+ if (objectAlreadyMapped ) {
478
+
479
+ // avoid multiple instances of the "same" object
480
+ boolean willCreateNewInstance = persistentProperty .getWither () != null ;
481
+ if (willCreateNewInstance ) {
482
+ throw new MappingException ("Cannot create a new instance of an already existing object." );
483
+ }
484
+
485
+ Object propertyValue = propertyAccessor .getProperty (persistentProperty );
486
+
487
+ boolean propertyValueNotNull = propertyValue != null ;
488
+
489
+ boolean populatedCollection = persistentProperty .isCollectionLike ()
490
+ && propertyValueNotNull
491
+ && !((Collection <?>) propertyValue ).isEmpty ();
492
+
493
+ boolean populatedMap = persistentProperty .isMap ()
494
+ && propertyValueNotNull
495
+ && !((Map <?, ?>) propertyValue ).isEmpty ();
496
+
497
+ boolean populatedScalarValue = !persistentProperty .isCollectionLike ()
498
+ && propertyValueNotNull ;
499
+
500
+ boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue ;
501
+
502
+ // avoid unnecessary re-assignment of values
503
+ if (propertyAlreadyPopulated ) {
504
+ return ;
505
+ }
506
+ }
507
+
438
508
createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , relationshipsFromResult , nodesFromResult )
439
509
.ifPresent (value -> propertyAccessor .setProperty (persistentProperty , value ));
440
510
};
@@ -654,6 +724,7 @@ static class KnownObjects {
654
724
private final Lock write = lock .writeLock ();
655
725
656
726
private final Map <Long , Object > internalIdStore = new HashMap <>();
727
+ private final Map <Long , Boolean > internalNextRecord = new HashMap <>();
657
728
private final Set <Long > idsInCreation = new HashSet <>();
658
729
659
730
private void storeObject (@ Nullable Long internalId , Object object ) {
@@ -664,6 +735,7 @@ private void storeObject(@Nullable Long internalId, Object object) {
664
735
write .lock ();
665
736
idsInCreation .remove (internalId );
666
737
internalIdStore .put (internalId , object );
738
+ internalNextRecord .put (internalId , false );
667
739
} finally {
668
740
write .unlock ();
669
741
}
@@ -725,5 +797,32 @@ private void removeFromInCreation(@Nullable Long internalId) {
725
797
write .unlock ();
726
798
}
727
799
}
800
+
801
+ private boolean alreadyMappedInPreviousRecord (@ Nullable Long internalId ) {
802
+ if (internalId == null ) {
803
+ return false ;
804
+ }
805
+ try {
806
+
807
+ read .lock ();
808
+
809
+ Boolean nextRecord = internalNextRecord .get (internalId );
810
+
811
+ if (nextRecord != null ) {
812
+ return nextRecord ;
813
+ }
814
+
815
+ } finally {
816
+ read .unlock ();
817
+ }
818
+ return false ;
819
+ }
820
+
821
+ /**
822
+ * Mark all currently existing objects as mapped.
823
+ */
824
+ private void nextRecord () {
825
+ internalNextRecord .replaceAll ((x , y ) -> true );
826
+ }
728
827
}
729
828
}
0 commit comments