#3529 - Fix for M2M property is empty in preDelete of BeanPersistAdapter (#3830)

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
This commit is contained in:
Rob Bygrave
2026-07-06 19:47:37 +12:00
committed by GitHub
co-authored by robin.bygrave
parent 1d654e350b
commit a2f954a60e
3 changed files with 82 additions and 2 deletions
@@ -132,6 +132,13 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
* Cascade to children must be suppressed to avoid FK violations.
*/
private boolean insertConflictSkipped;
/**
* Set true once controller.preDelete() has been invoked so that it is
* only ever fired once (as it is fired early, prior to cascading the
* delete to children/many's rather than as part of executing the delete).
*/
private boolean preDeleteCalled;
private boolean preDeleteResult = true;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
@@ -1276,14 +1283,29 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
private int executeDelete() {
setTenantId();
if (controller == null || controller.preDelete(this)) {
if (controllerPreDelete()) {
return beanManager.getBeanPersister().delete(this);
}
// delete handled by the BeanController so return 0
return 0;
}
/**
* Invoke controller.preDelete() if not already invoked.
* <p>
* This is called prior to cascading the delete to children (assoc many's /
* many-to-many intersection rows) so that the persist controller can still
* see those collections/relationships as they were before the cascade delete.
*/
public boolean controllerPreDelete() {
if (!preDeleteCalled) {
preDeleteCalled = true;
setTenantId();
preDeleteResult = controller == null || controller.preDelete(this);
}
return preDeleteResult;
}
/**
* Persist to the document store now (via buffer, not post commit).
*/
@@ -907,6 +907,11 @@ public final class DefaultPersister implements Persister {
* </p>
*/
private int delete(PersistRequestBean<?> request) {
// fire preDelete now, before cascading to children/many's so that the
// BeanPersistController/Adapter still sees the bean's collections and
// relationships as they are prior to the cascade delete
request.controllerPreDelete();
DeleteUnloadedForeignKeys unloadedForeignKeys = null;
if (request.isPersistCascade()) {
// delete children first ... register the
@@ -9,6 +9,9 @@ import io.ebean.event.BeanPersistRequest;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tests.model.m2m.MnyA;
import org.tests.model.m2m.MnyB;
import org.tests.model.m2m.MnyC;
import org.tests.model.m2m.MnyTopic;
import org.tests.model.basic.EBasicVer;
import org.tests.model.basic.UTDetail;
@@ -166,6 +169,56 @@ public class BeanPersistControllerTest {
db.shutdown();
}
@Test
public void preDelete_seesManyToManyCollection_beforeCascadeDelete() {
List<List<MnyC>> capturedCs = new ArrayList<>();
BeanPersistAdapter mnyBAdapter = new BeanPersistAdapter() {
@Override
public boolean isRegisterFor(Class<?> cls) {
return MnyB.class.isAssignableFrom(cls);
}
@Override
public boolean preDelete(BeanPersistRequest<?> request) {
// the M2M collection should still be intact here, before the
// intersection table rows are cascade deleted
capturedCs.add(new ArrayList<>(((MnyB) request.bean()).getCs()));
return true;
}
};
DatabaseBuilder config = Database.builder();
config.setName("h2ebasicver");
config.setRegister(false);
config.setDefaultServer(false);
config.loadFromProperties();
config.setDdlGenerate(true);
config.setDdlRun(true);
config.setDdlExtra(false);
config.addClass(MnyA.class);
config.addClass(MnyB.class);
config.addClass(MnyC.class);
config.add(mnyBAdapter);
Database db = config.build();
try {
MnyB mnyB = new MnyB();
MnyC mnyC = new MnyC();
mnyB.setCs(new ArrayList<>());
mnyB.getCs().add(mnyC);
db.save(mnyC);
db.save(mnyB);
MnyB mnyBFromDB = db.find(MnyB.class, mnyB.getId());
db.delete(mnyBFromDB);
assertThat(capturedCs).hasSize(1);
assertThat(capturedCs.get(0)).extracting("id").containsExactly(mnyC.getId());
} finally {
db.shutdown();
}
}
private Database createDatabase(PersistAdapter persistAdapter) {
DatabaseBuilder config = Database.builder();
config.setName("h2ebasicver");