Skip to content

Commit 571d3a1

Browse files
committed
GH-2683 - Use identityHashCode in save operations.
Relying on equals/hashCode in the domain entity can lead into situations where a existing object cannot found anymore because its hashCode value has changed. e.g. generated id or version attribute. Using the raw `System.identityHashCode` solves this problem. This code also simplifies the overall usage of caching properties and reduces the storage of object references into one central place. Closes #2683
1 parent 850bc45 commit 571d3a1

File tree

1 file changed

+55
-30
lines changed

1 file changed

+55
-30
lines changed

src/main/java/org/springframework/data/neo4j/core/mapping/NestedRelationshipProcessingStateMachine.java

+55-30
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Optional;
2424
import java.util.Set;
2525
import java.util.concurrent.locks.StampedLock;
26+
import java.util.stream.Collectors;
2627

2728
import org.apiguardian.api.API;
2829
import 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

Comments
 (0)