@@ -101,6 +101,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
101101 @ Override
102102 public <R > R read (Class <R > targetType , MapAccessor mapAccessor ) {
103103
104+ knownObjects .nextRecord ();
104105 @ SuppressWarnings ("unchecked" ) // ¯\_(ツ)_/¯
105106 Neo4jPersistentEntity <R > rootNodeDescription = (Neo4jPersistentEntity <R >) nodeDescriptionStore .getNodeDescription (targetType );
106107 MapAccessor queryRoot = determineQueryRoot (mapAccessor , rootNodeDescription );
@@ -274,30 +275,14 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
274275 Neo4jPersistentEntity <ET > concreteNodeDescription = (Neo4jPersistentEntity <ET >) nodeDescriptionAndLabels
275276 .getNodeDescription ();
276277
277- boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
278278 ET instance = instantiate (concreteNodeDescription , queryResult ,
279279 nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , relationshipsFromResult , nodesFromResult );
280280
281281 knownObjects .removeFromInCreation (internalId );
282- PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (instance );
283282
284- if (concreteNodeDescription .requiresPropertyPopulation ()) {
285-
286- // Fill simple properties
287- Predicate <Neo4jPersistentProperty > isConstructorParameter = concreteNodeDescription
288- .getPersistenceConstructor ()::isConstructorParameter ;
289- PropertyHandler <Neo4jPersistentProperty > handler = populateFrom (queryResult , propertyAccessor ,
290- isConstructorParameter , nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , isKotlinType );
291- concreteNodeDescription .doWithProperties (handler );
292-
293- // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
294- // reference the start again. Because it is getting still constructed, it won't be in the knownObjects
295- // store unless we temporarily put it there.
296- knownObjects .storeObject (internalId , instance );
297- // Fill associations
298- concreteNodeDescription .doWithAssociations (
299- populateFrom (queryResult , propertyAccessor , isConstructorParameter , relationshipsFromResult , nodesFromResult ));
300- }
283+ populateProperties (queryResult , nodeDescription , internalId , instance , lastMappedEntity , relationshipsFromResult , nodesFromResult , false );
284+
285+ PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (instance );
301286 ET bean = propertyAccessor .getBean ();
302287
303288 // save final state of the bean
@@ -310,10 +295,58 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
310295 if (mappedObject == null ) {
311296 mappedObject = mappedObjectSupplier .get ();
312297 knownObjects .storeObject (internalId , mappedObject );
298+ } else if (knownObjects .alreadyMappedInPreviousRecord (internalId )) {
299+ // If the object were created in a run before, it _could_ have missing relationships
300+ // (e.g. due to incomplete fetching by a custom query)
301+ // in such cases we will add the additional data from the next record.
302+ // This can and should only work for
303+ // 1. mutable owning types
304+ // AND (!!!)
305+ // 2. mutable target types
306+ // because we cannot just create new instances
307+ populateProperties (queryResult , nodeDescription , internalId , mappedObject , lastMappedEntity , relationshipsFromResult , nodesFromResult , true );
313308 }
314309 return mappedObject ;
315310 }
316311
312+
313+ private <ET > void populateProperties (MapAccessor queryResult , Neo4jPersistentEntity <ET > nodeDescription , Long internalId ,
314+ ET mappedObject , @ Nullable Object lastMappedEntity ,
315+ Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult , boolean objectAlreadyMapped ) {
316+
317+ List <String > allLabels = getLabels (queryResult , nodeDescription );
318+ NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
319+ .deriveConcreteNodeDescription (nodeDescription , allLabels );
320+
321+ @ SuppressWarnings ("unchecked" )
322+ Neo4jPersistentEntity <ET > concreteNodeDescription = (Neo4jPersistentEntity <ET >) nodeDescriptionAndLabels
323+ .getNodeDescription ();
324+
325+ if (!concreteNodeDescription .requiresPropertyPopulation ()) {
326+ return ;
327+ }
328+
329+ PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (mappedObject );
330+ Predicate <Neo4jPersistentProperty > isConstructorParameter = concreteNodeDescription
331+ .getPersistenceConstructor ()::isConstructorParameter ;
332+
333+ // if the object were mapped before, we assume that at least all properties are populated
334+ if (!objectAlreadyMapped ) {
335+ boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
336+ // Fill simple properties
337+ PropertyHandler <Neo4jPersistentProperty > handler = populateFrom (queryResult , propertyAccessor ,
338+ isConstructorParameter , nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , isKotlinType );
339+ concreteNodeDescription .doWithProperties (handler );
340+ }
341+ // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
342+ // reference the start again. Because it is getting still constructed, it won't be in the knownObjects
343+ // store unless we temporarily put it there.
344+ knownObjects .storeObject (internalId , mappedObject );
345+ // Fill associations
346+ concreteNodeDescription .doWithAssociations (
347+ populateFrom (queryResult , propertyAccessor , isConstructorParameter , objectAlreadyMapped , relationshipsFromResult , nodesFromResult ));
348+ }
349+
317350 @ Nullable
318351 private Long getInternalId (@ NonNull MapAccessor queryResult ) {
319352 return queryResult instanceof Node
@@ -405,8 +438,9 @@ public <T> T getParameterValue(PreferredConstructor.Parameter<T, Neo4jPersistent
405438 }
406439
407440 private PropertyHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult ,
408- PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
409- Collection <String > surplusLabels , Object targetNode , boolean ownerIsKotlinType ) {
441+ PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
442+ Collection <String > surplusLabels , @ Nullable Object targetNode , boolean ownerIsKotlinType ) {
443+
410444 return property -> {
411445 if (isConstructorParameter .test (property )) {
412446 return ;
@@ -428,20 +462,55 @@ private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryR
428462 };
429463 }
430464
465+ @ Nullable
431466 private static Object getValueOrDefault (boolean ownerIsKotlinType , Class <?> rawType , @ Nullable Object value ) {
432467
433468 return value == null && !ownerIsKotlinType && rawType .isPrimitive () ? ReflectionUtils .getPrimitiveDefault (rawType ) : value ;
434469 }
435470
436471 private AssociationHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult ,
437- PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
472+ PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
473+ boolean objectAlreadyMapped , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
474+
438475 return association -> {
439476
440477 Neo4jPersistentProperty persistentProperty = association .getInverse ();
478+
441479 if (isConstructorParameter .test (persistentProperty )) {
442480 return ;
443481 }
444482
483+ if (objectAlreadyMapped ) {
484+
485+ // avoid multiple instances of the "same" object
486+ boolean willCreateNewInstance = persistentProperty .getWither () != null ;
487+ if (willCreateNewInstance ) {
488+ throw new MappingException ("Cannot create a new instance of an already existing object." );
489+ }
490+
491+ Object propertyValue = propertyAccessor .getProperty (persistentProperty );
492+
493+ boolean propertyValueNotNull = propertyValue != null ;
494+
495+ boolean populatedCollection = persistentProperty .isCollectionLike ()
496+ && propertyValueNotNull
497+ && !((Collection <?>) propertyValue ).isEmpty ();
498+
499+ boolean populatedMap = persistentProperty .isMap ()
500+ && propertyValueNotNull
501+ && !((Map <?, ?>) propertyValue ).isEmpty ();
502+
503+ boolean populatedScalarValue = !persistentProperty .isCollectionLike ()
504+ && propertyValueNotNull ;
505+
506+ boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue ;
507+
508+ // avoid unnecessary re-assignment of values
509+ if (propertyAlreadyPopulated ) {
510+ return ;
511+ }
512+ }
513+
445514 createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , relationshipsFromResult , nodesFromResult )
446515 .ifPresent (value -> propertyAccessor .setProperty (persistentProperty , value ));
447516 };
@@ -662,6 +731,7 @@ static class KnownObjects {
662731 private final Lock write = lock .writeLock ();
663732
664733 private final Map <Long , Object > internalIdStore = new HashMap <>();
734+ private final Map <Long , Boolean > internalNextRecord = new HashMap <>();
665735 private final Set <Long > idsInCreation = new HashSet <>();
666736
667737 private void storeObject (@ Nullable Long internalId , Object object ) {
@@ -672,6 +742,7 @@ private void storeObject(@Nullable Long internalId, Object object) {
672742 write .lock ();
673743 idsInCreation .remove (internalId );
674744 internalIdStore .put (internalId , object );
745+ internalNextRecord .put (internalId , false );
675746 } finally {
676747 write .unlock ();
677748 }
@@ -733,5 +804,32 @@ private void removeFromInCreation(@Nullable Long internalId) {
733804 write .unlock ();
734805 }
735806 }
807+
808+ private boolean alreadyMappedInPreviousRecord (@ Nullable Long internalId ) {
809+ if (internalId == null ) {
810+ return false ;
811+ }
812+ try {
813+
814+ read .lock ();
815+
816+ Boolean nextRecord = internalNextRecord .get (internalId );
817+
818+ if (nextRecord != null ) {
819+ return nextRecord ;
820+ }
821+
822+ } finally {
823+ read .unlock ();
824+ }
825+ return false ;
826+ }
827+
828+ /**
829+ * Mark all currently existing objects as mapped.
830+ */
831+ private void nextRecord () {
832+ internalNextRecord .replaceAll ((x , y ) -> true );
833+ }
736834 }
737835}
0 commit comments