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 818d6c959 Fix deletion of entities from flattened attributes
818d6c959 is described below
commit 818d6c959a71b2f905315bc855ff873ba00bc668
Author: Jurgen <[email protected]>
AuthorDate: Tue May 7 20:29:00 2024 +0400
Fix deletion of entities from flattened attributes
(cherry picked from commit 338920e8d4ec6f11ee302688ccef5555530e0c2a)
---
RELEASE-NOTES.txt | 1 +
.../org/apache/cayenne/access/ObjectStore.java | 12 ++++++
.../cayenne/access/flush/RootRowOpProcessor.java | 43 ++++++++++++++++---
.../access/DataContextFlattenedAttributesIT.java | 49 ++++++++++++++++++++--
4 files changed, 96 insertions(+), 9 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 032f23dae..8b0912d6f 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -28,6 +28,7 @@ CAY-2841 Multi column ColumnSelect with SHARED_CACHE fails
after 1st select
CAY-2844 Joint prefetch doesn't use ObjEntity qualifier
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
----------------------------------
Release: 4.2
diff --git
a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
index cca72c644..74b778c79 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java
@@ -1024,6 +1024,18 @@ public class ObjectStore implements Serializable,
SnapshotEventListener, GraphMa
.getOrDefault(objectId, Collections.emptyMap()).values();
}
+ /**
+ * @since 4.2.1
+ */
+ public Map<String, ObjectId> getFlattenedPathIdMap(ObjectId objectId) {
+ if(trackedFlattenedPaths == null) {
+ return Collections.emptyMap();
+ }
+
+ return trackedFlattenedPaths
+ .getOrDefault(objectId, Collections.emptyMap());
+ }
+
/**
* Mark that flattened path for object has data row in DB.
* @since 4.1
diff --git
a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
index 12c1b42cd..759d1462e 100644
---
a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
+++
b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
@@ -19,7 +19,8 @@
package org.apache.cayenne.access.flush;
-import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
@@ -30,7 +31,11 @@ import
org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.DeleteRule;
import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
/**
* Visitor that runs all required actions based on operation type.
@@ -74,14 +79,40 @@ class RootRowOpProcessor implements DbRowOpVisitor<Void> {
@Override
public Void visitDelete(DeleteDbRowOp dbRow) {
- if (dbRowOpFactory.getDescriptor().getEntity().isReadOnly()) {
+ ObjEntity entity = dbRowOpFactory.getDescriptor().getEntity();
+ if (entity.isReadOnly()) {
throw new CayenneRuntimeException("Attempt to modify object(s)
mapped to a read-only entity: '%s'. " +
- "Can't commit changes.",
dbRowOpFactory.getDescriptor().getEntity().getName());
+ "Can't commit changes.", entity.getName());
}
diff.apply(deleteHandler);
- Collection<ObjectId> flattenedIds =
dbRowOpFactory.getStore().getFlattenedIds(dbRow.getChangeId());
- flattenedIds.forEach(id ->
dbRowOpFactory.getOrCreate(dbRowOpFactory.getDbEntity(id), id,
DbRowOpType.DELETE));
- if (dbRowOpFactory.getDescriptor().getEntity().getDeclaredLockType()
== ObjEntity.LOCK_TYPE_OPTIMISTIC) {
+
+ DbEntity dbSource = entity.getDbEntity();
+ Map<String, ObjectId> flattenedPathIdMap =
dbRowOpFactory.getStore().getFlattenedPathIdMap(dbRow.getChangeId());
+
+ flattenedPathIdMap.forEach((path, value) -> {
+ int indexOfDot = path.indexOf('.');
+ String relName = indexOfDot == -1 ? path : path.substring(0,
path.indexOf('.'));
+ DbRelationship dbRel = dbSource.getRelationship(relName);
+
+ // Don't delete if the target entity has a toMany relationship
with the source entity,
+ // as there may be other records in the source entity with
references to it.
+ if (!dbRel.getReverseRelationship().isToMany()) {
+
+ // Get the delete rule for any ObjRelationship matching the
flattened
+ // attributes DbRelationship, defaulting to CASCADE if not
found.
+ int deleteRule = entity.getRelationships().stream()
+ .filter(r ->
r.getDbRelationships().equals(Collections.singletonList(dbRel)))
+ .map(ObjRelationship::getDeleteRule).findFirst()
+ .orElse(DeleteRule.CASCADE);
+
+ if (deleteRule == DeleteRule.CASCADE) {
+
dbRowOpFactory.getOrCreate(dbRowOpFactory.getDbEntity(value),
+ value, DbRowOpType.DELETE);
+ }
+ }
+ });
+
+ if (entity.getDeclaredLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC) {
dbRowOpFactory.getDescriptor().visitAllProperties(new
OptimisticLockQualifierBuilder(dbRow, diff));
}
return null;
diff --git
a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
index 8e04719a4..c04f0f45a 100644
---
a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
+++
b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java
@@ -38,6 +38,7 @@ import org.apache.cayenne.testdo.testmap.Artist;
import org.apache.cayenne.testdo.testmap.CompoundPainting;
import org.apache.cayenne.testdo.testmap.CompoundPaintingLongNames;
import org.apache.cayenne.testdo.testmap.Gallery;
+import org.apache.cayenne.testdo.testmap.PaintingInfo;
import org.apache.cayenne.unit.di.server.CayenneProjects;
import org.apache.cayenne.unit.di.server.ServerCase;
import org.apache.cayenne.unit.di.server.UseServerRuntime;
@@ -186,7 +187,7 @@ public class DataContextFlattenedAttributesIT extends
ServerCase {
"artist2",
painting.getArtistName());
assertEquals(
- "CompoundPainting.getArtistName(): " +
painting.getGalleryName(),
+ "CompoundPainting.getGalleryName(): " +
painting.getGalleryName(),
painting.getToGallery().getGalleryName(),
painting.getGalleryName());
}
@@ -472,14 +473,56 @@ public class DataContextFlattenedAttributesIT extends
ServerCase {
Number artistCount = (Number) Cayenne.objectForQuery(context, new
EJBQLQuery(
"select count(a) from Artist a"));
- assertEquals(1, artistCount.intValue());
+ assertEquals(2, artistCount.intValue());
Number paintingCount = (Number) Cayenne.objectForQuery(context, new
EJBQLQuery(
"select count(a) from Painting a"));
assertEquals(0, paintingCount.intValue());
Number galleryCount = (Number) Cayenne.objectForQuery(context, new
EJBQLQuery(
"select count(a) from Gallery a"));
- assertEquals(0, galleryCount.intValue());
+ assertEquals(1, galleryCount.intValue());
+ }
+
+ @Test
+ public void testDelete2() throws Exception {
+ createTestDataSet();
+
+ long infoCount =
ObjectSelect.query(PaintingInfo.class).selectCount(context);
+ assertEquals("PaintingInfo", 8, infoCount);
+
+ List<CompoundPainting> objects =
ObjectSelect.query(CompoundPainting.class)
+ .where(CompoundPainting.ARTIST_NAME.eq("artist2"))
+ .select(context);
+
+ // Should have two paintings by the same artist
+ assertEquals("Paintings", 2, objects.size());
+
+ CompoundPainting cp0 = objects.get(0);
+ CompoundPainting cp1 = objects.get(1);
+
+ // Both paintings are at the same gallery
+ assertEquals("Gallery", cp0.getGalleryName(), cp1.getGalleryName());
+
+ context.invalidateObjects(cp0);
+ context.deleteObjects(cp1);
+ context.commitChanges();
+
+ // Delete should only have deleted the painting and its info,
+ // the painting's artist and gallery should not be deleted.
+
+ objects = ObjectSelect.query(CompoundPainting.class)
+ .where(CompoundPainting.ARTIST_NAME.eq("artist2"))
+ .select(runtime.newContext());
+
+ // Should now only have one painting by artist2
+ assertEquals("Painting", 1, objects.size());
+ // and that painting should have a valid gallery
+ assertNotNull("Gallery is null", objects.get(0).getToGallery());
+ assertNotNull("GalleryName is null",
objects.get(0).getToGallery().getGalleryName());
+
+ // There should be one less painting info now
+ infoCount =
ObjectSelect.query(PaintingInfo.class).selectCount(context);
+ assertEquals("PaintingInfo", 7, infoCount);
}
@Test