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 <[email protected]>
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();
+ }
}