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 <5031427+ju...@users.noreply.github.com>
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

Reply via email to