@@ -101,6 +101,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
101
101
@ Override
102
102
public <R > R read (Class <R > targetType , MapAccessor mapAccessor ) {
103
103
104
+ knownObjects .nextRecord ();
104
105
@ SuppressWarnings ("unchecked" ) // ¯\_(ツ)_/¯
105
106
Neo4jPersistentEntity <R > rootNodeDescription = (Neo4jPersistentEntity <R >) nodeDescriptionStore .getNodeDescription (targetType );
106
107
MapAccessor queryRoot = determineQueryRoot (mapAccessor , rootNodeDescription );
@@ -274,30 +275,14 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
274
275
Neo4jPersistentEntity <ET > concreteNodeDescription = (Neo4jPersistentEntity <ET >) nodeDescriptionAndLabels
275
276
.getNodeDescription ();
276
277
277
- boolean isKotlinType = KotlinDetector .isKotlinType (concreteNodeDescription .getType ());
278
278
ET instance = instantiate (concreteNodeDescription , queryResult ,
279
279
nodeDescriptionAndLabels .getDynamicLabels (), lastMappedEntity , relationshipsFromResult , nodesFromResult );
280
280
281
281
knownObjects .removeFromInCreation (internalId );
282
- PersistentPropertyAccessor <ET > propertyAccessor = concreteNodeDescription .getPropertyAccessor (instance );
283
282
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 );
301
286
ET bean = propertyAccessor .getBean ();
302
287
303
288
// save final state of the bean
@@ -310,10 +295,58 @@ private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescr
310
295
if (mappedObject == null ) {
311
296
mappedObject = mappedObjectSupplier .get ();
312
297
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 );
313
308
}
314
309
return mappedObject ;
315
310
}
316
311
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
+
317
350
@ Nullable
318
351
private Long getInternalId (@ NonNull MapAccessor queryResult ) {
319
352
return queryResult instanceof Node
@@ -405,8 +438,9 @@ public <T> T getParameterValue(PreferredConstructor.Parameter<T, Neo4jPersistent
405
438
}
406
439
407
440
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
+
410
444
return property -> {
411
445
if (isConstructorParameter .test (property )) {
412
446
return ;
@@ -428,20 +462,55 @@ private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryR
428
462
};
429
463
}
430
464
465
+ @ Nullable
431
466
private static Object getValueOrDefault (boolean ownerIsKotlinType , Class <?> rawType , @ Nullable Object value ) {
432
467
433
468
return value == null && !ownerIsKotlinType && rawType .isPrimitive () ? ReflectionUtils .getPrimitiveDefault (rawType ) : value ;
434
469
}
435
470
436
471
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
+
438
475
return association -> {
439
476
440
477
Neo4jPersistentProperty persistentProperty = association .getInverse ();
478
+
441
479
if (isConstructorParameter .test (persistentProperty )) {
442
480
return ;
443
481
}
444
482
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
+
445
514
createInstanceOfRelationships (persistentProperty , queryResult , (RelationshipDescription ) association , relationshipsFromResult , nodesFromResult )
446
515
.ifPresent (value -> propertyAccessor .setProperty (persistentProperty , value ));
447
516
};
@@ -662,6 +731,7 @@ static class KnownObjects {
662
731
private final Lock write = lock .writeLock ();
663
732
664
733
private final Map <Long , Object > internalIdStore = new HashMap <>();
734
+ private final Map <Long , Boolean > internalNextRecord = new HashMap <>();
665
735
private final Set <Long > idsInCreation = new HashSet <>();
666
736
667
737
private void storeObject (@ Nullable Long internalId , Object object ) {
@@ -672,6 +742,7 @@ private void storeObject(@Nullable Long internalId, Object object) {
672
742
write .lock ();
673
743
idsInCreation .remove (internalId );
674
744
internalIdStore .put (internalId , object );
745
+ internalNextRecord .put (internalId , false );
675
746
} finally {
676
747
write .unlock ();
677
748
}
@@ -733,5 +804,32 @@ private void removeFromInCreation(@Nullable Long internalId) {
733
804
write .unlock ();
734
805
}
735
806
}
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
+ }
736
834
}
737
835
}
0 commit comments