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 1ce7e8b7f CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update 1ce7e8b7f is described below commit 1ce7e8b7fd72b0a54ac8286fa2a6ad87a99b1410 Author: Nikita Timofeev <stari...@gmail.com> AuthorDate: Tue Aug 13 15:04:33 2024 +0400 CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update --- RELEASE-NOTES.txt | 1 + .../org/apache/cayenne/access/DataRowStore.java | 2 +- .../access/flush/DefaultDataDomainFlushAction.java | 48 ++++++++++++++++++++-- .../org/apache/cayenne/CircularDependencyIT.java | 31 ++++++++++++++ 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index ce24b821f..b7f985f28 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -124,3 +124,4 @@ CAY-2850 Query using Clob comparison with empty String fails CAY-2851 Replace Existing OneToOne From New Object CAY-2853 Incorrect deletion of entities from flattened attributes CAY-2854 Improve delete prevention detection of flattened attribute row +CAY-2866 DefaultDataDomainFlushAction breaks on circular relationship update diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java b/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java index a8cfbcb3f..a7545ba2b 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/DataRowStore.java @@ -334,7 +334,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/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java index 3d99e797f..d67ed20be 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java +++ b/cayenne/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/src/test/java/org/apache/cayenne/CircularDependencyIT.java b/cayenne/src/test/java/org/apache/cayenne/CircularDependencyIT.java index f4ae2c5b8..f65cf7e4e 100644 --- a/cayenne/src/test/java/org/apache/cayenne/CircularDependencyIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/CircularDependencyIT.java @@ -22,6 +22,7 @@ package org.apache.cayenne; import org.apache.cayenne.di.Inject; 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.OracleUnitDbAdapter; import org.apache.cayenne.unit.UnitDbAdapter; import org.apache.cayenne.unit.di.runtime.CayenneProjects; @@ -67,4 +68,34 @@ public class CircularDependencyIT extends RuntimeCase { } } + + @Test + public void testUpdate() { + E1 e1 = context.newObject(E1.class); + E2 e2 = context.newObject(E2.class); + + e1.setText("e1 #" + 1); + e2.setText("e2 #" + 2); + + 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(); + } }