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