2323import java .util .Optional ;
2424import java .util .Set ;
2525import java .util .concurrent .locks .StampedLock ;
26+ import java .util .stream .Collectors ;
2627
2728import org .apiguardian .api .API ;
2829import org .springframework .lang .NonNull ;
@@ -55,22 +56,17 @@ public enum ProcessState {
5556 */
5657 private final Set <RelationshipDescriptionWithSourceId > processedRelationshipDescriptions = new HashSet <>();
5758
58- /**
59- * The set of already processed related objects.
60- */
61- private final Set <Object > processedObjects = new HashSet <>();
62-
6359 /**
6460 * A map of processed objects pointing towards a possible new instance of themselves.
6561 * This will happen for immutable entities.
6662 */
67- private final Map <Object , Object > processedObjectsAlias = new HashMap <>();
63+ private final Map <Integer , Object > processedObjectsAlias = new HashMap <>();
6864
6965 /**
7066 * A map pointing from a processed object to the internal id.
7167 * This will be useful during the persistence to avoid another DB network round-trip.
7268 */
73- private final Map <Object , Long > processedObjectsIds = new HashMap <>();
69+ private final Map <Integer , Long > processedObjectsIds = new HashMap <>();
7470
7571 public NestedRelationshipProcessingStateMachine (final Neo4jMappingContext mappingContext ) {
7672
@@ -85,8 +81,7 @@ public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappin
8581 Assert .notNull (initialObject , "Initial object must not be null" );
8682 Assert .notNull (internalId , "The initial objects internal ID must not be null" );
8783
88- processedObjects .add (initialObject );
89- processedObjectsIds .put (initialObject , internalId );
84+ storeHashedVersionInProcessedObjectsIds (initialObject , internalId );
9085 }
9186
9287 /**
@@ -172,11 +167,12 @@ public void markRelationshipAsProcessed(Object fromId, @Nullable RelationshipDes
172167 * @param valueToStore If not {@literal null}, all non-null values will be marked as processed
173168 * @param internalId The internal id of the value processed
174169 */
175- public void markValueAsProcessed (Object valueToStore , @ Nullable Long internalId ) {
170+ public void markValueAsProcessed (Object valueToStore , Long internalId ) {
176171
177172 final long stamp = lock .writeLock ();
178173 try {
179174 doMarkValueAsProcessed (valueToStore , internalId );
175+ storeProcessedInAlias (valueToStore , valueToStore );
180176 } finally {
181177 lock .unlock (stamp );
182178 }
@@ -185,12 +181,8 @@ public void markValueAsProcessed(Object valueToStore, @Nullable Long internalId)
185181 private void doMarkValueAsProcessed (Object valueToStore , Long internalId ) {
186182
187183 Object value = extractRelatedValueFromRelationshipProperties (valueToStore );
188- this .processedObjects .add (valueToStore );
189- this .processedObjects .add (value );
190- if (internalId != null ) {
191- this .processedObjectsIds .put (valueToStore , internalId );
192- this .processedObjectsIds .put (value , internalId );
193- }
184+ storeHashedVersionInProcessedObjectsIds (valueToStore , internalId );
185+ storeHashedVersionInProcessedObjectsIds (value , internalId );
194186 }
195187
196188 /**
@@ -204,23 +196,29 @@ public boolean hasProcessedValue(Object value) {
204196 long stamp = lock .readLock ();
205197 try {
206198 Object valueToCheck = extractRelatedValueFromRelationshipProperties (value );
207- boolean processed = processedObjects . contains ( valueToCheck ) || processedObjectsAlias . containsKey (valueToCheck );
199+ boolean processed = hasProcessed (valueToCheck );
208200 // This can be the case the object has been loaded via an additional findXXX call
209201 // We can enforce sets and so on, but this is more user-friendly
210202 Class <?> typeOfValue = valueToCheck .getClass ();
211203 if (!processed && mappingContext .hasPersistentEntityFor (typeOfValue )) {
212204 Neo4jPersistentEntity <?> entity = mappingContext .getRequiredPersistentEntity (typeOfValue );
213205 Neo4jPersistentProperty idProperty = entity .getIdProperty ();
214206 Object id = idProperty == null ? null : entity .getPropertyAccessor (valueToCheck ).getProperty (idProperty );
215- Optional <Object > alreadyProcessedObject = id == null ? Optional .empty () : processedObjects .stream ()
207+ // After the lookup by system.identityHashCode failed for a processed object alias,
208+ // we must traverse or iterate over all value with the matching type and compare the domain ids
209+ // to figure out if the logical object has already been processed through a different object instance.
210+ // The type check is needed to avoid relationship ids <> node id conflicts.
211+ Optional <Object > alreadyProcessedObject = id == null ? Optional .empty () : processedObjectsAlias .values ().stream ()
216212 .filter (typeOfValue ::isInstance )
217213 .filter (processedObject -> id .equals (entity .getPropertyAccessor (processedObject ).getProperty (idProperty )))
218214 .findAny ();
219215 if (alreadyProcessedObject .isPresent ()) { // Skip the show the next time around.
220216 processed = true ;
221- Long internalId = this .getInternalId (alreadyProcessedObject .get ());
222- stamp = lock .tryConvertToWriteLock (stamp );
223- doMarkValueAsProcessed (valueToCheck , internalId );
217+ Long internalId = getInternalId (alreadyProcessedObject .get ());
218+ if (internalId != null ) {
219+ stamp = lock .tryConvertToWriteLock (stamp );
220+ doMarkValueAsProcessed (valueToCheck , internalId );
221+ }
224222 }
225223 }
226224 return processed ;
@@ -250,7 +248,7 @@ public boolean hasProcessedRelationship(Object fromId, @Nullable RelationshipDes
250248 public void markValueAsProcessedAs (Object valueToStore , Object bean ) {
251249 final long stamp = lock .writeLock ();
252250 try {
253- processedObjectsAlias . put (valueToStore , bean );
251+ storeProcessedInAlias (valueToStore , bean );
254252 } finally {
255253 lock .unlock (stamp );
256254 }
@@ -261,8 +259,8 @@ public Long getInternalId(Object object) {
261259 final long stamp = lock .readLock ();
262260 try {
263261 Object valueToCheck = extractRelatedValueFromRelationshipProperties (object );
264- Long possibleId = processedObjectsIds . get (valueToCheck );
265- return possibleId != null ? possibleId : processedObjectsIds . get ( processedObjectsAlias . get (valueToCheck ));
262+ Long possibleId = getProcessedObjectIds (valueToCheck );
263+ return possibleId != null ? possibleId : getProcessedObjectIds ( getProcessedAs (valueToCheck ));
266264 } finally {
267265 lock .unlock (stamp );
268266 }
@@ -273,18 +271,18 @@ public Object getProcessedAs(Object entity) {
273271
274272 final long stamp = lock .readLock ();
275273 try {
276- return processedObjectsAlias . getOrDefault ( entity , entity );
274+ return getProcessedAsWithDefaults ( entity );
277275 } finally {
278276 lock .unlock (stamp );
279277 }
280278 }
281279
282- private boolean hasProcessedAllOf ( @ Nullable Collection <?> valuesToStore ) {
283- // there can be null elements in the unified collection of values to store.
284- if (valuesToStore == null ) {
285- return false ;
280+ @ Nullable
281+ private Long getProcessedObjectIds ( @ Nullable Object entity ) {
282+ if (entity == null ) {
283+ return null ;
286284 }
287- return processedObjects . containsAll ( valuesToStore );
285+ return processedObjectsIds . get ( System . identityHashCode ( entity ) );
288286 }
289287
290288 @ NonNull
@@ -297,4 +295,31 @@ private Object extractRelatedValueFromRelationshipProperties(Object valueToStore
297295 }
298296 return value ;
299297 }
298+
299+ /*
300+ * Convenience wrapper functions to avoid exposing the System.identityHashCode "everywhere" in this class.
301+ */
302+ private void storeHashedVersionInProcessedObjectsIds (Object initialObject , Long internalId ) {
303+ processedObjectsIds .put (System .identityHashCode (initialObject ), internalId );
304+ }
305+
306+ private void storeProcessedInAlias (Object valueToStore , Object bean ) {
307+ processedObjectsAlias .put (System .identityHashCode (valueToStore ), bean );
308+ }
309+
310+ private Object getProcessedAsWithDefaults (Object entity ) {
311+ return processedObjectsAlias .getOrDefault (System .identityHashCode (entity ), entity );
312+ }
313+
314+ private boolean hasProcessed (Object valueToCheck ) {
315+ return processedObjectsAlias .containsKey (System .identityHashCode (valueToCheck ));
316+ }
317+
318+ private boolean hasProcessedAllOf (@ Nullable Collection <?> valuesToStore ) {
319+ // there can be null elements in the unified collection of values to store.
320+ if (valuesToStore == null ) {
321+ return false ;
322+ }
323+ return processedObjectsIds .keySet ().containsAll (valuesToStore .stream ().map (System ::identityHashCode ).collect (Collectors .toList ()));
324+ }
300325}
0 commit comments