From 81c6d4601dd06f9bf0d3d010079daacb367df269 Mon Sep 17 00:00:00 2001
From: rob bygrave
Date: Thu, 14 Jun 2018 16:09:45 +1200
Subject: [PATCH] #1425 - Stateless update with OneToMany with SoftDelete and
deleteMissingChildren = true ... does a hard delete
---
.../api/TxnProfileEventCodes.java | 2 +-
.../server/core/PersistRequest.java | 2 +-
.../server/core/PersistRequestBean.java | 25 +++---
.../server/deploy/BeanDescriptor.java | 17 ++--
.../server/deploy/IntersectionRow.java | 9 +-
.../server/persist/BatchedBeanHolder.java | 2 +-
.../server/persist/DefaultPersister.java | 87 +++++++++----------
.../server/persist/DeleteMode.java | 38 ++++++++
.../server/persist/SaveManyBeans.java | 8 +-
.../server/transaction/BeanPersistIds.java | 2 +-
.../org/tests/model/softdelete/EsdDetail.java | 33 +++++++
.../org/tests/model/softdelete/EsdMaster.java | 35 ++++++++
.../TestSoftDeleteStatelessUpdate.java | 74 ++++++++++++++++
13 files changed, 259 insertions(+), 75 deletions(-)
create mode 100644 src/main/java/io/ebeaninternal/server/persist/DeleteMode.java
create mode 100644 src/test/java/org/tests/model/softdelete/EsdDetail.java
create mode 100644 src/test/java/org/tests/model/softdelete/EsdMaster.java
create mode 100644 src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java
diff --git a/src/main/java/io/ebeaninternal/api/TxnProfileEventCodes.java b/src/main/java/io/ebeaninternal/api/TxnProfileEventCodes.java
index 0c17ad833..231d38296 100644
--- a/src/main/java/io/ebeaninternal/api/TxnProfileEventCodes.java
+++ b/src/main/java/io/ebeaninternal/api/TxnProfileEventCodes.java
@@ -13,7 +13,7 @@ public interface TxnProfileEventCodes {
String EVT_INSERT = "i";
String EVT_UPDATE = "u";
String EVT_DELETE = "d";
- String EVT_SOFT_DELETE = "ds";
+ String EVT_DELETE_SOFT = "ds";
String EVT_DELETE_PERMANENT = "dp";
String EVT_ORMUPDATE = "uo";
String FIND_UPDATE = "uq";
diff --git a/src/main/java/io/ebeaninternal/server/core/PersistRequest.java b/src/main/java/io/ebeaninternal/server/core/PersistRequest.java
index 46e7aa605..a031ae2fb 100644
--- a/src/main/java/io/ebeaninternal/server/core/PersistRequest.java
+++ b/src/main/java/io/ebeaninternal/server/core/PersistRequest.java
@@ -17,7 +17,7 @@ public abstract class PersistRequest extends BeanRequest implements BatchPostExe
INSERT(EVT_INSERT),
UPDATE(EVT_UPDATE),
DELETE(EVT_DELETE),
- SOFT_DELETE(EVT_SOFT_DELETE),
+ DELETE_SOFT(EVT_DELETE_SOFT),
DELETE_PERMANENT(EVT_DELETE_PERMANENT),
UPDATESQL(EVT_UPDATESQL),
CALLABLESQL(EVT_CALLABLESQL);
diff --git a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
index 07618f6b9..20c6e04b7 100644
--- a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
+++ b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java
@@ -23,6 +23,7 @@ import io.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
import io.ebeaninternal.server.deploy.id.ImportedId;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeaninternal.server.persist.BatchedSqlException;
+import io.ebeaninternal.server.persist.DeleteMode;
import io.ebeaninternal.server.persist.Flags;
import io.ebeaninternal.server.persist.PersistExecute;
import io.ebeaninternal.server.transaction.BeanPersistIdMap;
@@ -271,7 +272,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
onUpdateGeneratedProperties();
}
break;
- case SOFT_DELETE:
+ case DELETE_SOFT:
onUpdateGeneratedProperties();
break;
}
@@ -455,7 +456,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
beanDescriptor.cacheHandleUpdate(idValue, this, changeSet);
break;
case DELETE:
- case SOFT_DELETE:
+ case DELETE_SOFT:
beanDescriptor.cacheHandleDelete(idValue, this, changeSet);
break;
default:
@@ -475,7 +476,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
beanDescriptor.docStoreInsert(idValue, this, txn);
break;
case UPDATE:
- case SOFT_DELETE:
+ case DELETE_SOFT:
beanDescriptor.docStoreUpdate(idValue, this, txn);
break;
case DELETE:
@@ -496,7 +497,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
docStoreUpdates.queueIndex(beanDescriptor.getDocStoreQueueId(), idValue);
break;
case UPDATE:
- case SOFT_DELETE:
+ case DELETE_SOFT:
docStoreUpdates.queueIndex(beanDescriptor.getDocStoreQueueId(), idValue);
break;
case DELETE:
@@ -527,7 +528,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
beanPersistListener.deleted(bean);
break;
- case SOFT_DELETE:
+ case DELETE_SOFT:
beanPersistListener.softDeleted(bean);
break;
@@ -772,7 +773,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
executeUpdate();
return -1;
- case SOFT_DELETE:
+ case DELETE_SOFT:
prepareForSoftDelete();
executeSoftDelete();
return -1;
@@ -855,7 +856,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
}
switch (type) {
case DELETE:
- case SOFT_DELETE:
+ case DELETE_SOFT:
postDelete();
break;
case UPDATE:
@@ -968,7 +969,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
pendingPostUpdateNotify++;
}
break;
- case SOFT_DELETE:
+ case DELETE_SOFT:
controller.postSoftDelete(this);
break;
case DELETE:
@@ -993,7 +994,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
case DELETE:
transaction.logSummary("Deleted [" + name + "] [" + idValue + "]" + draft);
break;
- case SOFT_DELETE:
+ case DELETE_SOFT:
transaction.logSummary("SoftDelete [" + name + "] [" + idValue + "]" + draft);
break;
default:
@@ -1184,10 +1185,10 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
}
/**
- * Return true if this is a soft delete request.
+ * Return the delete mode - Soft or Hard.
*/
- public boolean isSoftDelete() {
- return Type.SOFT_DELETE == type;
+ public DeleteMode deleteMode() {
+ return Type.DELETE_SOFT == type ? DeleteMode.SOFT : DeleteMode.HARD;
}
/**
diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java
index ee277666b..ba0ecc52d 100644
--- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java
+++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java
@@ -62,6 +62,7 @@ import io.ebeaninternal.server.el.ElComparatorProperty;
import io.ebeaninternal.server.el.ElPropertyChainBuilder;
import io.ebeaninternal.server.el.ElPropertyDeploy;
import io.ebeaninternal.server.el.ElPropertyValue;
+import io.ebeaninternal.server.persist.DeleteMode;
import io.ebeaninternal.server.persist.DmlUtil;
import io.ebeaninternal.server.query.CQueryPlan;
import io.ebeaninternal.server.query.ExtraJoin;
@@ -907,7 +908,7 @@ public class BeanDescriptor implements BeanType, STreeType {
case INSERT:
return changeLogFilter.includeInsert(request) ? insertBeanChange(request) : null;
case UPDATE:
- case SOFT_DELETE:
+ case DELETE_SOFT:
return changeLogFilter.includeUpdate(request) ? updateBeanChange(request) : null;
case DELETE:
return changeLogFilter.includeDelete(request) ? deleteBeanChange(request) : null;
@@ -991,11 +992,11 @@ public class BeanDescriptor implements BeanType, STreeType {
jsonWriter.writeEndObject();
}
- public SqlUpdate deleteById(Object id, List
*/
void deleteManyDetails(SpiTransaction t, BeanDescriptor> desc, EntityBean parentBean,
- BeanPropertyAssocMany> many, List excludeDetailIds, boolean softDelete) {
+ BeanPropertyAssocMany> many, List excludeDetailIds, DeleteMode deleteMode) {
if (many.getCascadeInfo().isDelete()) {
// cascade delete the beans in the collection
BeanDescriptor> targetDesc = many.getTargetDescriptor();
- if (!softDelete || targetDesc.isSoftDelete()) {
+ if (deleteMode.isHard() || targetDesc.isSoftDelete()) {
if (targetDesc.isDeleteByStatement()) {
// Just delete all the children with one statement
IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds);
- SqlUpdate sqlDelete = intRow.createDelete(server, softDelete);
+ SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode);
executeSqlUpdate(sqlDelete, t);
} else {
@@ -1048,7 +1047,7 @@ public final class DefaultPersister implements Persister {
Object parentId = desc.getId(parentBean);
List idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds);
if (!idsByParentId.isEmpty()) {
- deleteChildrenById(t, targetDesc, idsByParentId, softDelete);
+ deleteChildrenById(t, targetDesc, idsByParentId, deleteMode);
}
}
}
@@ -1060,7 +1059,7 @@ public final class DefaultPersister implements Persister {
*
* Will use delete by object if the child entity has manyToMany relationships.
*/
- private void deleteChildrenById(SpiTransaction t, BeanDescriptor> targetDesc, List childIds, boolean softDelete) {
+ private void deleteChildrenById(SpiTransaction t, BeanDescriptor> targetDesc, List childIds, DeleteMode deleteMode) {
if (!targetDesc.isDeleteByBulk()) {
// convert into a list of reference objects and perform delete by object
@@ -1068,11 +1067,11 @@ public final class DefaultPersister implements Persister {
for (Object id : childIds) {
refList.add(targetDesc.createReference(id, null));
}
- deleteList(refList, t, softDelete, true);
+ deleteList(refList, t, deleteMode, true);
} else {
// perform delete by statement if possible
- delete(targetDesc, null, childIds, t, softDelete);
+ delete(targetDesc, null, childIds, t, deleteMode);
}
}
@@ -1137,16 +1136,16 @@ public final class DefaultPersister implements Persister {
*/
private void deleteAssocOne(PersistRequestBean> request) {
- boolean softDelete = request.isSoftDelete();
+ DeleteMode deleteMode = request.deleteMode();
for (BeanPropertyAssocOne> prop : request.getBeanDescriptor().propertiesOneImportedDelete()) {
- if (!softDelete || prop.isTargetSoftDelete()) {
+ if (deleteMode.isHard() || prop.isTargetSoftDelete()) {
if (request.isLoadedProperty(prop)) {
Object detailBean = prop.getValue(request.getEntityBean());
if (detailBean != null) {
EntityBean detail = (EntityBean) detailBean;
if (prop.hasId(detail)) {
- deleteRecurse(detail, request.getTransaction(), request.isSoftDelete());
+ deleteRecurse(detail, request.getTransaction(), deleteMode);
}
}
}
@@ -1245,7 +1244,7 @@ public final class DefaultPersister implements Persister {
type = Type.DELETE;
} else if (type == Type.DELETE && mgr.getBeanDescriptor().isSoftDelete()) {
// automatically convert to soft delete for types that support it
- type = Type.SOFT_DELETE;
+ type = Type.DELETE_SOFT;
}
return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, flags);
diff --git a/src/main/java/io/ebeaninternal/server/persist/DeleteMode.java b/src/main/java/io/ebeaninternal/server/persist/DeleteMode.java
new file mode 100644
index 000000000..9961a8bfc
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/persist/DeleteMode.java
@@ -0,0 +1,38 @@
+package io.ebeaninternal.server.persist;
+
+import io.ebeaninternal.server.core.PersistRequest;
+
+/**
+ * The delete mode of the persistence request. The mode is determined at the beginning of the request
+ * as being a Hard or Soft delete based and then the mode is used for that request and any cascading.
+ */
+public enum DeleteMode {
+
+ /**
+ * Soft/logical delete.
+ */
+ SOFT(PersistRequest.Type.DELETE_SOFT, false),
+
+ /**
+ * Hard/permanent delete.
+ */
+ HARD(PersistRequest.Type.DELETE_PERMANENT, true);
+
+ private boolean hard;
+
+ private PersistRequest.Type persistType;
+
+ DeleteMode(PersistRequest.Type persistType, boolean hard) {
+ this.persistType = persistType;
+ this.hard = hard;
+ }
+
+ public PersistRequest.Type persistType() {
+ return persistType;
+ }
+
+ public boolean isHard() {
+ return hard;
+ }
+
+}
diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java
index 063862967..52d75b4fd 100644
--- a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java
+++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java
@@ -33,6 +33,7 @@ class SaveManyBeans extends SaveManyBase {
private final BeanDescriptor> targetDescriptor;
private final boolean isMap;
private final boolean saveRecurseSkippable;
+ private final DeleteMode deleteMode;
private Collection> collection;
private DefaultPersister persister;
@@ -47,6 +48,7 @@ class SaveManyBeans extends SaveManyBase {
this.targetDescriptor = many.getTargetDescriptor();
this.isMap = many.getManyType().isMap();
this.saveRecurseSkippable = many.isSaveRecurseSkippable();
+ this.deleteMode = targetDescriptor.isSoftDelete() ? DeleteMode.SOFT : DeleteMode.HARD;
}
@Override
@@ -58,7 +60,7 @@ class SaveManyBeans extends SaveManyBase {
// we only allow one direction based on first traversed basis
boolean saveIntersectionFromThisDirection = isSaveIntersection();
if (cascade) {
- saveAssocManyDetails( false);
+ saveAssocManyDetails(false);
}
// for ManyToMany save the 'relationship' via inserts/deletes
// into/from the intersection table
@@ -125,7 +127,7 @@ class SaveManyBeans extends SaveManyBase {
// collect the Id's (to exclude from deleteManyDetails)
List detailIds = collectIds(collection, targetDescriptor, isMap);
// deleting missing children - children not in our collected detailIds
- persister.deleteManyDetails(transaction, many.getBeanDescriptor(), parentBean, many, detailIds, false);
+ persister.deleteManyDetails(transaction, many.getBeanDescriptor(), parentBean, many, detailIds, deleteMode);
}
transaction.depth(+1);
@@ -320,7 +322,7 @@ class SaveManyBeans extends SaveManyBase {
// the object from the 'other' side of the ManyToMany
// build a intersection row for 'delete'
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish);
- SqlUpdate sqlDelete = intRow.createDelete(server, false);
+ SqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD);
persister.executeSqlUpdate(sqlDelete, transaction);
}
}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java b/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java
index 6bc530e9e..cf77829ee 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java
@@ -176,7 +176,7 @@ public class BeanPersistIds {
addUpdateId(id);
break;
case DELETE:
- case SOFT_DELETE:
+ case DELETE_SOFT:
addDeleteId(id);
break;
diff --git a/src/test/java/org/tests/model/softdelete/EsdDetail.java b/src/test/java/org/tests/model/softdelete/EsdDetail.java
new file mode 100644
index 000000000..ec96fa3e9
--- /dev/null
+++ b/src/test/java/org/tests/model/softdelete/EsdDetail.java
@@ -0,0 +1,33 @@
+package org.tests.model.softdelete;
+
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+
+@Entity
+public class EsdDetail extends BaseSoftDelete {
+
+ String name;
+
+ @ManyToOne(optional = false)
+ EsdMaster master;
+
+ public EsdDetail(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public EsdMaster getMaster() {
+ return master;
+ }
+
+ public void setMaster(EsdMaster master) {
+ this.master = master;
+ }
+}
diff --git a/src/test/java/org/tests/model/softdelete/EsdMaster.java b/src/test/java/org/tests/model/softdelete/EsdMaster.java
new file mode 100644
index 000000000..23bb3d33a
--- /dev/null
+++ b/src/test/java/org/tests/model/softdelete/EsdMaster.java
@@ -0,0 +1,35 @@
+package org.tests.model.softdelete;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.OneToMany;
+import java.util.List;
+
+@Entity
+public class EsdMaster extends BaseSoftDelete {
+
+ String name;
+
+ @OneToMany(mappedBy = "master", cascade = CascadeType.ALL)
+ List details;
+
+ public EsdMaster(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getDetails() {
+ return details;
+ }
+
+ public void setDetails(List details) {
+ this.details = details;
+ }
+}
diff --git a/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java b/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java
new file mode 100644
index 000000000..85901d391
--- /dev/null
+++ b/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java
@@ -0,0 +1,74 @@
+package org.tests.softdelete;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebeantest.LoggedSql;
+import org.junit.Test;
+import org.tests.model.softdelete.EsdDetail;
+import org.tests.model.softdelete.EsdMaster;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestSoftDeleteStatelessUpdate extends BaseTestCase {
+
+ @Test
+ public void test() {
+
+ EsdMaster master = new EsdMaster("m1");
+ master.getDetails().add(new EsdDetail("d1"));
+ master.getDetails().add(new EsdDetail("d2"));
+ master.getDetails().add(new EsdDetail("d3"));
+
+
+ Ebean.save(master);
+
+ EsdMaster upd = new EsdMaster("m1-modified");
+ upd.setId(master.getId());
+
+ EsdDetail d1 = new EsdDetail("d1");
+ d1.setId(master.getDetails().get(0).getId());
+ upd.getDetails().add(d1);
+
+ EsdDetail d3 = new EsdDetail("d3-mod");
+ d3.setId(master.getDetails().get(2).getId());
+ upd.getDetails().add(d3);
+
+
+ LoggedSql.start();
+
+ Ebean.getDefaultServer().update(upd, null, true);
+
+ List sql = LoggedSql.collect();
+ assertThat(sql).hasSize(4);
+ assertThat(sql.get(0)).contains("update esd_master set name=? where id=?");
+ assertThat(sql.get(1)).contains("update esd_detail set deleted=true where master_id = ? and not");
+
+
+ EsdMaster fetchedWithSoftDeletes = Ebean.find(EsdMaster.class)
+ .setId(master.getId())
+ .setIncludeSoftDeletes()
+ .fetch("details")
+ .findOne();
+
+ assertThat(fetchedWithSoftDeletes.getDetails()).hasSize(3);
+
+ sql = LoggedSql.collect();
+ assertThat(sql).hasSize(1);
+ assertThat(sql.get(0)).contains("left join esd_detail t1 on t1.master_id = t0.id where t0.id = ?");
+
+ EsdMaster fetchedWithOutSoftDeletes = Ebean.find(EsdMaster.class)
+ .setId(master.getId())
+ .fetch("details")
+ .findOne();
+
+ assertThat(fetchedWithOutSoftDeletes.getDetails()).hasSize(2);
+
+ sql = LoggedSql.stop();
+ assertThat(sql).hasSize(1);
+ assertThat(sql.get(0)).contains("left join esd_detail t1 on t1.master_id = t0.id and t1.deleted = false where t0.id = ?");
+
+
+ }
+}