23
23
import java .util .Optional ;
24
24
import java .util .Set ;
25
25
import java .util .concurrent .locks .StampedLock ;
26
+ import java .util .stream .Collectors ;
26
27
27
28
import org .apiguardian .api .API ;
28
29
import org .springframework .lang .NonNull ;
@@ -55,22 +56,17 @@ public enum ProcessState {
55
56
*/
56
57
private final Set <RelationshipDescriptionWithSourceId > processedRelationshipDescriptions = new HashSet <>();
57
58
58
- /**
59
- * The set of already processed related objects.
60
- */
61
- private final Set <Object > processedObjects = new HashSet <>();
62
-
63
59
/**
64
60
* A map of processed objects pointing towards a possible new instance of themselves.
65
61
* This will happen for immutable entities.
66
62
*/
67
- private final Map <Object , Object > processedObjectsAlias = new HashMap <>();
63
+ private final Map <Integer , Object > processedObjectsAlias = new HashMap <>();
68
64
69
65
/**
70
66
* A map pointing from a processed object to the internal id.
71
67
* This will be useful during the persistence to avoid another DB network round-trip.
72
68
*/
73
- private final Map <Object , Long > processedObjectsIds = new HashMap <>();
69
+ private final Map <Integer , Long > processedObjectsIds = new HashMap <>();
74
70
75
71
public NestedRelationshipProcessingStateMachine (final Neo4jMappingContext mappingContext ) {
76
72
@@ -85,8 +81,7 @@ public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappin
85
81
Assert .notNull (initialObject , "Initial object must not be null." );
86
82
Assert .notNull (internalId , "The initial objects internal ID must not be null." );
87
83
88
- processedObjects .add (initialObject );
89
- processedObjectsIds .put (initialObject , internalId );
84
+ storeHashedVersionInProcessedObjectsIds (initialObject , internalId );
90
85
}
91
86
92
87
/**
@@ -172,11 +167,12 @@ public void markRelationshipAsProcessed(Object fromId, @Nullable RelationshipDes
172
167
* @param valueToStore If not {@literal null}, all non-null values will be marked as processed
173
168
* @param internalId The internal id of the value processed
174
169
*/
175
- public void markValueAsProcessed (Object valueToStore , @ Nullable Long internalId ) {
170
+ public void markValueAsProcessed (Object valueToStore , Long internalId ) {
176
171
177
172
final long stamp = lock .writeLock ();
178
173
try {
179
174
doMarkValueAsProcessed (valueToStore , internalId );
175
+ storeProcessedInAlias (valueToStore , valueToStore );
180
176
} finally {
181
177
lock .unlock (stamp );
182
178
}
@@ -185,12 +181,8 @@ public void markValueAsProcessed(Object valueToStore, @Nullable Long internalId)
185
181
private void doMarkValueAsProcessed (Object valueToStore , Long internalId ) {
186
182
187
183
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 );
194
186
}
195
187
196
188
/**
@@ -204,23 +196,29 @@ public boolean hasProcessedValue(Object value) {
204
196
long stamp = lock .readLock ();
205
197
try {
206
198
Object valueToCheck = extractRelatedValueFromRelationshipProperties (value );
207
- boolean processed = processedObjects . contains ( valueToCheck ) || processedObjectsAlias . containsKey (valueToCheck );
199
+ boolean processed = hasProcessed (valueToCheck );
208
200
// This can be the case the object has been loaded via an additional findXXX call
209
201
// We can enforce sets and so on, but this is more user-friendly
210
202
Class <?> typeOfValue = valueToCheck .getClass ();
211
203
if (!processed && mappingContext .hasPersistentEntityFor (typeOfValue )) {
212
204
Neo4jPersistentEntity <?> entity = mappingContext .getRequiredPersistentEntity (typeOfValue );
213
205
Neo4jPersistentProperty idProperty = entity .getIdProperty ();
214
206
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 ()
216
212
.filter (typeOfValue ::isInstance )
217
213
.filter (processedObject -> id .equals (entity .getPropertyAccessor (processedObject ).getProperty (idProperty )))
218
214
.findAny ();
219
215
if (alreadyProcessedObject .isPresent ()) { // Skip the show the next time around.
220
216
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
+ }
224
222
}
225
223
}
226
224
return processed ;
@@ -250,7 +248,7 @@ public boolean hasProcessedRelationship(Object fromId, @Nullable RelationshipDes
250
248
public void markValueAsProcessedAs (Object valueToStore , Object bean ) {
251
249
final long stamp = lock .writeLock ();
252
250
try {
253
- processedObjectsAlias . put (valueToStore , bean );
251
+ storeProcessedInAlias (valueToStore , bean );
254
252
} finally {
255
253
lock .unlock (stamp );
256
254
}
@@ -261,8 +259,8 @@ public Long getInternalId(Object object) {
261
259
final long stamp = lock .readLock ();
262
260
try {
263
261
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 ));
266
264
} finally {
267
265
lock .unlock (stamp );
268
266
}
@@ -273,18 +271,18 @@ public Object getProcessedAs(Object entity) {
273
271
274
272
final long stamp = lock .readLock ();
275
273
try {
276
- return processedObjectsAlias . getOrDefault ( entity , entity );
274
+ return getProcessedAsWithDefaults ( entity );
277
275
} finally {
278
276
lock .unlock (stamp );
279
277
}
280
278
}
281
279
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 ;
286
284
}
287
- return processedObjects . containsAll ( valuesToStore );
285
+ return processedObjectsIds . get ( System . identityHashCode ( entity ) );
288
286
}
289
287
290
288
@ NonNull
@@ -297,4 +295,31 @@ private Object extractRelatedValueFromRelationshipProperties(Object valueToStore
297
295
}
298
296
return value ;
299
297
}
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
+ }
300
325
}
0 commit comments