2525import java .util .Map ;
2626import java .util .Optional ;
2727import java .util .Set ;
28+ import java .util .concurrent .ConcurrentHashMap ;
2829import java .util .concurrent .locks .Lock ;
2930import java .util .concurrent .locks .ReentrantReadWriteLock ;
3031import java .util .function .BiConsumer ;
@@ -334,6 +335,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
334335
335336 // save final state of the bean
336337 knownObjects .storeObject (internalId , bean );
338+ knownObjects .mappedWithQueryResult (internalId , queryResult );
337339 return bean ;
338340 };
339341
@@ -342,21 +344,36 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
342344 if (mappedObject == null ) {
343345 mappedObject = mappedObjectSupplier .get ();
344346 knownObjects .storeObject (internalId , mappedObject );
345- } else if (knownObjects .alreadyMappedInPreviousRecord (internalId )) {
346- // If the object were created in a run before, it _could_ have missing relationships
347- // (e.g. due to incomplete fetching by a custom query)
348- // in such cases we will add the additional data from the next record.
347+ knownObjects .mappedWithQueryResult (internalId , queryResult );
348+ } else if (knownObjects .alreadyMappedInPreviousRecord (internalId ) || hasMoreFields (queryResult .asMap (), knownObjects .getQueryResultsFor (internalId ))) {
349+ // If the object were created in a run before or from a different path that represents another projection,
350+ // it _could_ have missing relationships and properties.
351+ // In such cases, we will add the additional data from the next record.
349352 // This can and should only work for
350- // 1. mutable owning types
353+ // 1. Mutable owning types
351354 // AND (!!!)
352- // 2. mutable target types
355+ // 2. Mutable target types
353356 // because we cannot just create new instances
354357 populateProperties (queryResult , (Neo4jPersistentEntity <ET >) genericTargetNodeDescription , nodeDescription , internalId , mappedObject , lastMappedEntity , relationshipsFromResult , nodesFromResult , true );
355358 }
356359 // due to a needed side effect in `populateProperties`, the entity might have been changed
357360 return getMostCurrentInstance (internalId , mappedObject );
358361 }
359362
363+ private boolean hasMoreFields (Map <String , Object > currentQueryResult , Set <Map <String , Object >> savedQueryResults ) {
364+ if (savedQueryResults .isEmpty ()) {
365+ return true ;
366+ }
367+ Set <String > currentFields = new HashSet <>(currentQueryResult .keySet ());
368+ Set <String > alreadyProcessedFields = new HashSet <>();
369+
370+ for (Map <String , Object > savedQueryResult : savedQueryResults ) {
371+ alreadyProcessedFields .addAll (savedQueryResult .keySet ());
372+ }
373+ currentFields .removeAll (alreadyProcessedFields );
374+ return !currentFields .isEmpty ();
375+ }
376+
360377 @ Nullable
361378 private <ET > ET getMostCurrentInstance (String internalId , ET fallbackInstance ) {
362379 return (ET ) (knownObjects .getObject (internalId ) != null ? knownObjects .getObject (internalId ) : fallbackInstance );
@@ -383,21 +400,19 @@ private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEnt
383400 Predicate <Neo4jPersistentProperty > isConstructorParameter = concreteNodeDescription
384401 .getInstanceCreatorMetadata ()::isCreatorParameter ;
385402
386- // if the object were mapped before, we assume that at least all properties are populated
387- if (!objectAlreadyMapped ) {
388- boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
389- // Fill simple properties
390- PropertyHandler <Neo4jPersistentProperty > handler = populateFrom (queryResult , propertyAccessor ,
391- isConstructorParameter , nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , isKotlinType );
392- PropertyHandlerSupport .of (concreteNodeDescription ).doWithProperties (handler );
393- }
403+ boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
404+ // Fill simple properties
405+ PropertyHandler <Neo4jPersistentProperty > handler = populateFrom (queryResult , propertyAccessor ,
406+ isConstructorParameter , nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , isKotlinType , objectAlreadyMapped );
407+ PropertyHandlerSupport .of (concreteNodeDescription ).doWithProperties (handler );
394408 // in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
395409 // reference the start again. Because it is getting still constructed, it won't be in the knownObjects
396410 // store unless we temporarily put it there.
397411 knownObjects .storeObject (internalId , propertyAccessor .getBean ());
412+ knownObjects .mappedWithQueryResult (internalId , queryResult );
398413
399414 AssociationHandlerSupport .of (concreteNodeDescription ).doWithAssociations (
400- populateFrom (queryResult , baseNodeDescription , propertyAccessor , isConstructorParameter , objectAlreadyMapped , relationshipsFromResult , nodesFromResult ));
415+ populateFrom (queryResult , baseNodeDescription , propertyAccessor , isConstructorParameter , objectAlreadyMapped , relationshipsFromResult , nodesFromResult , internalId ));
401416 }
402417
403418 @ NonNull
@@ -499,22 +514,25 @@ public <T> T getParameterValue(Parameter<T, Neo4jPersistentProperty> parameter)
499514
500515 private PropertyHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult ,
501516 PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
502- Collection <String > surplusLabels , @ Nullable Object targetNode , boolean ownerIsKotlinType ) {
517+ Collection <String > surplusLabels , @ Nullable Object targetNode , boolean ownerIsKotlinType , boolean objectAlreadyMapped ) {
503518
504519 return property -> {
505520 if (isConstructorParameter .test (property )) {
506521 return ;
507522 }
508523
509524 TypeInformation <?> typeInformation = property .getTypeInformation ();
510- if (property .isDynamicLabels ()) {
511- propertyAccessor .setProperty (property ,
512- createDynamicLabelsProperty (typeInformation , surplusLabels ));
513- } else if (property .isAnnotationPresent (TargetNode .class )) {
514- if (queryResult instanceof Relationship ) {
515- propertyAccessor .setProperty (property , targetNode );
525+ if (!objectAlreadyMapped ) {
526+ if (property .isDynamicLabels ()) {
527+ propertyAccessor .setProperty (property ,
528+ createDynamicLabelsProperty (typeInformation , surplusLabels ));
529+ } else if (property .isAnnotationPresent (TargetNode .class )) {
530+ if (queryResult instanceof Relationship ) {
531+ propertyAccessor .setProperty (property , targetNode );
532+ }
516533 }
517- } else {
534+ }
535+ if (!property .isDynamicLabels () && !property .isAnnotationPresent (TargetNode .class )) {
518536 Object value = conversionService .readValue (extractValueOf (property , queryResult ), typeInformation , property .getOptionalConverter ());
519537 if (value != null ) {
520538 Class <?> rawType = typeInformation .getType ();
@@ -532,7 +550,7 @@ private static Object getValueOrDefault(boolean ownerIsKotlinType, Class<?> rawT
532550
533551 private AssociationHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult , NodeDescription <?> baseDescription ,
534552 PersistentPropertyAccessor <?> propertyAccessor , Predicate <Neo4jPersistentProperty > isConstructorParameter ,
535- boolean objectAlreadyMapped , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult ) {
553+ boolean objectAlreadyMapped , Collection <Relationship > relationshipsFromResult , Collection <Node > nodesFromResult , String internalId ) {
536554
537555 return association -> {
538556
@@ -555,15 +573,15 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
555573
556574 boolean propertyValueNotNull = propertyValue != null ;
557575
558- boolean populatedCollection = persistentProperty .isCollectionLike ()
576+ boolean populatedCollection = objectAlreadyMapped && persistentProperty .isCollectionLike ()
559577 && propertyValueNotNull
560578 && !((Collection <?>) propertyValue ).isEmpty ();
561579
562- boolean populatedMap = persistentProperty .isMap ()
580+ boolean populatedMap = objectAlreadyMapped && persistentProperty .isMap ()
563581 && propertyValueNotNull
564582 && !((Map <?, ?>) propertyValue ).isEmpty ();
565583
566- boolean populatedScalarValue = !persistentProperty .isCollectionLike () && !persistentProperty .isMap ()
584+ boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty .isCollectionLike () && !persistentProperty .isMap ()
567585 && propertyValueNotNull ;
568586
569587 if (populatedCollection ) {
@@ -592,6 +610,7 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
592610
593611 createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , baseDescription , relationshipsFromResult , nodesFromResult )
594612 .ifPresent (value -> propertyAccessor .setProperty (persistentProperty , value ));
613+
595614 };
596615 }
597616
@@ -903,6 +922,7 @@ static class KnownObjects {
903922 private final Set <String > idsInCreation = new HashSet <>();
904923
905924 private final Map <String , Integer > processedRelationships = new HashMap <>();
925+ private final Map <String , Set <Map <String , Object >>> mappedQueryResults = new HashMap <>();
906926
907927 private void storeObject (@ Nullable String internalId , Object object ) {
908928 if (internalId == null ) {
@@ -1029,5 +1049,24 @@ private void nextRecord() {
10291049 previousRecords .addAll (internalCurrentRecord .keySet ());
10301050 internalCurrentRecord .clear ();
10311051 }
1052+
1053+ private void mappedWithQueryResult (String internalId , MapAccessor queryResult ) {
1054+ try {
1055+ write .lock ();
1056+ mappedQueryResults .computeIfAbsent (internalId , id -> ConcurrentHashMap .newKeySet ())
1057+ .add (queryResult .asMap ());
1058+ } finally {
1059+ write .unlock ();
1060+ }
1061+ }
1062+
1063+ private Set <Map <String , Object >> getQueryResultsFor (String internalId ) {
1064+ try {
1065+ read .lock ();
1066+ return mappedQueryResults .get (internalId );
1067+ } finally {
1068+ read .unlock ();
1069+ }
1070+ }
10321071 }
10331072}
0 commit comments