Skip to content

Commit 2e9b9ed

Browse files
GH-903 - Increment only version properties of changed relationship entities.
The fix is to use an increment of 0 in the statements handling existing relationship entities. While I tried at first to exclude to corresponding builders in `org.neo4j.ogm.context.EntityGraphMapper#getRelationshipBuilder` (and with them the edges and eventually the statements) from the list of statements to execute, we didn’t succeed with that. The `PizzaIntegrationTest`, more specifically, the `shouldBeAbleToSaveAndRetrieveFullyLoadedPizza` became flaky with that. I have no clue what the actual tests meaning is, but I assume the underlying cause is the fact the the context won’t get updated with all the affected entities (the test uses a couple of different sessions). That means: A fully hydrated and changed object is updated in one session, which may or may not have the information about the content of the local graph. When the statements of unchanged entities are excluded, they won’t never be returned from the datatbase, hence not make their way into the session cache and therefore will never update the update in question. This fixes #903.
1 parent 17b37f1 commit 2e9b9ed

File tree

12 files changed

+349
-23
lines changed

12 files changed

+349
-23
lines changed

core/src/main/java/org/neo4j/ogm/context/EntityGraphMapper.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,7 @@ public CompileContext map(Object entity, int horizon) {
152152
DirectedRelationship directedRelationship = new DirectedRelationship(relationshipType,
153153
Direction.OUTGOING);
154154

155-
RelationshipBuilder relationshipBuilder = getRelationshipBuilder(compiler, entity, directedRelationship,
156-
false);
155+
RelationshipBuilder relationshipBuilder = getRelationshipBuilder(compiler, entity, directedRelationship, mappingContext.isDirty(entity));
157156

158157
// 2. create or update the actual relationship (edge) in the graph
159158
updateRelationshipEntity(compiler.context(), entity, relationshipBuilder, reInfo);
@@ -573,7 +572,7 @@ private RelationshipBuilder getRelationshipBuilder(Compiler cypherBuilder, Objec
573572
EntityUtils.setIdentity(entity, null, metaData);
574573
}
575574
} else {
576-
relationshipBuilder = cypherBuilder.existingRelationship(relId, directedRelationship.direction(), directedRelationship.type());
575+
relationshipBuilder = cypherBuilder.existingRelationship(relId, directedRelationship.direction(), directedRelationship.type(), mappingContext.isDirty(entity));
577576

578577
this.mappingContext.getSnapshotOf(entity).ifPresent(snapshot ->
579578
relationshipBuilder
@@ -753,7 +752,7 @@ private <T> void updateVersionField(Object entity, PropertyContainerBuilder<T> b
753752

754753
if (version == null) {
755754
version = 0L;
756-
} else {
755+
} else if (mappingContext.isDirty(entity)) {
757756
version = version + 1;
758757
}
759758
fieldInfo.writeDirect(entity, version);

core/src/main/java/org/neo4j/ogm/cypher/compiler/Compiler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public interface Compiler {
7171
* @param direction The direction of the existing relationship
7272
* @return A new {@link RelationshipBuilder} bound to the identified relationship entity
7373
*/
74-
RelationshipBuilder existingRelationship(Long existingRelationshipId, Relationship.Direction direction, String type);
74+
RelationshipBuilder existingRelationship(Long existingRelationshipId, Relationship.Direction direction, String type, boolean wasDirty);
7575

7676
/**
7777
* Defines a relationship deletion between the specified start node to end node with the given relationship type and direction.

core/src/main/java/org/neo4j/ogm/cypher/compiler/MultiStatementCypherCompiler.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,11 @@ public NodeBuilder existingNode(Long existingNodeId) {
9393
}
9494

9595
@Override
96-
public RelationshipBuilder existingRelationship(Long existingRelationshipId, Relationship.Direction direction, String type) {
96+
public RelationshipBuilder existingRelationship(Long existingRelationshipId, Relationship.Direction direction, String type, boolean wasDirty) {
9797
String key = existingRelationshipId + ";" + direction.name();
98-
return existingRelationshipBuilders.computeIfAbsent(key, k -> new DefaultRelationshipBuilder(type, existingRelationshipId));
98+
RelationshipBuilder relationshipBuilder = existingRelationshipBuilders.computeIfAbsent(key, k -> new DefaultRelationshipBuilder(type, existingRelationshipId));
99+
relationshipBuilder.setDirty(wasDirty);
100+
return relationshipBuilder;
99101
}
100102

101103
@Override
@@ -199,15 +201,21 @@ public List<Statement> updateRelationshipStatements() {
199201
return Collections.emptyList();
200202
}
201203

202-
Set<Edge> relationships = new HashSet<>(existingRelationshipBuilders.size());
203-
List<Statement> statements = new ArrayList<>(existingRelationshipBuilders.size());
204-
for (RelationshipBuilder relBuilder : existingRelationshipBuilders.values()) {
205-
relationships.add(relBuilder.edge());
204+
Map<Boolean, Set<Edge>> collect = existingRelationshipBuilders.values().stream()
205+
.collect(partitioningBy(RelationshipBuilder::isDirty, Collectors.mapping(RelationshipBuilder::edge, Collectors.toSet())));
206+
207+
List<Statement> result = new ArrayList<>();
208+
if (!collect.get(true).isEmpty()) {
209+
ExistingRelationshipStatementBuilder builder = new ExistingRelationshipStatementBuilder(collect.get(true), statementFactory, true);
210+
result.add(builder.build());
206211
}
207-
ExistingRelationshipStatementBuilder existingRelationshipBuilder = new ExistingRelationshipStatementBuilder(
208-
relationships, statementFactory);
209-
statements.add(existingRelationshipBuilder.build());
210-
return statements;
212+
213+
if (!collect.get(false).isEmpty()) {
214+
ExistingRelationshipStatementBuilder builder = new ExistingRelationshipStatementBuilder(collect.get(false), statementFactory, false);
215+
result.add(builder.build());
216+
}
217+
218+
return result;
211219
}
212220

213221
@Override

core/src/main/java/org/neo4j/ogm/cypher/compiler/RelationshipBuilder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ public interface RelationshipBuilder extends PropertyContainerBuilder<Relationsh
5353
void setRelationshipEntity(boolean relationshipEntity);
5454

5555
boolean isNew();
56+
boolean isDirty();
57+
58+
void setDirty(boolean dirty);
5659

5760
Edge edge();
5861

core/src/main/java/org/neo4j/ogm/cypher/compiler/builders/node/DefaultRelationshipBuilder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class DefaultRelationshipBuilder extends AbstractPropertyContainerBuilder
3838
private boolean singleton = true; // will be false if the relationship can be mapped multiple times between two instances
3939
private boolean bidirectional = false;
4040
private boolean relationshipEntity = false;
41+
private boolean dirty = true;
4142

4243
public DefaultRelationshipBuilder(String type, boolean bidirectional) {
4344
this(type, null);
@@ -138,4 +139,14 @@ public RelationshipBuilder setVersionProperty(String name, Long version) {
138139
super.targetContainer.setVersion(new PropertyModel<>(name, version));
139140
return this;
140141
}
142+
143+
@Override
144+
public boolean isDirty() {
145+
return dirty;
146+
}
147+
148+
@Override
149+
public void setDirty(boolean dirty) {
150+
this.dirty = dirty;
151+
}
141152
}

core/src/main/java/org/neo4j/ogm/cypher/compiler/builders/statement/ExistingRelationshipStatementBuilder.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ public class ExistingRelationshipStatementBuilder implements CypherStatementBuil
4141

4242
private final Set<Edge> edges;
4343

44-
public ExistingRelationshipStatementBuilder(Set<Edge> edges, StatementFactory statementFactory) {
44+
/**
45+
* Set to {@literal true} if all the edges above are dirty (changed) edges.
46+
*/
47+
private final boolean dirtyEdges;
48+
49+
public ExistingRelationshipStatementBuilder(Set<Edge> edges, StatementFactory statementFactory, boolean dirtyOnes) {
4550
this.edges = edges;
4651
this.statementFactory = statementFactory;
52+
this.dirtyEdges = dirtyOnes;
4753
}
4854

4955
@Override
@@ -56,7 +62,7 @@ public Statement build() {
5662
queryBuilder.append("UNWIND $rows AS row MATCH ()-[r]->() WHERE ID(r) = row.relId ");
5763

5864
if (firstEdge.hasVersionProperty()) {
59-
queryBuilder.append(OptimisticLockingUtils.getFragmentForExistingNodesAndRelationships(firstEdge, "r"));
65+
queryBuilder.append(OptimisticLockingUtils.getFragmentForExistingNodesAndRelationships(firstEdge, "r", dirtyEdges ? 1 : 0));
6066
}
6167

6268
queryBuilder.append(firstEdge.createPropertyRemovalFragment("r"));

core/src/main/java/org/neo4j/ogm/cypher/compiler/builders/statement/OptimisticLockingUtils.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ final class OptimisticLockingUtils {
3838

3939
private final static String VERSION_PROPERTY_CHECK_FOR_EXISTING_NODES_AND_RELATIONSHIPS = ""
4040
+ "AND %1$s.`%2$s` = row.`%2$s` "
41-
+ "SET %1$s.`%2$s` = %1$s.`%2$s` + 1 "
41+
+ "SET %1$s.`%2$s` = %1$s.`%2$s` + %3$d "
4242
+ "WITH %1$s, row "
43-
+ "WHERE %1$s.`%2$s` = row.`%2$s` + 1 ";
43+
+ "WHERE %1$s.`%2$s` = row.`%2$s` + %3$d ";
4444

4545
/**
4646
* In case an entity with an externally assigned ID has also a @Version field, the version check is
@@ -56,14 +56,25 @@ final class OptimisticLockingUtils {
5656
+ "WITH %1$s, row "
5757
+ "WHERE (row.`%2$s` IS NULL OR %1$s.`%2$s` = row.`%2$s` + 1) ";
5858

59+
5960
/**
6061
* @param container node / relationship to check
6162
* @param variable The variable representing the node or relationship to upgrade
62-
* @return
63+
* @return A fragment to be included in a statement to increment the version property
6364
*/
6465
static String getFragmentForExistingNodesAndRelationships(PropertyContainer container, String variable) {
66+
return getFragmentForExistingNodesAndRelationships(container, variable, 1);
67+
}
68+
69+
/**
70+
* @param container node / relationship to check
71+
* @param variable The variable representing the node or relationship to upgrade
72+
* @param increment use {@literal 0} to avoid any increment (for non-dirty relationships)
73+
* @return A fragment to be included in a statement to increment the version property
74+
*/
75+
static String getFragmentForExistingNodesAndRelationships(PropertyContainer container, String variable, int increment) {
6576
String key = container.getVersion().getKey();
66-
return String.format(VERSION_PROPERTY_CHECK_FOR_EXISTING_NODES_AND_RELATIONSHIPS, variable, key);
77+
return String.format(VERSION_PROPERTY_CHECK_FOR_EXISTING_NODES_AND_RELATIONSHIPS, variable, key, increment);
6778
}
6879

6980
/**

neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/examples/locking/NodeOptimisticLockingTest.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.Before;
2828
import org.junit.BeforeClass;
2929
import org.junit.Test;
30+
import org.neo4j.ogm.domain.locking.FriendOf;
3031
import org.neo4j.ogm.domain.locking.Location;
3132
import org.neo4j.ogm.domain.locking.PowerUser;
3233
import org.neo4j.ogm.domain.locking.User;
@@ -232,7 +233,7 @@ public void assertWorkingLocksOnReReadWithChangedRelationships() {
232233
// Re-Read on purpose, see ticket
233234
bert = session2.load(User.class, bertId);
234235
bert.setName("Bert");
235-
bert.addFriend(ernie);
236+
FriendOf friendOf = bert.addFriend(ernie);
236237
session2.save(bert);
237238
assertThat(bert.getVersion()).isEqualTo(2L);
238239

@@ -256,6 +257,19 @@ public void assertWorkingLocksOnReReadWithChangedRelationships() {
256257
.queryResults().iterator().next();
257258
session1.clear();
258259
assertThat(versions).containsEntry("userVersion", 3L);
260+
assertThat(versions).containsEntry("relVersion", 0L);
261+
262+
friendOf.setDescription("an updated description");
263+
264+
session2.save(bert);
265+
assertThat(bert.getVersion()).isEqualTo(3L);
266+
267+
versions = session1.query("MATCH (a:User {name: $name}) -[f:FRIEND_OF] -> (:User) RETURN"
268+
+ " a.version as userVersion, f.version as relVersion, f.description as description", Collections.singletonMap("name", bert.getName()))
269+
.queryResults().iterator().next();
270+
session1.clear();
271+
assertThat(versions).containsEntry("userVersion", 3L);
259272
assertThat(versions).containsEntry("relVersion", 1L);
273+
assertThat(versions).containsEntry("description", "an updated description");
260274
}
261275
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2002-2022 "Neo4j,"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package org.neo4j.ogm.persistence.examples.versioned_rel_entity;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
24+
import org.neo4j.ogm.annotation.Id;
25+
import org.neo4j.ogm.annotation.NodeEntity;
26+
import org.neo4j.ogm.annotation.Relationship;
27+
28+
/**
29+
* @author Michael J. Simons
30+
*/
31+
@NodeEntity
32+
public class A {
33+
34+
@Id
35+
private String id;
36+
37+
@Relationship(value = "R", direction = Relationship.Direction.INCOMING)
38+
private List<R> bs = new ArrayList<>();
39+
40+
public A() {
41+
}
42+
43+
public A(String id) {
44+
this.id = id;
45+
}
46+
47+
public String getId() {
48+
return id;
49+
}
50+
51+
public R add(B b) {
52+
R r = new R(this, b);
53+
this.bs.add(r);
54+
return r;
55+
}
56+
57+
public List<R> getBs() {
58+
return bs;
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return "A{" +
64+
"id='" + id + '\'' +
65+
'}';
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2002-2022 "Neo4j,"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
package org.neo4j.ogm.persistence.examples.versioned_rel_entity;
20+
21+
import org.neo4j.ogm.annotation.GeneratedValue;
22+
import org.neo4j.ogm.annotation.Id;
23+
import org.neo4j.ogm.annotation.NodeEntity;
24+
25+
/**
26+
* @author Michael J. Simons
27+
*/
28+
@NodeEntity
29+
public class B {
30+
31+
@Id @GeneratedValue
32+
private Long id;
33+
34+
private String name;
35+
36+
public B() {
37+
}
38+
39+
public B(String name) {
40+
this.name = name;
41+
}
42+
43+
public Long getId() {
44+
return id;
45+
}
46+
47+
public String getName() {
48+
return name;
49+
}
50+
51+
@Override public String toString() {
52+
return "B{" +
53+
"id=" + id +
54+
", name='" + name + '\'' +
55+
'}';
56+
}
57+
}

0 commit comments

Comments
 (0)