25
25
import java .util .Map ;
26
26
import java .util .Optional ;
27
27
import java .util .Set ;
28
+ import java .util .concurrent .ConcurrentHashMap ;
28
29
import java .util .concurrent .locks .Lock ;
29
30
import java .util .concurrent .locks .ReentrantReadWriteLock ;
30
31
import java .util .function .BiConsumer ;
@@ -334,6 +335,7 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
334
335
335
336
// save final state of the bean
336
337
knownObjects .storeObject (internalId , bean );
338
+ knownObjects .mappedWithQueryResult (internalId , queryResult );
337
339
return bean ;
338
340
};
339
341
@@ -342,21 +344,36 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
342
344
if (mappedObject == null ) {
343
345
mappedObject = mappedObjectSupplier .get ();
344
346
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.
349
352
// This can and should only work for
350
- // 1. mutable owning types
353
+ // 1. Mutable owning types
351
354
// AND (!!!)
352
- // 2. mutable target types
355
+ // 2. Mutable target types
353
356
// because we cannot just create new instances
354
357
populateProperties (queryResult , (Neo4jPersistentEntity <ET >) genericTargetNodeDescription , nodeDescription , internalId , mappedObject , lastMappedEntity , relationshipsFromResult , nodesFromResult , true );
355
358
}
356
359
// due to a needed side effect in `populateProperties`, the entity might have been changed
357
360
return getMostCurrentInstance (internalId , mappedObject );
358
361
}
359
362
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
+
360
377
@ Nullable
361
378
private <ET > ET getMostCurrentInstance (String internalId , ET fallbackInstance ) {
362
379
return (ET ) (knownObjects .getObject (internalId ) != null ? knownObjects .getObject (internalId ) : fallbackInstance );
@@ -383,21 +400,19 @@ private <ET> void populateProperties(MapAccessor queryResult, Neo4jPersistentEnt
383
400
Predicate <Neo4jPersistentProperty > isConstructorParameter = concreteNodeDescription
384
401
.getInstanceCreatorMetadata ()::isCreatorParameter ;
385
402
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 );
394
408
// in a cyclic graph / with bidirectional relationships, we could end up in a state in which we
395
409
// reference the start again. Because it is getting still constructed, it won't be in the knownObjects
396
410
// store unless we temporarily put it there.
397
411
knownObjects .storeObject (internalId , propertyAccessor .getBean ());
412
+ knownObjects .mappedWithQueryResult (internalId , queryResult );
398
413
399
414
AssociationHandlerSupport .of (concreteNodeDescription ).doWithAssociations (
400
- populateFrom (queryResult , baseNodeDescription , propertyAccessor , isConstructorParameter , objectAlreadyMapped , relationshipsFromResult , nodesFromResult ));
415
+ populateFrom (queryResult , baseNodeDescription , propertyAccessor , isConstructorParameter , objectAlreadyMapped , relationshipsFromResult , nodesFromResult , internalId ));
401
416
}
402
417
403
418
@ NonNull
@@ -499,22 +514,25 @@ public <T> T getParameterValue(Parameter<T, Neo4jPersistentProperty> parameter)
499
514
500
515
private PropertyHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult ,
501
516
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 ) {
503
518
504
519
return property -> {
505
520
if (isConstructorParameter .test (property )) {
506
521
return ;
507
522
}
508
523
509
524
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
+ }
516
533
}
517
- } else {
534
+ }
535
+ if (!property .isDynamicLabels () && !property .isAnnotationPresent (TargetNode .class )) {
518
536
Object value = conversionService .readValue (extractValueOf (property , queryResult ), typeInformation , property .getOptionalConverter ());
519
537
if (value != null ) {
520
538
Class <?> rawType = typeInformation .getType ();
@@ -532,7 +550,7 @@ private static Object getValueOrDefault(boolean ownerIsKotlinType, Class<?> rawT
532
550
533
551
private AssociationHandler <Neo4jPersistentProperty > populateFrom (MapAccessor queryResult , NodeDescription <?> baseDescription ,
534
552
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 ) {
536
554
537
555
return association -> {
538
556
@@ -555,15 +573,15 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
555
573
556
574
boolean propertyValueNotNull = propertyValue != null ;
557
575
558
- boolean populatedCollection = persistentProperty .isCollectionLike ()
576
+ boolean populatedCollection = objectAlreadyMapped && persistentProperty .isCollectionLike ()
559
577
&& propertyValueNotNull
560
578
&& !((Collection <?>) propertyValue ).isEmpty ();
561
579
562
- boolean populatedMap = persistentProperty .isMap ()
580
+ boolean populatedMap = objectAlreadyMapped && persistentProperty .isMap ()
563
581
&& propertyValueNotNull
564
582
&& !((Map <?, ?>) propertyValue ).isEmpty ();
565
583
566
- boolean populatedScalarValue = !persistentProperty .isCollectionLike () && !persistentProperty .isMap ()
584
+ boolean populatedScalarValue = objectAlreadyMapped && !persistentProperty .isCollectionLike () && !persistentProperty .isMap ()
567
585
&& propertyValueNotNull ;
568
586
569
587
if (populatedCollection ) {
@@ -592,6 +610,7 @@ private AssociationHandler<Neo4jPersistentProperty> populateFrom(MapAccessor que
592
610
593
611
createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , baseDescription , relationshipsFromResult , nodesFromResult )
594
612
.ifPresent (value -> propertyAccessor .setProperty (persistentProperty , value ));
613
+
595
614
};
596
615
}
597
616
@@ -903,6 +922,7 @@ static class KnownObjects {
903
922
private final Set <String > idsInCreation = new HashSet <>();
904
923
905
924
private final Map <String , Integer > processedRelationships = new HashMap <>();
925
+ private final Map <String , Set <Map <String , Object >>> mappedQueryResults = new HashMap <>();
906
926
907
927
private void storeObject (@ Nullable String internalId , Object object ) {
908
928
if (internalId == null ) {
@@ -1029,5 +1049,24 @@ private void nextRecord() {
1029
1049
previousRecords .addAll (internalCurrentRecord .keySet ());
1030
1050
internalCurrentRecord .clear ();
1031
1051
}
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
+ }
1032
1071
}
1033
1072
}
0 commit comments