This is an automated email from the ASF dual-hosted git repository. ntimofeev pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
The following commit(s) were added to refs/heads/master by this push: new 3c2512600 CAY-2867 Tweak GraphBasedDbRowOpSorter logic to allow related updates 3c2512600 is described below commit 3c25126008ee01f01fccbcb1c29b5f93cb3e527d Author: Nikita Timofeev <stari...@gmail.com> AuthorDate: Thu Aug 15 16:54:03 2024 +0400 CAY-2867 Tweak GraphBasedDbRowOpSorter logic to allow related updates --- RELEASE-NOTES.txt | 1 + .../flush/operation/GraphBasedDbRowOpSorter.java | 59 +++++-- ...tyWithMeaningfulPKAndCustomDbRowOpSorterIT.java | 24 +++ .../DataContextEntityWithMeaningfulPKIT.java | 23 ++- .../operation/GraphBasedDbRowOpSorterTest.java | 181 +++++++++++++++++++++ 5 files changed, 275 insertions(+), 13 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b7f985f28..3a5031e99 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -78,6 +78,7 @@ CAY-2857 Java 22 support CAY-2858 Redesign Collection and Map Property API CAY-2862 Cleanup and upgrade Maven plugins dependencies CAY-2865 Upgrade test dependencies +CAY-2867 Tweak GraphBasedDbRowOpSorter logic to allow related updates Bug Fixes: diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java index 8ed961773..7818afd1e 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java @@ -35,7 +35,9 @@ import org.apache.cayenne.di.Inject; import org.apache.cayenne.di.Provider; import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.graph.GraphManager; +import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.DbJoin; import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.query.ObjectIdQuery; @@ -125,7 +127,7 @@ public class GraphBasedDbRowOpSorter implements DbRowOpSorter { // get graph edges for reflexive relationships DbRowOpType opType = op.accept(rowOpTypeVisitor); relationships.getOrDefault(op.getEntity(), Collections.emptyList()).forEach(relationship -> - getParentsOpId(op, relationship).forEach(parentOpId -> + getParentsOpId(op, relationship).forEach((parentOpId, snapshot) -> indexByDbId.getOrDefault(parentOpId, Collections.emptyList()).forEach(parentOp -> { if(op == parentOp) { return; @@ -141,10 +143,25 @@ public class GraphBasedDbRowOpSorter implements DbRowOpSorter { } break; case UPDATE: - if(parentOpType != DbRowOpType.DELETE) { - graph.add(op, parentOp); - } else { - graph.add(parentOp, op); + switch (parentOpType) { + case INSERT: + graph.add(op, parentOp); + break; + case DELETE: + graph.add(parentOp, op); + break; + case UPDATE: + // Update will only depend on the operations where FK could be affected + Boolean fkReferenceUpdated = parentOp.accept(new FkUpdateChecker(relationship)); + if (fkReferenceUpdated == Boolean.TRUE) { + // TODO: this sorting logic works only for the setting relationship to null case + // if the meaningful PK is updated we got more places to cover, before fixing this logic + // namely: + // - no update would be generated for any dependent entities for the master PK update + // - update would fail with constraint violation even if it is generated + graph.add(parentOp, op); + } + break; } break; case DELETE: @@ -170,21 +187,21 @@ public class GraphBasedDbRowOpSorter implements DbRowOpSorter { }); } - private List<EffectiveOpId> getParentsOpId(DbRowOp op, DbRelationship relationship) { + private Map<EffectiveOpId, Map<String, Object>> getParentsOpId(DbRowOp op, DbRelationship relationship) { List<Map<String, Object>> parentIdSnapshots = op.accept(new DbRowOpSnapshotVisitor(relationship)); if(parentIdSnapshots.size() == 1) { EffectiveOpId id = effectiveIdFor(relationship, parentIdSnapshots.get(0)); if(id != null) { - return Collections.singletonList(id); + return Collections.singletonMap(id, parentIdSnapshots.get(0)); } else { - return Collections.emptyList(); + return Collections.emptyMap(); } } else { - List<EffectiveOpId> effectiveOpIds = new ArrayList<>(parentIdSnapshots.size()); + Map<EffectiveOpId, Map<String, Object>> effectiveOpIds = new HashMap<>(parentIdSnapshots.size()); parentIdSnapshots.forEach(snapshot -> { EffectiveOpId id = this.effectiveIdFor(relationship, snapshot); if(id != null) { - effectiveOpIds.add(id); + effectiveOpIds.put(id, snapshot); } }); return effectiveOpIds; @@ -258,7 +275,7 @@ public class GraphBasedDbRowOpSorter implements DbRowOpSorter { QueryResponse response = object.getObjectContext().getChannel().onQuery(null, query); @SuppressWarnings("unchecked") List<DataRow> result = (List<DataRow>) response.firstList(); - if (result == null || result.size() == 0) { + if (result == null || result.isEmpty()) { return Collections.emptyMap(); } @@ -311,4 +328,24 @@ public class GraphBasedDbRowOpSorter implements DbRowOpSorter { return DbRowOpType.UPDATE; } } + + private static class FkUpdateChecker implements DbRowOpVisitor<Boolean> { + private final DbRelationship relationship; + + public FkUpdateChecker(DbRelationship relationship) { + this.relationship = relationship; + } + + @Override + public Boolean visitUpdate(UpdateDbRowOp dbRow) { + for(DbJoin join : relationship.getJoins()) { + for (DbAttribute attribute : dbRow.getValues().getUpdatedAttributes()) { + if (attribute.getName().equals(join.getTargetName())) { + return true; + } + } + } + return false; + } + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKAndCustomDbRowOpSorterIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKAndCustomDbRowOpSorterIT.java index 09bfeaf34..fbc17e995 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKAndCustomDbRowOpSorterIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKAndCustomDbRowOpSorterIT.java @@ -76,4 +76,28 @@ public class DataContextEntityWithMeaningfulPKAndCustomDbRowOpSorterIT extends R context.commitChanges(); } + @Test + public void test_MeaningfulPkWithFkUpdate() { + // setup + MeaningfulPKTest1 obj = context.newObject(MeaningfulPKTest1.class); + obj.setPkAttribute(1001); + obj.setDescr("aaa"); + context.commitChanges(); + + MeaningfulPKDep dep = context.newObject(MeaningfulPKDep.class); + dep.setToMeaningfulPK(obj); + dep.setPk(10); + context.commitChanges(); + + // check that operations are sorted correctly + dep.setToMeaningfulPK(null); + obj.setPkAttribute(1002); + context.commitChanges(); + + // set relationship with a new PK + dep.setDescr("test"); + dep.setToMeaningfulPK(obj); + context.commitChanges(); + } + } diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java index 0bffbbe29..76f149fc9 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java @@ -20,6 +20,7 @@ package org.apache.cayenne.access; import org.apache.cayenne.Cayenne; +import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.ObjectId; @@ -39,7 +40,6 @@ import org.junit.Ignore; import org.junit.Test; import java.util.List; -import java.util.Map; import static org.junit.Assert.*; @@ -78,7 +78,7 @@ public class DataContextEntityWithMeaningfulPKIT extends RuntimeCase { int id = Cayenne.intPKForObject(obj); - Map snapshot = context.getObjectStore().getDataRowCache().getCachedSnapshot(obj.getObjectId()); + DataRow snapshot = context.getObjectStore().getDataRowCache().getCachedSnapshot(obj.getObjectId()); assertNotNull(snapshot); assertTrue(snapshot.containsKey(MeaningfulPKTest1.PK_ATTRIBUTE_PK_COLUMN)); assertEquals(id, snapshot.get(MeaningfulPKTest1.PK_ATTRIBUTE_PK_COLUMN)); @@ -250,4 +250,23 @@ public class DataContextEntityWithMeaningfulPKIT extends RuntimeCase { assertNotNull(depChild.getMeaningfulPk()); assertNull(depChild.getMeaningfulPk().getPk()); } + + @Test(expected = CayenneRuntimeException.class) + public void test_MeaningfulPkWithFkUpdate() { + // setup + MeaningfulPKTest1 obj = context.newObject(MeaningfulPKTest1.class); + obj.setPkAttribute(1001); + obj.setDescr("aaa"); + context.commitChanges(); + + MeaningfulPKDep dep = context.newObject(MeaningfulPKDep.class); + dep.setToMeaningfulPK(obj); + dep.setPk(10); + context.commitChanges(); + + // this would fail as the DefaultDbRowOpSorter unable to deal with the meaningful PK update + dep.setToMeaningfulPK(null); + obj.setPkAttribute(1002); + context.commitChanges(); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorterTest.java b/cayenne/src/test/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorterTest.java new file mode 100644 index 000000000..7b1a0f192 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorterTest.java @@ -0,0 +1,181 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ + +package org.apache.cayenne.access.flush.operation; + +import org.apache.cayenne.ObjectId; +import org.apache.cayenne.PersistenceState; +import org.apache.cayenne.Persistent; +import org.apache.cayenne.access.DataDomain; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.EntitySorter; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; + +public class GraphBasedDbRowOpSorterTest { + private EntitySorter entitySorter; + private DbRowOpSorter sorter; + + @Before + public void createSorter() { + entitySorter = mock(EntitySorter.class); + EntityResolver entityResolver = mock(EntityResolver.class); + + when(entitySorter.getDbEntityComparator()) + .thenReturn(Comparator.comparing(DbEntity::getName)); + when(entitySorter.isReflexive(argThat(ent -> ent.getName().equals("reflexive")))) + .thenReturn(true); + + DataDomain dataDomain = mock(DataDomain.class); + when(dataDomain.getEntitySorter()).thenReturn(entitySorter); + when(dataDomain.getEntityResolver()).thenReturn(entityResolver); + + sorter = new GraphBasedDbRowOpSorter(() -> dataDomain); + } + + @Test + public void sortEmptyList() { + List<DbRowOp> rows = new ArrayList<>(); + List<DbRowOp> sorted = sorter.sort(rows); + assertTrue(sorted.isEmpty()); + } + + @Test + public void sortByOpEntity() { + ObjectId id1 = ObjectId.of("test4", "id", 1); + ObjectId id2 = ObjectId.of("test2", "id", 2); + ObjectId id3 = ObjectId.of("test3", "id", 3); + ObjectId id4 = ObjectId.of("test1", "id", 4); + + DbRowOp op1 = new InsertDbRowOp(mockObject(id1), mockEntity("test4"), id1); + DbRowOp op2 = new InsertDbRowOp(mockObject(id2), mockEntity("test2"), id2); + DbRowOp op3 = new InsertDbRowOp(mockObject(id3), mockEntity("test3"), id3); + DbRowOp op4 = new InsertDbRowOp(mockObject(id4), mockEntity("test1"), id4); + + List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4); + List<DbRowOp> expected = Arrays.asList(op1, op2, op3, op4); + + List<DbRowOp> sorted = sorter.sort(rows); + assertEquals(expected, sorted); + } + + @Test + public void sortById() { + ObjectId id1 = ObjectId.of("test", "id", 1); + ObjectId id2 = ObjectId.of("test", "id", 2); + ObjectId id3 = ObjectId.of("test", "id", 2); + ObjectId id4 = ObjectId.of("test", "id", 3); + + DbEntity test = mockEntity("test"); + InsertDbRowOp op1 = new InsertDbRowOp(mockObject(id1), test, id1); + InsertDbRowOp op2 = new InsertDbRowOp(mockObject(id2), test, id2); + DeleteDbRowOp op3 = new DeleteDbRowOp(mockObject(id3), test, id3); + DeleteDbRowOp op4 = new DeleteDbRowOp(mockObject(id4), test, id4); + + List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4); + List<DbRowOp> expected = Arrays.asList(op3, op1, op2, op4); + + List<DbRowOp> sorted = sorter.sort(rows); + assertEquals(expected, sorted); + } + + @Test + public void sortByIdDifferentEntities() { + ObjectId id1 = ObjectId.of("test2", "id", 1); + ObjectId id2 = ObjectId.of("test", "id", 2); + ObjectId id3 = ObjectId.of("test", "id", 2); + ObjectId id4 = ObjectId.of("test", "id", 3); + ObjectId id5 = ObjectId.of("test2", "id", 4); + ObjectId id6 = ObjectId.of("test", "id", 5); + ObjectId id7 = ObjectId.of("test", "id", 8); + ObjectId id8 = ObjectId.of("test2", "id", 7); + ObjectId id9 = ObjectId.of("test2", "id", 4); + ObjectId id10 = ObjectId.of("test", "id", 8); + + DbEntity test = mockEntity("test"); + DbEntity test2 = mockEntity("test2"); + BaseDbRowOp[] op = new BaseDbRowOp[10]; + op[0] = new InsertDbRowOp(mockObject(id1), test2, id1); + op[1] = new InsertDbRowOp(mockObject(id2), test, id2); + op[2] = new DeleteDbRowOp(mockObject(id3), test, id3); + op[3] = new UpdateDbRowOp(mockObject(id4), test, id4); + op[4] = new InsertDbRowOp(mockObject(id5), test2, id5); + op[5] = new DeleteDbRowOp(mockObject(id6), test, id6); + op[6] = new InsertDbRowOp(mockObject(id7), test, id7); + op[7] = new UpdateDbRowOp(mockObject(id8), test2, id8); + op[8] = new DeleteDbRowOp(mockObject(id9), test2, id9); + op[9] = new DeleteDbRowOp(mockObject(id10), test, id10); + + List<DbRowOp> expected = Arrays.asList(op[2], op[8], op[9], op[0], op[1], op[3], op[4], op[5], op[6], op[7]); + List<DbRowOp> sorted = sorter.sort(Arrays.asList(op)); + + assertEquals(expected, sorted); + } + + @Test + public void sortReflexive() { + ObjectId id1 = ObjectId.of("reflexive", "id", 1); + ObjectId id2 = ObjectId.of("reflexive", "id", 2); + ObjectId id3 = ObjectId.of("reflexive", "id", 3); + ObjectId id4 = ObjectId.of("reflexive", "id", 4); + + DbEntity reflexive = mockEntity("reflexive"); + DbRowOp op1 = new InsertDbRowOp(mockObject(id1), reflexive, id1); + DbRowOp op2 = new InsertDbRowOp(mockObject(id2), reflexive, id2); + DbRowOp op3 = new InsertDbRowOp(mockObject(id3), reflexive, id3); + DbRowOp op4 = new InsertDbRowOp(mockObject(id4), reflexive, id4); + + List<DbRowOp> rows = Arrays.asList(op1, op2, op3, op4); + List<DbRowOp> expected = Arrays.asList(op1, op2, op3, op4); + + List<DbRowOp> sorted = sorter.sort(rows); + assertEquals(expected, sorted); // no actual sorting is done + verifyNoInteractions(entitySorter); + } + + private Persistent mockObject(ObjectId id) { + Persistent persistent = mock(Persistent.class); + when(persistent.getObjectId()).thenReturn(id); + when(persistent.getPersistenceState()).thenReturn(PersistenceState.MODIFIED); + return persistent; + } + + private DbEntity mockEntity(String name) { + DbAttribute attribute1 = new DbAttribute("id"); + attribute1.setPrimaryKey(true); + DbAttribute attribute2 = new DbAttribute("attr"); + DbEntity testEntity = new DbEntity(name); + testEntity.addAttribute(attribute1); + testEntity.addAttribute(attribute2); + return testEntity; + } +} \ No newline at end of file