@@ -102,6 +102,8 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
102102 public <R > R read (Class <R > targetType , MapAccessor mapAccessor ) {
103103
104104 Neo4jPersistentEntity <R > rootNodeDescription = (Neo4jPersistentEntity ) nodeDescriptionStore .getNodeDescription (targetType );
105+ knownObjects .nextRecord ();
106+
105107 MapAccessor queryRoot = determineQueryRoot (mapAccessor , rootNodeDescription );
106108 if (queryRoot == null ) {
107109 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
272274 Neo4jPersistentEntity <ET > concreteNodeDescription = (Neo4jPersistentEntity <ET >) nodeDescriptionAndLabels
273275 .getNodeDescription ();
274276
275- boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
276277 ET instance = instantiate (concreteNodeDescription , queryResult ,
277278 nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , relationshipsFromResult , nodesFromResult );
278279
279280 knownObjects .removeFromInCreation (internalId );
280- PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (instance );
281281
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 );
299285 ET bean = propertyAccessor .getBean ();
300286
301287 // save final state of the bean
302288 knownObjects .storeObject (internalId , bean );
303289 return bean ;
304290 };
305291
306- Object mappedObject = knownObjects .getObject (internalId );
292+ ET mappedObject = ( ET ) knownObjects .getObject (internalId );
307293 if (mappedObject == null ) {
308- mappedObject = mappedObjectSupplier .get ();
294+ mappedObject = ( ET ) mappedObjectSupplier .get ();
309295 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 );
310306 }
311307 return (ET ) mappedObject ;
312308 }
313309
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+
314348 @ Nullable
315349 private Long getInternalId (@ NonNull MapAccessor queryResult ) {
316350 return queryResult instanceof Node
@@ -398,8 +432,9 @@ public Object getParameterValue(PreferredConstructor.Parameter parameter) {
398432 }
399433
400434 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+
403438 return property -> {
404439 if (isConstructorParameter .test (property )) {
405440 return ;
@@ -421,20 +456,55 @@ private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryR
421456 };
422457 }
423458
459+ @ Nullable
424460 private static Object getValueOrDefault (boolean ownerIsKotlinType , Class <?> rawType , @ Nullable Object value ) {
425461
426462 return value == null && !ownerIsKotlinType && rawType .isPrimitive () ? ReflectionUtils .getPrimitiveDefault (rawType ) : value ;
427463 }
428464
429465 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+
431469 return association -> {
432470
433471 Neo4jPersistentProperty persistentProperty = association .getInverse ();
472+
434473 if (isConstructorParameter .test (persistentProperty )) {
435474 return ;
436475 }
437476
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+
438508 createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , relationshipsFromResult , nodesFromResult )
439509 .ifPresent (value -> propertyAccessor .setProperty (persistentProperty , value ));
440510 };
@@ -654,6 +724,7 @@ static class KnownObjects {
654724 private final Lock write = lock .writeLock ();
655725
656726 private final Map <Long , Object > internalIdStore = new HashMap <>();
727+ private final Map <Long , Boolean > internalNextRecord = new HashMap <>();
657728 private final Set <Long > idsInCreation = new HashSet <>();
658729
659730 private void storeObject (@ Nullable Long internalId , Object object ) {
@@ -664,6 +735,7 @@ private void storeObject(@Nullable Long internalId, Object object) {
664735 write .lock ();
665736 idsInCreation .remove (internalId );
666737 internalIdStore .put (internalId , object );
738+ internalNextRecord .put (internalId , false );
667739 } finally {
668740 write .unlock ();
669741 }
@@ -725,5 +797,32 @@ private void removeFromInCreation(@Nullable Long internalId) {
725797 write .unlock ();
726798 }
727799 }
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+ }
728827 }
729828}
0 commit comments