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 idList, boolean softDelete) { + public SqlUpdate deleteById(Object id, List idList, DeleteMode mode) { if (id != null) { - return deleteById(id, softDelete); + return deleteById(id, mode); } else { - return deleteByIdList(idList, softDelete); + return deleteByIdList(idList, mode); } } @@ -1017,9 +1018,9 @@ public class BeanDescriptor implements BeanType, STreeType { * Return SQL that can be used to delete a list of Id's without any optimistic * concurrency checking. */ - private SqlUpdate deleteByIdList(List idList, boolean softDelete) { + private SqlUpdate deleteByIdList(List idList, DeleteMode mode) { - String baseSql = softDelete ? softDeleteByIdInSql : deleteByIdInSql; + String baseSql = mode.isHard() ? deleteByIdInSql : softDeleteByIdInSql; StringBuilder sb = new StringBuilder(baseSql); String inClause = idBinder.getIdInValueExprDelete(idList.size()); sb.append(inClause); @@ -1033,9 +1034,9 @@ public class BeanDescriptor implements BeanType, STreeType { * Return SQL that can be used to delete by Id without any optimistic * concurrency checking. */ - private SqlUpdate deleteById(Object id, boolean softDelete) { + private SqlUpdate deleteById(Object id, DeleteMode mode) { - String baseSql = softDelete ? softDeleteByIdSql : deleteByIdSql; + String baseSql = mode.isHard() ? deleteByIdSql : softDeleteByIdSql; DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(baseSql); Object[] bindValues = idBinder.getBindValues(id); diff --git a/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java b/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java index 59c13838a..305469af2 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java +++ b/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java @@ -6,6 +6,7 @@ import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.server.core.DefaultSqlUpdate; import io.ebeaninternal.server.expression.DefaultExpressionRequest; import io.ebeaninternal.server.expression.IdInExpression; +import io.ebeaninternal.server.persist.DeleteMode; import java.util.LinkedHashMap; import java.util.List; @@ -72,16 +73,16 @@ public class IntersectionRow { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SqlUpdate createDelete(SpiEbeanServer server, boolean softDelete) { + public SqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); - if (softDelete) { + if (deleteMode.isHard()) { + sb.append("delete from ").append(tableName); + } else { sb.append("update ").append(tableName).append(" set "); sb.append(targetDescriptor.getSoftDeleteDbSet()); - } else { - sb.append("delete from ").append(tableName); } sb.append(" where "); diff --git a/src/main/java/io/ebeaninternal/server/persist/BatchedBeanHolder.java b/src/main/java/io/ebeaninternal/server/persist/BatchedBeanHolder.java index 6c974cc56..61c46ef0c 100644 --- a/src/main/java/io/ebeaninternal/server/persist/BatchedBeanHolder.java +++ b/src/main/java/io/ebeaninternal/server/persist/BatchedBeanHolder.java @@ -142,7 +142,7 @@ public class BatchedBeanHolder { return inserts.size(); case UPDATE: - case SOFT_DELETE: + case DELETE_SOFT: if (updates == null) { updates = new ArrayList<>(); } diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 415738cec..a259f8b84 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -357,9 +357,8 @@ public final class DefaultPersister implements Persister { /** * Recursively delete the bean. This calls back to the EbeanServer. */ - private int deleteRecurse(EntityBean detailBean, Transaction t, boolean softDelete) { - Type deleteType = softDelete ? Type.SOFT_DELETE : Type.DELETE_PERMANENT; - return deleteRequest(createRequest(detailBean, t, deleteType)); + private int deleteRecurse(EntityBean detailBean, Transaction t, DeleteMode deleteMode) { + return deleteRequest(createRequest(detailBean, t, deleteMode.persistType())); } @Override @@ -605,13 +604,13 @@ public final class DefaultPersister implements Persister { } } - private void deleteList(List beanList, SpiTransaction t, boolean softDelete, boolean children) { + private void deleteList(List beanList, SpiTransaction t, DeleteMode deleteMode, boolean children) { if (children) { t.depth(-1); t.checkBatchEscalationOnCollection(); } for (Object aBeanList : beanList) { - deleteRecurse((EntityBean) aBeanList, t, softDelete); + deleteRecurse((EntityBean) aBeanList, t, deleteMode); } if (children) { t.flushBatchOnCollection(); @@ -630,9 +629,9 @@ public final class DefaultPersister implements Persister { } BeanDescriptor descriptor = beanDescriptorManager.getBeanDescriptor(beanType); - boolean softDelete = !permanent && descriptor.isSoftDelete(); + DeleteMode deleteMode = (permanent || !descriptor.isSoftDelete()) ? DeleteMode.HARD : DeleteMode.SOFT; if (descriptor.isMultiTenant()) { - return deleteAsBeans(ids, transaction, permanent, descriptor); + return deleteAsBeans(ids, transaction, deleteMode, descriptor); } ArrayList idList = new ArrayList<>(ids.size()); @@ -641,19 +640,19 @@ public final class DefaultPersister implements Persister { idList.add(descriptor.convertId(id)); } - return delete(descriptor, null, idList, transaction, softDelete); + return delete(descriptor, null, idList, transaction, deleteMode); } /** * Convert delete by Ids into delete many beans. */ - private int deleteAsBeans(Collection ids, Transaction transaction, boolean permanent, BeanDescriptor descriptor) { + private int deleteAsBeans(Collection ids, Transaction transaction, DeleteMode deleteMode, BeanDescriptor descriptor) { // convert to a delete by bean int total = 0; for (Object id : ids) { EntityBean bean = descriptor.createEntityBean(); descriptor.convertSetId(id, bean); - int rowCount = deleteRecurse(bean, transaction, permanent); + int rowCount = deleteRecurse(bean, transaction, deleteMode); if (rowCount == -1) { total = -1; } else if (total != -1) { @@ -677,14 +676,14 @@ public final class DefaultPersister implements Persister { } id = descriptor.convertId(id); - boolean softDelete = !permanent && descriptor.isSoftDelete(); - return delete(descriptor, id, null, transaction, softDelete); + DeleteMode deleteMode = (permanent || !descriptor.isSoftDelete()) ? DeleteMode.HARD : DeleteMode.SOFT; + return delete(descriptor, id, null, transaction, deleteMode); } /** * Delete by Id or a List of Id's. */ - private int delete(BeanDescriptor descriptor, Object id, List idList, Transaction transaction, boolean softDelete) { + private int delete(BeanDescriptor descriptor, Object id, List idList, Transaction transaction, DeleteMode deleteMode) { SpiTransaction t = (SpiTransaction) transaction; if (t.isPersistCascade()) { @@ -693,14 +692,14 @@ public final class DefaultPersister implements Persister { // We actually need to execute a query to get the foreign key values // as they are required for the delete cascade. Query back just the // Id and the appropriate foreign key values - Query q = deleteRequiresQuery(descriptor, propImportDelete, softDelete); + Query q = deleteRequiresQuery(descriptor, propImportDelete, deleteMode); if (idList != null) { q.where().idIn(idList); if (t.isLogSummary()) { t.logSummary("-- DeleteById of " + descriptor.getName() + " ids[" + idList + "] requires fetch of foreign key values"); } List beanList = server.findList(q, t); - deleteList(beanList, t, softDelete, false); + deleteList(beanList, t, deleteMode, false); return beanList.size(); } else { @@ -712,7 +711,7 @@ public final class DefaultPersister implements Persister { if (bean == null) { return 0; } else { - return deleteRecurse(bean, t, softDelete); + return deleteRecurse(bean, t, deleteMode); } } } @@ -724,14 +723,14 @@ public final class DefaultPersister implements Persister { for (BeanPropertyAssocOne expOne : expOnes) { BeanDescriptor targetDesc = expOne.getTargetDescriptor(); // only cascade soft deletes when supported by target - if (!softDelete || targetDesc.isSoftDelete()) { - if (!softDelete && targetDesc.isDeleteByStatement()) { + if (deleteMode.isHard() || targetDesc.isSoftDelete()) { + if (deleteMode.isHard() && targetDesc.isDeleteByStatement()) { SqlUpdate sqlDelete = expOne.deleteByParentId(id, idList); executeSqlUpdate(sqlDelete, t); } else { List childIds = expOne.findIdsByParentId(id, idList, t); if (childIds != null && !childIds.isEmpty()) { - deleteChildrenById(t, targetDesc, childIds, softDelete); + deleteChildrenById(t, targetDesc, childIds, deleteMode); } } } @@ -742,8 +741,8 @@ public final class DefaultPersister implements Persister { for (BeanPropertyAssocMany many : manys) { BeanDescriptor targetDesc = many.getTargetDescriptor(); // only cascade soft deletes when supported by target - if (!softDelete || targetDesc.isSoftDelete()) { - if (!softDelete && targetDesc.isDeleteByStatement()) { + if (deleteMode.isHard() || targetDesc.isSoftDelete()) { + if (deleteMode.isHard() && targetDesc.isDeleteByStatement()) { // we can just delete children with a single statement SqlUpdate sqlDelete = many.deleteByParentId(id, idList); executeSqlUpdate(sqlDelete, t); @@ -751,14 +750,14 @@ public final class DefaultPersister implements Persister { // we need to fetch the Id's to delete (recurse or notify L2 cache) List childIds = many.findIdsByParentId(id, idList, t, null); if (!childIds.isEmpty()) { - delete(targetDesc, null, childIds, t, softDelete); + delete(targetDesc, null, childIds, t, deleteMode); } } } } } - if (!softDelete) { + if (deleteMode.isHard()) { // ManyToMany's ... delete from intersection table BeanPropertyAssocMany[] manys = descriptor.propertiesManyToMany(); for (BeanPropertyAssocMany many : manys) { @@ -771,7 +770,7 @@ public final class DefaultPersister implements Persister { } // delete the bean(s) - SqlUpdate deleteById = descriptor.deleteById(id, idList, softDelete); + SqlUpdate deleteById = descriptor.deleteById(id, idList, deleteMode); if (t.isLogSummary()) { if (idList != null) { t.logSummary("-- Deleting " + descriptor.getName() + " Ids: " + idList); @@ -822,7 +821,7 @@ public final class DefaultPersister implements Persister { * We need to create and execute a query to get the foreign key values as * the delete cascades to them (foreign keys). */ - private Query deleteRequiresQuery(BeanDescriptor desc, BeanPropertyAssocOne[] propImportDelete, boolean softDelete) { + private Query deleteRequiresQuery(BeanDescriptor desc, BeanPropertyAssocOne[] propImportDelete, DeleteMode deleteMode) { Query q = server.createQuery(desc.getBeanType()); StringBuilder sb = new StringBuilder(30); @@ -831,7 +830,7 @@ public final class DefaultPersister implements Persister { } q.setAutoTune(false); q.select(sb.toString()); - if (!softDelete) { + if (deleteMode.isHard() && desc.isSoftDelete()) { // hard delete so we want this query to include logically deleted rows (if any) q.setIncludeSoftDeletes(); } @@ -956,7 +955,7 @@ public final class DefaultPersister implements Persister { BeanDescriptor desc = request.getBeanDescriptor(); EntityBean parentBean = request.getEntityBean(); - boolean softDelete = request.isSoftDelete(); + DeleteMode deleteMode = request.deleteMode(); BeanPropertyAssocOne[] expOnes = desc.propertiesOneExportedDelete(); if (expOnes.length > 0) { @@ -964,11 +963,11 @@ public final class DefaultPersister implements Persister { DeleteUnloadedForeignKeys unloaded = null; for (BeanPropertyAssocOne prop : expOnes) { // for soft delete check cascade type also supports soft delete - if (!softDelete || prop.isTargetSoftDelete()) { + if (deleteMode.isHard() || prop.isTargetSoftDelete()) { if (request.isLoadedProperty(prop)) { Object detailBean = prop.getValue(parentBean); if (detailBean != null) { - deleteRecurse((EntityBean) detailBean, t, softDelete); + deleteRecurse((EntityBean) detailBean, t, deleteMode); } } else { if (unloaded == null) { @@ -987,7 +986,7 @@ public final class DefaultPersister implements Persister { // Many's with delete cascade for (BeanPropertyAssocMany many : desc.propertiesManyDelete()) { if (many.hasJoinTable()) { - if (!softDelete) { + if (deleteMode.isHard()) { // delete associated rows from intersection table (but not during soft delete) deleteAssocManyIntersection(parentBean, many, t, request.isPublish()); } @@ -996,7 +995,7 @@ public final class DefaultPersister implements Persister { if (ModifyListenMode.REMOVALS == many.getModifyListenMode()) { // PrivateOwned ... // if soft delete then check target also supports soft delete - if (!softDelete || many.isTargetSoftDelete()) { + if (deleteMode.isHard() || many.isTargetSoftDelete()) { Object details = many.getValue(parentBean); if (details instanceof BeanCollection) { Set modifyRemovals = ((BeanCollection) details).getModifyRemovals(); @@ -1006,7 +1005,7 @@ public final class DefaultPersister implements Persister { for (Object detail : modifyRemovals) { EntityBean detailBean = (EntityBean) detail; if (many.hasId(detailBean)) { - deleteRecurse(detailBean, t, softDelete); + deleteRecurse(detailBean, t, deleteMode); } } } @@ -1014,7 +1013,7 @@ public final class DefaultPersister implements Persister { } } - deleteManyDetails(t, desc, parentBean, many, null, softDelete); + deleteManyDetails(t, desc, parentBean, many, null, deleteMode); } } @@ -1031,16 +1030,16 @@ public final class DefaultPersister implements Persister { *

*/ 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 = ?"); + + + } +}