Skip to content

Commit

Permalink
Fix deletion of entities from flattened attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
Jugen committed May 17, 2024
1 parent dd92536 commit 338920e
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 10 deletions.
3 changes: 2 additions & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,5 @@ CAY-2840 Vertical Inheritance: Missing subclass attributes with joint prefetch
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-2851 Replace Existing OneToOne From New Object
CAY-2853 Incorrect deletion of entities from flattened attributes
12 changes: 12 additions & 0 deletions cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,18 @@ public Collection<ObjectId> getFlattenedIds(ObjectId objectId) {
.getOrDefault(objectId, Collections.emptyMap()).values();
}

/**
* @since 5.0
*/
public Map<CayennePath,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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<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<CayennePath,ObjectId> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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<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
Expand Down

0 comments on commit 338920e

Please sign in to comment.