This is an automated email from the ASF dual-hosted git repository. ntimofeev pushed a commit to branch STABLE-4.2 in repository https://gitbox.apache.org/repos/asf/cayenne.git
The following commit(s) were added to refs/heads/STABLE-4.2 by this push: new f1291235d CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update f1291235d is described below commit f1291235d2dd40b2b47ceb141772e256a5b9da26 Author: Nikita Timofeev <stari...@gmail.com> AuthorDate: Tue Aug 13 17:26:08 2024 +0400 CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update --- RELEASE-NOTES.txt | 10 ++++ .../org/apache/cayenne/access/DataRowStore.java | 2 +- .../access/flush/DefaultDataDomainFlushAction.java | 48 +++++++++++++++-- .../org/apache/cayenne/CircularDependencyIT.java | 61 +++++++++++++++++++++- 4 files changed, 115 insertions(+), 6 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 6446db64a..36dd5af24 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -7,6 +7,16 @@ https://cayenne.apache.org/ To browse individual bug reports check out project issue tracker: https://issues.apache.org/jira/browse/CAY +---------------------------------- +Release: 4.2.2 +Date: +---------------------------------- + +Bug Fixes: + +CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update + + ---------------------------------- Release: 4.2.1 Date: diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java index f2fe18ad2..e730b6c98 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowStore.java @@ -336,7 +336,7 @@ public class DataRowStore implements Serializable { if (deletedSnapshotIds.isEmpty() && invalidatedSnapshotIds.isEmpty() && updatedSnapshots.isEmpty() - && indirectlyModifiedIds.isEmpty()) { + && (indirectlyModifiedIds == null || indirectlyModifiedIds.isEmpty())) { logger.warn("postSnapshotsChangeEvent.. bogus call... no changes."); return; } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java index 3d99e797f..d67ed20be 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java @@ -41,6 +41,8 @@ import org.apache.cayenne.access.flush.operation.DbRowOpMerger; import org.apache.cayenne.access.flush.operation.DbRowOpSorter; import org.apache.cayenne.access.flush.operation.DbRowOp; import org.apache.cayenne.access.flush.operation.DbRowOpVisitor; +import org.apache.cayenne.access.flush.operation.DeleteDbRowOp; +import org.apache.cayenne.access.flush.operation.InsertDbRowOp; import org.apache.cayenne.access.flush.operation.OpIdFactory; import org.apache.cayenne.access.flush.operation.UpdateDbRowOp; import org.apache.cayenne.graph.CompoundDiff; @@ -89,7 +91,9 @@ public class DefaultDataDomainFlushAction implements DataDomainFlushAction { List<? extends Query> queries = createQueries(sortedOps); executeQueries(queries); createReplacementIds(objectStore, afterCommitDiff, sortedOps); - postprocess(context, objectStoreGraphDiff, afterCommitDiff, sortedOps); + // note: we are using here not filtered operations, but the original ones, + // as we need them all for the postprocessing + postprocess(context, objectStoreGraphDiff, afterCommitDiff, deduplicatedOps); return afterCommitDiff; } @@ -139,8 +143,25 @@ public class DefaultDataDomainFlushAction implements DataDomainFlushAction { } protected List<DbRowOp> filterOps(List<DbRowOp> dbRowOps) { - // clear phantom update (this can be from insert/delete of arc with transient object) - dbRowOps.forEach(row -> row.accept(PhantomDbRowOpCleaner.INSTANCE)); + List<DbRowOp> opsToRemove = null; + for (DbRowOp row : dbRowOps) { + // clear phantom update (this can be from insert/delete of arc with transient object) + row.accept(PhantomDbRowOpCleaner.INSTANCE); + // check for an empty update that we may need to filter out + if (row.accept(EmptyRowChecker.INSTANCE)) { + if (opsToRemove == null) { + opsToRemove = new ArrayList<>(); + } + opsToRemove.add(row); + } + } + + if(opsToRemove != null && !opsToRemove.isEmpty()) { + // make a duplicate, to keep original operations intact + dbRowOps = new ArrayList<>(dbRowOps); + dbRowOps.removeAll(opsToRemove); + } + return dbRowOps; } @@ -224,11 +245,30 @@ public class DefaultDataDomainFlushAction implements DataDomainFlushAction { @Override public Void visitUpdate(UpdateDbRowOp dbRow) { - // if(dbRow.getChangeId().isTemporary() && !dbRow.getChangeId().isReplacementIdAttached()) { dbRow.getValues().clear(); } return null; } } + + protected static class EmptyRowChecker implements DbRowOpVisitor<Boolean> { + + protected static final EmptyRowChecker INSTANCE = new EmptyRowChecker(); + + @Override + public Boolean visitDelete(DeleteDbRowOp dbRow) { + return false; + } + + @Override + public Boolean visitInsert(InsertDbRowOp dbRow) { + return false; + } + + @Override + public Boolean visitUpdate(UpdateDbRowOp dbRow) { + return dbRow.getValues().isEmpty(); + } + } } diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java index 6cfc68f7c..1a7923b51 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/CircularDependencyIT.java @@ -20,13 +20,20 @@ package org.apache.cayenne; import org.apache.cayenne.di.Inject; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; import org.apache.cayenne.testdo.relationships.E1; import org.apache.cayenne.testdo.relationships.E2; +import org.apache.cayenne.testdo.relationships.ReflexiveAndToOne; import org.apache.cayenne.unit.di.server.CayenneProjects; import org.apache.cayenne.unit.di.server.ServerCase; import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.After; import org.junit.Test; +import java.sql.SQLException; +import java.sql.Types; + import static org.junit.Assert.*; @UseServerRuntime(CayenneProjects.RELATIONSHIPS_PROJECT) @@ -34,7 +41,28 @@ public class CircularDependencyIT extends ServerCase { @Inject private ObjectContext context; - + + @Inject + private DBHelper dbHelper; + + @After + public void cleanUp() throws SQLException { + // manually cleanup circular references + TableHelper e1 = new TableHelper(dbHelper, "CYCLE_E1", "id", "e2_id", "text"); + e1.setColumnTypes(Types.INTEGER, Types.INTEGER, Types.VARCHAR); + TableHelper e2 = new TableHelper(dbHelper, "CYCLE_E2", "id", "e1_id", "text"); + e2.setColumnTypes(Types.INTEGER, Types.INTEGER, Types.VARCHAR); + TableHelper reflexive = new TableHelper(dbHelper, "REFLEXIVE_AND_TO_ONE", "REFLEXIVE_AND_TO_ONE_ID", "NAME", "PARENT_ID"); + + e1.update().set("e2_id", null, Types.INTEGER).execute(); + e2.update().set("e1_id", null, Types.INTEGER).execute(); + e1.deleteAll(); + e2.deleteAll(); + + reflexive.update().set("PARENT_ID", null, Types.INTEGER).execute(); + reflexive.deleteAll(); + } + @Test() public void testCycle() { E1 e1 = context.newObject(E1.class); @@ -55,4 +83,35 @@ public class CircularDependencyIT extends ServerCase { } } + + @Test + public void testUpdate() { + E1 e1 = context.newObject(E1.class); + E2 e2 = context.newObject(E2.class); + + e1.setText("e1 #" + 1); + e2.setText("e2 #" + 2); + context.commitChanges(); + + e1.setE2(e2); + context.commitChanges(); + + e2.setE1(e1); + context.commitChanges(); + } + + @Test + public void testUpdateSelfRelationship() { + ReflexiveAndToOne e1 = context.newObject(ReflexiveAndToOne.class); + ReflexiveAndToOne e2 = context.newObject(ReflexiveAndToOne.class); + + e1.setName("e1 #" + 1); + e2.setName("e2 #" + 2); + + e1.setToParent(e2); + context.commitChanges(); + + e2.setToParent(e1); + context.commitChanges(); + } }