PR #3656 fix - lazy initialised O2M relation are not correctly persisted on subsequent saves (#3793)

- Root cause: SaveManyBeans.removeAssocManyOrphans() only called setModifyListening when insertedParent=true. A first save with null O2M skipped it; the lazily-initialized collection on subsequent saves had no listen mode → .clear() untracked → orphan not deleted.
 - Fix: Added else { setListenMode(c, many); } to set the listen mode for existing collections that were never initialized (uses the existing null-guard helper).
 - Tests: testModifyListenModeSet2 now passes; all 26 cascade tests green.

-------------
Original #3656 description:

We found an issue, when O2M relations are not correctly persisted to the DB.

This happens, when

a bean is saved and the O2M property is empty
something is added and cleared again in two subsequent saves.
the same master-bean object has to be used
The issue here is, that the BeanCollection is lazily initialized with no modifyListenMode set after the first save.
This happens only for O2M relations with no order column. (Others work fine) See: https://github.com/ebean-orm/ebean-agent/blob/d4c40f1ce85c58f99cb0a85152aaa0a0075a9c01/ebean-agent/src/main/java/io/ebean/enhance/entity/FieldMeta.java#L469

And we need also a save, where the bean is saved with an empty/null value in the O2M property.
Subsequent saves will not update the modifyListenMode. See SaveManyBeans

      if (insertedParent) {
        // after insert set the modify listening mode for private owned etc
        c.setModifyListening(many.modifyListenMode());
      }

We found this in one of our unit-tests, where we've configured a bean for different states. It is probably something, that should not be too critical in real code, as you normally save a bean only once (When the bean was retrieved from DB, it should not occur)

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
This commit is contained in:
Rob Bygrave
2026-06-24 17:05:11 +12:00
committed by GitHub
co-authored by robin.bygrave
parent ca19a78c23
commit 490c70a7b7
2 changed files with 45 additions and 0 deletions
@@ -345,6 +345,10 @@ final class SaveManyBeans extends SaveManyBase {
if (insertedParent) {
// after insert set the modify listening mode for private owned etc
c.setModifyListening(many.modifyListenMode());
} else {
// a lazily initialized collection (e.g. first save had null value) has no
// listen mode yet - set it now so that subsequent modifications are tracked
setListenMode(c, many);
}
// We must not reset when we still have to update other entities in the collection and set their new orderColumn value
if (!hasOrderColumn) {
@@ -1,9 +1,12 @@
package org.tests.cascade;
import io.ebean.bean.BeanCollection;
import io.ebean.xtest.BaseTestCase;
import io.ebean.DB;
import io.ebean.test.LoggedSql;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Section;
import org.tests.model.basic.SubSection;
import java.util.Collections;
import java.util.List;
@@ -149,6 +152,44 @@ public class TestOrderedList extends BaseTestCase {
}
@Test
public void testModifyListenModeSet1() {
final OmOrderedMaster master = new OmOrderedMaster("Master");
final OmOrderedDetail detail1 = new OmOrderedDetail("Detail1");
DB.save(detail1);
DB.save(master);
master.getDetails().add(detail1);
DB.save(master);
master.getDetails().clear();
DB.save(master);
OmOrderedMaster masterDb = DB.find(OmOrderedMaster.class, master.getId());
assertThat(masterDb.getDetails()).isEmpty();
}
@Test
public void testModifyListenModeSet2() {
final Section master = new Section();
final SubSection detail1 = new SubSection();
DB.save(detail1);
DB.save(master);
master.getSubSections().add(detail1);
DB.save(master);
master.getSubSections().clear();
DB.save(master);
Section masterDb = DB.find(Section.class, master.getId());
assertThat(masterDb.getSubSections()).isEmpty();
}
@Test
public void testModifyListWithCache() {
final OmCacheOrderedMaster master = new OmCacheOrderedMaster("Master");