@@ -144,7 +144,7 @@ private <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, @Nullable Ne
144144 Node node = value .asNode ();
145145 if (primaryLabels .stream ().anyMatch (node ::hasLabel )) { // it has a matching label
146146 // We haven't seen this node yet, so we take it
147- if (knownObjects .getObject (node .id ()) == null ) {
147+ if (knownObjects .getObject ("N" + node .id ()) == null ) {
148148 matchingNodes .add (node );
149149 } else {
150150 seenMatchingNodes .add (node );
@@ -263,22 +263,23 @@ private static MapAccessor mergeRootNodeWithRecord(Node node, MapAccessor record
263263 private <ET > ET map (MapAccessor queryResult , MapAccessor allValues , Neo4jPersistentEntity <ET > nodeDescription ) {
264264 Collection <Relationship > relationshipsFromResult = extractRelationships (allValues );
265265 Collection <Node > nodesFromResult = extractNodes (allValues );
266- return map (queryResult , nodeDescription , null , relationshipsFromResult , nodesFromResult );
266+ return map (queryResult , nodeDescription , null , null , relationshipsFromResult , nodesFromResult );
267267 }
268268
269269 private <ET > ET map (MapAccessor queryResult , Neo4jPersistentEntity <ET > nodeDescription ,
270- @ Nullable Object lastMappedEntity , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
270+ @ Nullable Object lastMappedEntity , @ Nullable RelationshipDescription relationshipDescription , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
271271
272272 // if the given result does not contain an identifier to the mapped object cannot get temporarily saved
273- Long internalId = getInternalId (queryResult );
273+ String direction = relationshipDescription != null ? relationshipDescription .getDirection ().name () : null ;
274+ String internalId = getInternalId (queryResult , direction );
274275
275276 Supplier <ET > mappedObjectSupplier = () -> {
276277 if (knownObjects .isInCreation (internalId )) {
277278 throw new MappingException (
278279 String .format (
279280 "The node with id %s has a logical cyclic mapping dependency. " +
280281 "Its creation caused the creation of another node that has a reference to this." ,
281- internalId )
282+ internalId . substring ( 1 ) )
282283 );
283284 }
284285 knownObjects .setInCreation (internalId );
@@ -326,7 +327,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
326327 }
327328
328329
329- private <ET > void populateProperties (MapAccessor queryResult , Neo4jPersistentEntity <ET > nodeDescription , Long internalId ,
330+ private <ET > void populateProperties (MapAccessor queryResult , Neo4jPersistentEntity <ET > nodeDescription , String internalId ,
330331 ET mappedObject , @ Nullable Object lastMappedEntity ,
331332 Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult , boolean objectAlreadyMapped ) {
332333
@@ -364,12 +365,30 @@ private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEnt
364365 }
365366
366367 @ Nullable
367- private Long getInternalId (@ NonNull MapAccessor queryResult ) {
368- return queryResult instanceof Node
369- ? (Long ) ((Node ) queryResult ).id ()
370- : queryResult .get (Constants .NAME_OF_INTERNAL_ID ) == null || queryResult .get (Constants .NAME_OF_INTERNAL_ID ).isNull ()
371- ? null
372- : queryResult .get (Constants .NAME_OF_INTERNAL_ID ).asLong ();
368+ private String getInternalId (@ NonNull MapAccessor queryResult , @ Nullable String seed ) {
369+ if (queryResult instanceof Node ) {
370+ return "N" + ((Node ) queryResult ).id ();
371+ } else if (queryResult instanceof Relationship ) {
372+ Relationship relationshipValue = (Relationship ) queryResult ;
373+ return "R" + seed + relationshipValue .id ();
374+ } else if (!(queryResult .get (Constants .NAME_OF_INTERNAL_ID ) == null || queryResult .get (Constants .NAME_OF_INTERNAL_ID ).isNull ())) {
375+ return "N" + queryResult .get (Constants .NAME_OF_INTERNAL_ID ).asLong ();
376+ }
377+
378+ return null ;
379+ }
380+
381+ @ Nullable
382+ private Long getInternalIdAsLong (@ NonNull MapAccessor queryResult ) {
383+ if (queryResult instanceof Node ) {
384+ return ((Node ) queryResult ).id ();
385+ } else if (queryResult instanceof Relationship ) {
386+ return ((Relationship ) queryResult ).id ();
387+ } else if (!(queryResult .get (Constants .NAME_OF_INTERNAL_ID ) == null || queryResult .get (Constants .NAME_OF_INTERNAL_ID ).isNull ())) {
388+ return queryResult .get (Constants .NAME_OF_INTERNAL_ID ).asLong ();
389+ }
390+
391+ return null ;
373392 }
374393
375394 @ NonNull
@@ -538,6 +557,23 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
538557 boolean populatedScalarValue = !persistentProperty .isCollectionLike () && !persistentProperty .isMap ()
539558 && propertyValueNotNull ;
540559
560+ if (populatedCollection ) {
561+ createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , baseDescription , relationshipsFromResult , nodesFromResult , false )
562+ .ifPresent (value -> {
563+ Collection <?> providedCollection = (Collection <?>) value ;
564+ Collection <?> existingValue = (Collection <?>) propertyValue ;
565+ Collection <Object > newValue = CollectionFactory .createCollection (existingValue .getClass (), providedCollection .size () + existingValue .size ());
566+
567+ RelationshipDescription relationshipDescription = (RelationshipDescription ) association ;
568+ Map <Object , Object > mergedValues = new HashMap <>();
569+ mergeCollections (relationshipDescription , existingValue , mergedValues );
570+ mergeCollections (relationshipDescription , providedCollection , mergedValues );
571+
572+ newValue .addAll (mergedValues .values ());
573+ propertyAccessor .setProperty (persistentProperty , newValue );
574+ });
575+ }
576+
541577 boolean propertyAlreadyPopulated = populatedCollection || populatedMap || populatedScalarValue ;
542578
543579 // avoid unnecessary re-assignment of values
@@ -550,9 +586,33 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
550586 };
551587 }
552588
589+ private void mergeCollections (RelationshipDescription relationshipDescription , Collection <?> values , Map <Object , Object > mergedValues ) {
590+ for (Object existingValueInCollection : values ) {
591+ if (relationshipDescription .hasRelationshipProperties ()) {
592+ Object existingIdPropertyValue = ((Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity ())
593+ .getPropertyAccessor (existingValueInCollection )
594+ .getProperty (((Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity ()).getIdProperty ());
595+
596+ mergedValues .put (existingIdPropertyValue , existingValueInCollection );
597+ } else if (!relationshipDescription .isDynamic ()) { // should not happen because this is all inside populatedCollection (but better safe than sorry)
598+ Object existingIdPropertyValue = ((Neo4jPersistentEntity <?>) relationshipDescription .getTarget ())
599+ .getPropertyAccessor (existingValueInCollection )
600+ .getProperty (((Neo4jPersistentEntity <?>) relationshipDescription .getTarget ()).getIdProperty ());
601+
602+ mergedValues .put (existingIdPropertyValue , existingValueInCollection );
603+ }
604+ }
605+ }
606+
607+ private Optional <Object > createInstanceOfRelationships (Neo4jPersistentProperty persistentProperty , MapAccessor values ,
608+ RelationshipDescription relationshipDescription , NodeDescription <?> baseDescription , Collection <Relationship > relationshipsFromResult ,
609+ Collection <Node > nodesFromResult ) {
610+ return createInstanceOfRelationships (persistentProperty , values , relationshipDescription , baseDescription , relationshipsFromResult , nodesFromResult , true );
611+ }
612+
553613 private Optional <Object > createInstanceOfRelationships (Neo4jPersistentProperty persistentProperty , MapAccessor values ,
554614 RelationshipDescription relationshipDescription , NodeDescription <?> baseDescription , Collection <Relationship > relationshipsFromResult ,
555- Collection <Node > nodesFromResult ) {
615+ Collection <Node > nodesFromResult , boolean fetchMore ) {
556616
557617 String typeOfRelationship = relationshipDescription .getType ();
558618 String targetLabel = relationshipDescription .getTarget ().getPrimaryLabel ();
@@ -592,7 +652,7 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
592652 List <Object > relationshipsAndProperties = new ArrayList <>();
593653
594654 if (Values .NULL .equals (list )) {
595- Long sourceNodeId = getInternalId (values );
655+ Long sourceNodeId = getInternalIdAsLong (values );
596656
597657 Function <Relationship , Long > sourceIdSelector = relationshipDescription .isIncoming () ? Relationship ::endNodeId : Relationship ::startNodeId ;
598658 Function <Relationship , Long > targetIdSelector = relationshipDescription .isIncoming () ? Relationship ::startNodeId : Relationship ::endNodeId ;
@@ -619,22 +679,37 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
619679 // If this relationship got processed twice (OUTGOING, INCOMING), it is never needed again
620680 // and therefor should not be in the list.
621681 // Otherwise, for highly linked data it could potentially cause a StackOverflowError.
622- if (knownObjects .hasProcessedRelationshipCompletely (possibleRelationship .id ())) {
682+ String direction = relationshipDescription .getDirection ().name ();
683+ if (knownObjects .hasProcessedRelationshipCompletely ("R" + direction + possibleRelationship .id ())) {
623684 relationshipsFromResult .remove (possibleRelationship );
624685 }
625686 // If the target is the same(equal) node, get the related object from the cache.
626687 // Avoiding the call to the map method also breaks an endless cycle of trying to finish
627688 // the property population of _this_ object.
628689 // The initial population will happen at the end of this mapping. This is sufficient because
629690 // it only affects properties not changing the instance of the object.
630- Object mappedObject = sourceNodeId != null && sourceNodeId .equals (targetNodeId )
631- ? knownObjects .getObject (sourceNodeId )
632- : map (possibleValueNode , concreteTargetNodeDescription , null , relationshipsFromResult , nodesFromResult );
633- if (relationshipDescription .hasRelationshipProperties ()) {
691+ Object mappedObject ;
692+ if (fetchMore ) {
693+ mappedObject = sourceNodeId != null && sourceNodeId .equals (targetNodeId )
694+ ? knownObjects .getObject ("N" + sourceNodeId )
695+ : map (possibleValueNode , concreteTargetNodeDescription , null , null , relationshipsFromResult , nodesFromResult );
696+ } else {
697+ Object objectFromStore = knownObjects .getObject ("N" + targetNodeId );
698+ mappedObject = objectFromStore != null
699+ ? objectFromStore
700+ : map (possibleValueNode , concreteTargetNodeDescription , null , null , relationshipsFromResult , nodesFromResult );
701+ }
634702
635- Object relationshipProperties = map (possibleRelationship ,
636- (Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity (),
637- mappedObject , relationshipsFromResult , nodesFromResult );
703+ if (relationshipDescription .hasRelationshipProperties ()) {
704+ Object relationshipProperties ;
705+ if (fetchMore ) {
706+ relationshipProperties = map (possibleRelationship , (Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity (), mappedObject , relationshipDescription , relationshipsFromResult , nodesFromResult );
707+ } else {
708+ Object objectFromStore = knownObjects .getObject (getInternalId (possibleRelationship , relationshipDescription .getDirection ().name ()));
709+ relationshipProperties = objectFromStore != null
710+ ? objectFromStore
711+ : map (possibleRelationship , (Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity (), mappedObject , relationshipDescription , relationshipsFromResult , nodesFromResult );
712+ }
638713 relationshipsAndProperties .add (relationshipProperties );
639714 mappedObjectHandler .accept (possibleRelationship .type (), relationshipProperties );
640715 } else {
@@ -651,7 +726,15 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
651726 Neo4jPersistentEntity <?> concreteTargetNodeDescription =
652727 getMostConcreteTargetNodeDescription (genericTargetNodeDescription , relatedEntity );
653728
654- Object valueEntry = map (relatedEntity , concreteTargetNodeDescription , null , relationshipsFromResult , nodesFromResult );
729+ Object valueEntry ;
730+ if (fetchMore ) {
731+ valueEntry = map (relatedEntity , concreteTargetNodeDescription , null , null , relationshipsFromResult , nodesFromResult );
732+ } else {
733+ Object objectFromStore = knownObjects .getObject (getInternalId (relatedEntity , null ));
734+ valueEntry = objectFromStore != null
735+ ? objectFromStore
736+ : map (relatedEntity , concreteTargetNodeDescription , null , null , relationshipsFromResult , nodesFromResult );
737+ }
655738
656739 if (relationshipDescription .hasRelationshipProperties ()) {
657740 String sourceLabel = relationshipDescription .getSource ().getMostAbstractParentLabel (baseDescription );
@@ -660,9 +743,16 @@ private Optional<Object> createInstanceOfRelationships(Neo4jPersistentProperty p
660743 Relationship relatedEntityRelationship = relatedEntity .get (relationshipSymbolicName )
661744 .asRelationship ();
662745
663- Object relationshipProperties = map (relatedEntityRelationship ,
664- (Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity (),
665- valueEntry , relationshipsFromResult , nodesFromResult );
746+ Object relationshipProperties ;
747+ if (fetchMore ) {
748+ relationshipProperties = map (relatedEntityRelationship , (Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity (), valueEntry , relationshipDescription , relationshipsFromResult , nodesFromResult );
749+ } else {
750+ Object objectFromStore = knownObjects .getObject (getInternalId (relatedEntityRelationship , relationshipDescription .getDirection ().name ()));
751+ relationshipProperties = objectFromStore != null
752+ ? objectFromStore
753+ : map (relatedEntityRelationship , (Neo4jPersistentEntity <?>) relationshipDescription .getRelationshipPropertiesEntity (), valueEntry , relationshipDescription , relationshipsFromResult , nodesFromResult );
754+ }
755+
666756 relationshipsAndProperties .add (relationshipProperties );
667757 mappedObjectHandler .accept (relatedEntity .get (RelationshipDescription .NAME_OF_RELATIONSHIP_TYPE ).asString (), relationshipProperties );
668758 } else {
@@ -779,14 +869,14 @@ static class KnownObjects {
779869 private final Lock read = lock .readLock ();
780870 private final Lock write = lock .writeLock ();
781871
782- private final Map <Long , Object > internalIdStore = new HashMap <>();
783- private final Map <Long , Boolean > internalCurrentRecord = new HashMap <>();
784- private final Set <Long > previousRecords = new HashSet <>();
785- private final Set <Long > idsInCreation = new HashSet <>();
872+ private final Map <String , Object > internalIdStore = new HashMap <>();
873+ private final Map <String , Boolean > internalCurrentRecord = new HashMap <>();
874+ private final Set <String > previousRecords = new HashSet <>();
875+ private final Set <String > idsInCreation = new HashSet <>();
786876
787- private final Map <Long , Integer > processedRelationships = new HashMap <>();
877+ private final Map <String , Integer > processedRelationships = new HashMap <>();
788878
789- private void storeObject (@ Nullable Long internalId , Object object ) {
879+ private void storeObject (@ Nullable String internalId , Object object ) {
790880 if (internalId == null ) {
791881 return ;
792882 }
@@ -800,7 +890,7 @@ private void storeObject(@Nullable Long internalId, Object object) {
800890 }
801891 }
802892
803- private void setInCreation (@ Nullable Long internalId ) {
893+ private void setInCreation (@ Nullable String internalId ) {
804894 if (internalId == null ) {
805895 return ;
806896 }
@@ -812,7 +902,7 @@ private void setInCreation(@Nullable Long internalId) {
812902 }
813903 }
814904
815- private boolean isInCreation (@ Nullable Long internalId ) {
905+ private boolean isInCreation (@ Nullable String internalId ) {
816906 if (internalId == null ) {
817907 return false ;
818908 }
@@ -825,7 +915,7 @@ private boolean isInCreation(@Nullable Long internalId) {
825915 }
826916
827917 @ Nullable
828- private Object getObject (@ Nullable Long internalId ) {
918+ private Object getObject (@ Nullable String internalId ) {
829919 if (internalId == null ) {
830920 return null ;
831921 }
@@ -845,7 +935,7 @@ private Object getObject(@Nullable Long internalId) {
845935 return null ;
846936 }
847937
848- private void removeFromInCreation (@ Nullable Long internalId ) {
938+ private void removeFromInCreation (@ Nullable String internalId ) {
849939 if (internalId == null ) {
850940 return ;
851941 }
@@ -857,7 +947,7 @@ private void removeFromInCreation(@Nullable Long internalId) {
857947 }
858948 }
859949
860- private boolean alreadyMappedInPreviousRecord (@ Nullable Long internalId ) {
950+ private boolean alreadyMappedInPreviousRecord (@ Nullable String internalId ) {
861951 if (internalId == null ) {
862952 return false ;
863953 }
@@ -877,7 +967,7 @@ private boolean alreadyMappedInPreviousRecord(@Nullable Long internalId) {
877967 * It increases the process count of relationships (mapped by their ids)
878968 * AND checks if it was already processed twice (INCOMING/OUTGOING).
879969 */
880- private boolean hasProcessedRelationshipCompletely (Long relationshipId ) {
970+ private boolean hasProcessedRelationshipCompletely (String relationshipId ) {
881971 try {
882972 write .lock ();
883973
0 commit comments