diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java index 9f886e1632..6be7d0dbae 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java @@ -1011,6 +1011,18 @@ public Collection getFlattenedIds(ObjectId objectId) { .getOrDefault(objectId, Collections.emptyMap()).values(); } + /** + * @since 5.0 + */ + public Map 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/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java index 12c1b42cd6..c14b54a832 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java +++ b/cayenne/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.List; +import java.util.Map; import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.ObjectId; @@ -29,7 +30,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.exp.path.CayennePath; 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; /** @@ -74,14 +79,38 @@ public Void visitUpdate(UpdateDbRowOp dbRow) { @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 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 flattenedPathIdMap = dbRowOpFactory.getStore().getFlattenedPathIdMap(dbRow.getChangeId()); + + flattenedPathIdMap.entrySet().forEach(entry -> { + DbRelationship dbRel = dbSource.getRelationship(entry.getKey().first().toString()); + + // 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(List.of(dbRel))) + .map(r -> r.getDeleteRule()).findFirst() + .orElse(DeleteRule.CASCADE); + + if (deleteRule == DeleteRule.CASCADE) { + dbRowOpFactory.getOrCreate(dbRowOpFactory.getDbEntity(entry.getValue()), + entry.getValue(), DbRowOpType.DELETE); + } + } + }); + + if (entity.getDeclaredLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC) { dbRowOpFactory.getDescriptor().visitAllProperties(new OptimisticLockQualifierBuilder(dbRow, diff)); } return null; diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java index 7d941dff14..28456f02a8 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextFlattenedAttributesIT.java @@ -37,6 +37,7 @@ 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.runtime.CayenneProjects; import org.apache.cayenne.unit.di.runtime.RuntimeCase; import org.apache.cayenne.unit.di.runtime.UseCayenneRuntime; @@ -185,7 +186,7 @@ public void testSelectCompound2() throws Exception { "artist2", painting.getArtistName()); assertEquals( - "CompoundPainting.getArtistName(): " + painting.getGalleryName(), + "CompoundPainting.getGalleryName(): " + painting.getGalleryName(), painting.getToGallery().getGalleryName(), painting.getGalleryName()); } @@ -471,14 +472,56 @@ public void testDelete() throws Exception { 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 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