From 0ad52cf6a7fa3cdbd6cdfd420f614f94af209ed4 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Tue, 1 Dec 2015 23:21:11 +1300 Subject: [PATCH 1/3] #118 - ENH: Add support for for Soft Deletes (Logical Deletion) ... - initial --- src/main/java/com/avaje/ebean/Query.java | 5 + .../avaje/ebean/annotation/SoftDelete.java | 27 +++ .../avaje/ebean/bean/EntityBeanIntercept.java | 2 +- .../com/avaje/ebeaninternal/api/SpiQuery.java | 13 ++ .../server/core/PersistRequest.java | 2 +- .../server/core/PersistRequestBean.java | 20 ++ .../server/deploy/BeanDescriptor.java | 58 +++++- .../server/deploy/BeanProperty.java | 34 ++++ .../server/deploy/BeanPropertyAssocMany.java | 2 +- .../server/deploy/IntersectionRow.java | 37 ++-- .../deploy/meta/DeployBeanProperty.java | 42 ++++ .../deploy/meta/DeployBeanPropertyLists.java | 10 + .../server/deploy/parse/AnnotationFields.java | 4 + .../server/persist/DefaultPersister.java | 188 ++++++++++-------- .../server/query/CQueryBuilder.java | 19 ++ .../ebeaninternal/server/query/SqlTree.java | 7 + .../server/query/SqlTreeNode.java | 5 + .../server/query/SqlTreeNodeBean.java | 10 + .../server/query/SqlTreeNodeExtraJoin.java | 5 + .../query/SqlTreeNodeManyWhereJoin.java | 5 + .../server/querydefn/DefaultOrmQuery.java | 26 +++ .../model/softdelete/BaseSoftDelete.java | 45 +++++ .../model/softdelete/EBasicSoftDelete.java | 27 +++ .../tests/softdelete/TestSoftDeleteBasic.java | 43 ++++ 24 files changed, 531 insertions(+), 105 deletions(-) create mode 100644 src/main/java/com/avaje/ebean/annotation/SoftDelete.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/BaseSoftDelete.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java create mode 100644 src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index 00367523e..7043fd0f7 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -302,6 +302,11 @@ public interface Query extends Serializable { */ Query asDraft(); + /** + * Execute the query including soft deleted rows. + */ + Query includeSoftDeletes(); + /** * Cancel the query execution if supported by the underlying database and * driver. diff --git a/src/main/java/com/avaje/ebean/annotation/SoftDelete.java b/src/main/java/com/avaje/ebean/annotation/SoftDelete.java new file mode 100644 index 000000000..56fde390e --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/SoftDelete.java @@ -0,0 +1,27 @@ +package com.avaje.ebean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to indicate a property on an entity bean used to control 'soft delete' + * (also known as 'logical delete'). + *

+ * The property should be of type boolean, int or short. + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SoftDelete { + + /** + * Specify the bind value that matches 'deleted' state. + *

+ * If not specified then for boolean this is true and + * for int and short this value is 1. + *

+ */ + String value() default ""; +} diff --git a/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java b/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java index e1e2845a1..c67f16616 100644 --- a/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java +++ b/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java @@ -507,7 +507,7 @@ public final class EntityBeanIntercept implements Serializable { setDirty(true); } - private void setChangedProperty(int propertyIndex) { + public void setChangedProperty(int propertyIndex) { if (changedProps == null) { changedProps = new boolean[owner._ebean_getPropertyNames().length]; } diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 05b1a7136..912dc6354 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -96,6 +96,10 @@ public interface SpiQuery extends Query { } enum TemporalMode { + /** + * Includes soft deletes rows in the result. + */ + SOFT_DELETED, /** * Query runs against draft tables. */ @@ -183,6 +187,11 @@ public interface SpiQuery extends Query { */ boolean isAsDraft(); + /** + * Return true if this query includes soft deleted rows. + */ + boolean isIncludeSoftDeletes(); + /** * Return the asOf Timestamp which the query should run as. */ @@ -198,6 +207,10 @@ public interface SpiQuery extends Query { */ List getAsOfTableAlias(); + void addSoftDeletePredicate(String softDeletePredicate); + + List getSoftDeletePredicates(); + /** * Return a listener that wants to be notified when the bean collection is * first used. diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java index baecf7470..0d1eeb9ae 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java @@ -12,7 +12,7 @@ import com.avaje.ebeaninternal.server.persist.PersistExecute; public abstract class PersistRequest extends BeanRequest implements BatchPostExecute { public enum Type { - INSERT, UPDATE, DELETE, UPDATESQL, CALLABLESQL + INSERT, UPDATE, DELETE, SOFT_DELETE, UPDATESQL, CALLABLESQL } protected boolean persistCascade; diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java index 324fee809..00b69ba60 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java @@ -479,6 +479,11 @@ public final class PersistRequestBean extends PersistRequest implements BeanP persistExecute.executeUpdateBean(this); return -1; + case SOFT_DELETE: + prepareForSoftDelete(); + persistExecute.executeUpdateBean(this); + return -1; + case DELETE: return persistExecute.executeDeleteBean(this); @@ -487,6 +492,14 @@ public final class PersistRequestBean extends PersistRequest implements BeanP } } + /** + * Soft delete is executed as update so we want to set deleted=true property. + */ + private void prepareForSoftDelete() { + + beanDescriptor.setSoftDeleteValue(entityBean); + } + @Override public int executeOrQueue() { @@ -801,4 +814,11 @@ public final class PersistRequestBean extends PersistRequest implements BeanP public String getUpdateTable() { return publish ? beanDescriptor.getBaseTable() : beanDescriptor.getDraftTable(); } + + /** + * Return true if this is a soft delete request. + */ + public boolean isSoftDelete() { + return Type.SOFT_DELETE == type; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 3f0320b30..30fc2d8ac 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -149,6 +149,9 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { private final String baseTableVersionsBetween; private final boolean historySupport; + private final BeanProperty softDeleteProperty; + private final boolean softDelete; + private final String draftTable; /** @@ -316,8 +319,9 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { private String idBinderIdSql; private String deleteByIdSql; - private String deleteByIdInSql; + private String softDeleteByIdSql; + private String softDeleteByIdInSql; private final String name; @@ -395,6 +399,8 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { // helper object used to derive lists of properties DeployBeanPropertyLists listHelper = new DeployBeanPropertyLists(owner, this, deploy); + this.softDeleteProperty = listHelper.getSoftDeleteProperty(); + this.softDelete = (softDeleteProperty != null); this.idProperty = listHelper.getId(); this.versionProperty = listHelper.getVersionProperty(); this.draftDirty = listHelper.getDraftDirty(); @@ -621,6 +627,14 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { deleteByIdSql = "delete from " + baseTable + " where " + idEqualsSql; deleteByIdInSql = "delete from " + baseTable + " where " + idBinderInLHSSqlNoAlias + " "; + if (softDelete) { + softDeleteByIdSql = "update " + baseTable + " set " + getSoftDeleteDbSet() + " where " + idEqualsSql; + softDeleteByIdInSql = "update " + baseTable + " set " + getSoftDeleteDbSet() + " where " + idBinderInLHSSqlNoAlias + " "; + } else { + softDeleteByIdSql = null; + softDeleteByIdInSql = null; + } + if (!isEmbedded()) { // parse every named update up front into sql dml for (DeployNamedUpdate namedUpdate : namedUpdates.values()) { @@ -710,11 +724,11 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { cacheHelp.initialise(); } - public SqlUpdate deleteById(Object id, List idList) { + public SqlUpdate deleteById(Object id, List idList, boolean softDelete) { if (id != null) { - return deleteById(id); + return deleteById(id, softDelete); } else { - return deleteByIdList(idList); + return deleteByIdList(idList, softDelete); } } @@ -729,9 +743,10 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { * Return SQL that can be used to delete a list of Id's without any optimistic * concurrency checking. */ - private SqlUpdate deleteByIdList(List idList) { + private SqlUpdate deleteByIdList(List idList, boolean softDelete) { - StringBuilder sb = new StringBuilder(deleteByIdInSql); + String baseSql = softDelete ? softDeleteByIdInSql : deleteByIdInSql; + StringBuilder sb = new StringBuilder(baseSql); String inClause = idBinder.getIdInValueExprDelete(idList.size()); sb.append(inClause); @@ -746,9 +761,10 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { * Return SQL that can be used to delete by Id without any optimistic * concurrency checking. */ - private SqlUpdate deleteById(Object id) { + private SqlUpdate deleteById(Object id, boolean softDelete) { - DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(deleteByIdSql); + String baseSql = softDelete ? softDeleteByIdSql : deleteByIdSql; + DefaultSqlUpdate sqlDelete = new DefaultSqlUpdate(baseSql); Object[] bindValues = idBinder.getBindValues(id); for (int i = 0; i < bindValues.length; i++) { @@ -1175,6 +1191,16 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { return deleteRecurseSkippable; } + /** + * Return true if delete can use a single SQL statement. + * + * This implies cascade delete does not continue depth wise and that this is no + * associated L2 bean caching. + */ + public boolean isDeleteByStatement() { + return deleteRecurseSkippable && !isBeanCaching(); + } + /** * Find a property annotated with @WhenCreated or @CreatedTimestamp. */ @@ -1932,6 +1958,22 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { return readAuditing; } + public boolean isSoftDelete() { + return softDelete; + } + + public void setSoftDeleteValue(EntityBean bean) { + softDeleteProperty.setSoftDeleteValue(bean); + } + + public String getSoftDeleteDbSet() { + return softDeleteProperty.getSoftDeleteDbSet(); + } + + public String getSoftDeletePredicate(String tableAlias) { + return softDeleteProperty.getSoftDeleteDbPredicate(tableAlias); + } + /** * Return true if this entity type is draftable. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index d97f11480..f7691b92d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -229,6 +229,10 @@ public class BeanProperty implements ElPropertyValue { final boolean draftReset; + final boolean softDelete; + + final Object softDeleteValue; + final boolean indexed; final String indexName; @@ -258,6 +262,8 @@ public class BeanProperty implements ElPropertyValue { this.draftDirty = deploy.isDraftDirty(); this.draftOnly = deploy.isDraftOnly(); this.draftReset = deploy.isDraftReset(); + this.softDelete = deploy.isSoftDelete(); + this.softDeleteValue = deploy.getSoftDeleteValue(); this.secondaryTable = deploy.isSecondaryTable(); if (secondaryTable) { @@ -345,6 +351,8 @@ public class BeanProperty implements ElPropertyValue { this.draftDirty = source.draftDirty; this.draftOnly = source.draftOnly; this.draftReset = source.draftReset; + this.softDelete = source.softDelete; + this.softDeleteValue = source.softDeleteValue; this.fetchEager = source.fetchEager; this.unidirectionalShadow = source.unidirectionalShadow; this.discriminator = source.discriminator; @@ -616,6 +624,25 @@ public class BeanProperty implements ElPropertyValue { } } + public String getSoftDeleteDbSet() { + return dbColumn +"=true"; + } + + /** + * Return the DB literal predicate used to filter out soft deleted rows from a query. + */ + public String getSoftDeleteDbPredicate(String tableAlias) { + return tableAlias+"."+dbColumn+"=false"; + } + + /** + * Set the soft delete property value on the bean without invoking lazy loading. + */ + public void setSoftDeleteValue(EntityBean bean) { + setValue(bean, softDeleteValue); + bean._ebean_getIntercept().setChangedProperty(propertyIndex); + } + /** * Set the value of the property without interception or * PropertyChangeSupport. @@ -1056,6 +1083,13 @@ public class BeanProperty implements ElPropertyValue { return draftReset; } + /** + * Return true if this property is the soft delete property. + */ + public boolean isSoftDelete() { + return softDelete; + } + /** * Return true if this property should be included in an Insert. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 80094dc70..5e39a9e53 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -800,7 +800,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { public IntersectionRow buildManyDeleteChildren(EntityBean parentBean, ArrayList excludeDetailIds) { - IntersectionRow row = new IntersectionRow(tableJoin.getTable()); + IntersectionRow row = new IntersectionRow(tableJoin.getTable(), targetDescriptor); if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { row.setExcludeIds(excludeDetailIds, getTargetDescriptor()); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/IntersectionRow.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/IntersectionRow.java index f459e2115..68e0d5474 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/IntersectionRow.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/IntersectionRow.java @@ -15,13 +15,21 @@ public class IntersectionRow { private final String tableName; + private final BeanDescriptor targetDescriptor; + private final LinkedHashMap values = new LinkedHashMap(); private ArrayList excludeIds; private BeanDescriptor excludeDescriptor; + public IntersectionRow(String tableName, BeanDescriptor targetDescriptor) { + this.tableName = tableName; + this.targetDescriptor = targetDescriptor; + } + public IntersectionRow(String tableName) { this.tableName = tableName; + this.targetDescriptor = null; } /** @@ -64,22 +72,20 @@ public class IntersectionRow { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SqlUpdate createDelete(EbeanServer server) { + public SqlUpdate createDelete(EbeanServer server, boolean softDelete) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); - sb.append("delete from ").append(tableName).append(" where "); - - int count = 0; - for (Map.Entry entry : values.entrySet()) { - if (count++ > 0) { - sb.append(" and "); - } - sb.append(entry.getKey()); - sb.append(" = ?"); - bindParams.setParameter(count, entry.getValue()); + if (softDelete) { + sb.append("update ").append(tableName).append(" set "); + sb.append(targetDescriptor.getSoftDeleteDbSet()); + } else { + sb.append("delete from ").append(tableName); } + sb.append(" where "); + + int count = setBindParams(bindParams, sb); if (excludeIds != null) { IdInExpression idIn = new IdInExpression(excludeIds); @@ -108,6 +114,13 @@ public class IntersectionRow { StringBuilder sb = new StringBuilder(); sb.append("delete from ").append(tableName).append(" where "); + setBindParams(bindParams, sb); + + return new DefaultSqlUpdate(server, sb.toString(), bindParams); + } + + private int setBindParams(BindParams bindParams, StringBuilder sb) { + int count = 0; for (Map.Entry entry : values.entrySet()) { if (count++ > 0) { @@ -120,6 +133,6 @@ public class IntersectionRow { bindParams.setParameter(count, entry.getValue()); } - return new DefaultSqlUpdate(server, sb.toString(), bindParams); + return count; } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index ce806d237..b4ab4b93d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -189,6 +189,9 @@ public class DeployBeanProperty { private boolean draftDirty; private boolean draftReset; + private boolean softDelete; + private String softDeleteValue = ""; + public DeployBeanProperty(DeployBeanDescriptor desc, Class propertyType, ScalarType scalarType, ScalarTypeConverter typeConverter) { this.desc = desc; this.propertyType = propertyType; @@ -872,4 +875,43 @@ public class DeployBeanProperty { public boolean isDraftReset() { return draftReset; } + + public void setSoftDelete(String softDeleteValue) { + this.softDelete = true; + this.softDeleteValue = softDeleteValue; + } + + public boolean isSoftDelete() { + return softDelete; + } + + public Object getSoftDeleteValue() { + return !softDelete ? null : "".equals(softDeleteValue) ? defaultSoftDeleteValue() : parseSoftDeleteValue(); + } + + private Object parseSoftDeleteValue() { + if (Boolean.class.equals(propertyType) || boolean.class.equals(propertyType)) { + return Boolean.parseBoolean(softDeleteValue); + } + if (Integer.class.equals(propertyType) || int.class.equals(propertyType)) { + return Integer.parseInt(softDeleteValue); + } + if (Short.class.equals(propertyType) || short.class.equals(propertyType)) { + return Short.parseShort(softDeleteValue); + } + throw new IllegalStateException("@SoftDelete on ["+getFullBeanName()+"] mapped to unsupported type propertyType["+propertyType+"]"); + } + + private Object defaultSoftDeleteValue() { + if (Boolean.class.equals(propertyType) || boolean.class.equals(propertyType)) { + return Boolean.TRUE; + } + if (Integer.class.equals(propertyType) || int.class.equals(propertyType)) { + return Integer.valueOf(1); + } + if (Short.class.equals(propertyType) || short.class.equals(propertyType)) { + return Short.valueOf("1"); + } + throw new IllegalStateException("@SoftDelete on ["+getFullBeanName()+"] mapped to unsupported type propertyType["+propertyType+"]"); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java index d8bcaad5c..700fe851f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java @@ -293,6 +293,16 @@ public class DeployBeanPropertyLists { return draftDirty; } + public BeanProperty getSoftDeleteProperty() { + + for (BeanProperty prop: nonManys) { + if (prop.isSoftDelete()) { + return prop; + } + } + return null; + } + /** * Mode used to determine which BeanPropertyAssoc to include. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index 95b7cb3d8..851fdd46f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -158,6 +158,10 @@ public class AnnotationFields extends AnnotationParser { if (get(prop, DraftReset.class) != null) { prop.setDraftReset(); } + SoftDelete softDelete = get(prop, SoftDelete.class); + if (softDelete != null) { + prop.setSoftDelete(softDelete.value()); + } DbJson dbJson = get(prop, DbJson.class); if (dbJson != null) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java index ea5968aed..ef6b4b61d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java @@ -33,7 +33,6 @@ import com.avaje.ebeaninternal.server.deploy.ManyType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.persistence.OptimisticLockException; import javax.persistence.PersistenceException; import java.util.ArrayList; import java.util.Collection; @@ -60,8 +59,6 @@ import java.util.Set; */ public final class DefaultPersister implements Persister { - private static final Logger SUM = LoggerFactory.getLogger("org.avaje.ebean.SUM"); - private static final Logger PUB = LoggerFactory.getLogger("org.avaje.ebean.PUB"); private static final Logger logger = LoggerFactory.getLogger(DefaultPersister.class); @@ -337,9 +334,10 @@ public final class DefaultPersister implements Persister { /** * Recursively delete the bean. This calls back to the EbeanServer. */ - private void deleteRecurse(Object detailBean, Transaction t) { - // NB: a new PersistRequest is made - server.delete(detailBean, t); + private void deleteRecurse(EntityBean detailBean, Transaction t, boolean softDelete) { + + Type deleteType = softDelete ? Type.SOFT_DELETE : Type.DELETE; + deleteRequest(createRequest(detailBean, t, deleteType)); } /** @@ -508,7 +506,7 @@ public final class DefaultPersister implements Persister { if (request.isDraftable()) { // we have just deleting a draft bean so now we need to delete the // associated 'live' bean. This is effectively an 'automatic publish'. - deleteRequest(createRequest(request.createReference(), t, Type.DELETE, true)); + deleteRequest(createPublishRequest(request.createReference(), t, Type.DELETE, true)); } return deleted; } @@ -541,10 +539,10 @@ public final class DefaultPersister implements Persister { } } - private void deleteList(List beanList, Transaction t) { + private void deleteList(List beanList, Transaction t, boolean softDelete) { for (int i = 0; i < beanList.size(); i++) { - EntityBean bean = (EntityBean) beanList.get(i); - delete(bean, t); +// deleteRecurse((EntityBean) beanList.get(i), t, softDelete); + delete((EntityBean) beanList.get(i), t); } } @@ -565,7 +563,7 @@ public final class DefaultPersister implements Persister { idList.add(descriptor.convertId(id)); } - delete(descriptor, null, idList, transaction); + delete(descriptor, null, idList, transaction, descriptor.isSoftDelete()); } /** @@ -577,13 +575,13 @@ public final class DefaultPersister implements Persister { // convert to appropriate type if required id = descriptor.convertId(id); - return delete(descriptor, id, null, transaction); + return delete(descriptor, id, null, transaction, descriptor.isSoftDelete()); } /** * Delete by Id or a List of Id's. */ - private int delete(BeanDescriptor descriptor, Object id, List idList, Transaction transaction) { + private int delete(BeanDescriptor descriptor, Object id, List idList, Transaction transaction, boolean softDelete) { SpiTransaction t = (SpiTransaction) transaction; if (t.isPersistCascade()) { @@ -599,7 +597,7 @@ public final class DefaultPersister implements Persister { t.logSummary("-- DeleteById of " + descriptor.getName() + " ids[" + idList + "] requires fetch of foreign key values"); } List beanList = server.findList(q, t); - deleteList(beanList, t); + deleteList(beanList, t, softDelete); return beanList.size(); } else { @@ -611,7 +609,7 @@ public final class DefaultPersister implements Persister { if (bean == null) { return 0; } else { - delete(bean, t); + deleteRecurse(bean, t, softDelete); return 1; } } @@ -623,12 +621,15 @@ public final class DefaultPersister implements Persister { BeanPropertyAssocOne[] expOnes = descriptor.propertiesOneExportedDelete(); for (int i = 0; i < expOnes.length; i++) { BeanDescriptor targetDesc = expOnes[i].getTargetDescriptor(); - if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) { - SqlUpdate sqlDelete = expOnes[i].deleteByParentId(id, idList); - executeSqlUpdate(sqlDelete, t); - } else { - List childIds = expOnes[i].findIdsByParentId(id, idList, t); - deleteChildrenById(t, targetDesc, childIds); + // only cascade soft deletes when supported by target + if (!softDelete || targetDesc.isSoftDelete()) { + if (!softDelete && targetDesc.isDeleteByStatement()) { + SqlUpdate sqlDelete = expOnes[i].deleteByParentId(id, idList); + executeSqlUpdate(sqlDelete, t); + } else { + List childIds = expOnes[i].findIdsByParentId(id, idList, t); + deleteChildrenById(t, targetDesc, childIds, softDelete); + } } } @@ -636,32 +637,37 @@ public final class DefaultPersister implements Persister { BeanPropertyAssocMany[] manys = descriptor.propertiesManyDelete(); for (int i = 0; i < manys.length; i++) { BeanDescriptor targetDesc = manys[i].getTargetDescriptor(); - if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) { - // we can just delete children with a single statement - SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList); - executeSqlUpdate(sqlDelete, t); - } else { - // we need to fetch the Id's to delete (recurse or notify L2 cache) - List childIds = manys[i].findIdsByParentId(id, idList, t, null); - if (!childIds.isEmpty()) { - delete(targetDesc, null, childIds, t); + // only cascade soft deletes when supported by target + if (!softDelete || targetDesc.isSoftDelete()) { + if (!softDelete && targetDesc.isDeleteByStatement()) { + // we can just delete children with a single statement + SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList); + executeSqlUpdate(sqlDelete, t); + } else { + // we need to fetch the Id's to delete (recurse or notify L2 cache) + List childIds = manys[i].findIdsByParentId(id, idList, t, null); + if (!childIds.isEmpty()) { + delete(targetDesc, null, childIds, t, softDelete); + } } } } } - // ManyToMany's ... delete from intersection table - BeanPropertyAssocMany[] manys = descriptor.propertiesManyToMany(); - for (int i = 0; i < manys.length; i++) { - SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList); - if (t.isLogSummary()) { - t.logSummary("-- Deleting intersection table entries: " + manys[i].getFullBeanName()); + if (!softDelete) { + // ManyToMany's ... delete from intersection table + BeanPropertyAssocMany[] manys = descriptor.propertiesManyToMany(); + for (int i = 0; i < manys.length; i++) { + SqlUpdate sqlDelete = manys[i].deleteByParentId(id, idList); + if (t.isLogSummary()) { + t.logSummary("-- Deleting intersection table entries: " + manys[i].getFullBeanName()); + } + executeSqlUpdate(sqlDelete, t); } - executeSqlUpdate(sqlDelete, t); } // delete the bean(s) - SqlUpdate deleteById = descriptor.deleteById(id, idList); + SqlUpdate deleteById = descriptor.deleteById(id, idList, softDelete); if (t.isLogSummary()) { if (idList != null) { t.logSummary("-- Deleting " + descriptor.getName() + " Ids: " + idList); @@ -924,7 +930,7 @@ public final class DefaultPersister implements Persister { EntityBean eb = (EntityBean) removedBean; if (eb._ebean_getIntercept().isLoaded()) { // only delete if the bean was loaded meaning that it is known to exist in the DB - deleteRequest(createRequest(removedBean, t, PersistRequest.Type.DELETE, saveMany.isPublish())); + deleteRequest(createPublishRequest(removedBean, t, PersistRequest.Type.DELETE, saveMany.isPublish())); } } } @@ -1031,7 +1037,7 @@ public final class DefaultPersister implements Persister { } } // deleting missing children - children not in our collected detailIds - deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds); + deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds, false); } t.depth(-1); @@ -1175,7 +1181,7 @@ public final class DefaultPersister implements Persister { // the object from the 'other' side of the ManyToMany // build a intersection row for 'delete' IntersectionRow intRow = prop.buildManyToManyMapBean(saveManyPropRequest.getParentBean(), otherDelete, saveManyPropRequest.isPublish()); - SqlUpdate sqlDelete = intRow.createDelete(server); + SqlUpdate sqlDelete = intRow.createDelete(server, false); executeSqlUpdate(sqlDelete, t); } } @@ -1206,6 +1212,7 @@ public final class DefaultPersister implements Persister { BeanDescriptor desc = request.getBeanDescriptor(); EntityBean parentBean = request.getEntityBean(); + boolean softDelete = request.isSoftDelete(); BeanPropertyAssocOne[] expOnes = desc.propertiesOneExportedDelete(); if (expOnes.length > 0) { @@ -1213,16 +1220,19 @@ public final class DefaultPersister implements Persister { DeleteUnloadedForeignKeys unloaded = null; for (int i = 0; i < expOnes.length; i++) { BeanPropertyAssocOne prop = expOnes[i]; - if (request.isLoadedProperty(prop)) { - Object detailBean = prop.getValue(parentBean); - if (detailBean != null) { - deleteRecurse(detailBean, t); + // for soft delete check cascade type also supports soft delete + if (!softDelete || prop.getTargetDescriptor().isSoftDelete()) { + if (request.isLoadedProperty(prop)) { + Object detailBean = prop.getValue(parentBean); + if (detailBean != null) { + deleteRecurse((EntityBean)detailBean, t, softDelete); + } + } else { + if (unloaded == null) { + unloaded = new DeleteUnloadedForeignKeys(server, request); + } + unloaded.add(prop); } - } else { - if (unloaded == null) { - unloaded = new DeleteUnloadedForeignKeys(server, request); - } - unloaded.add(prop); } } if (unloaded != null) { @@ -1242,23 +1252,26 @@ public final class DefaultPersister implements Persister { if (ModifyListenMode.REMOVALS.equals(manys[i].getModifyListenMode())) { // PrivateOwned ... - Object details = manys[i].getValue(parentBean); - if (details instanceof BeanCollection) { - Set modifyRemovals = ((BeanCollection) details).getModifyRemovals(); - if (modifyRemovals != null && !modifyRemovals.isEmpty()) { + // if soft delete then check target also supports soft delete + if (!softDelete || manys[i].getTargetDescriptor().isSoftDelete()) { + Object details = manys[i].getValue(parentBean); + if (details instanceof BeanCollection) { + Set modifyRemovals = ((BeanCollection) details).getModifyRemovals(); + if (modifyRemovals != null && !modifyRemovals.isEmpty()) { - // delete the orphans that have been removed from the collection - for (Object detail : modifyRemovals) { - EntityBean detailBean = (EntityBean) detail; - if (manys[i].hasId(detailBean)) { - deleteRecurse(detailBean, t); + // delete the orphans that have been removed from the collection + for (Object detail : modifyRemovals) { + EntityBean detailBean = (EntityBean) detail; + if (manys[i].hasId(detailBean)) { + deleteRecurse(detailBean, t, softDelete); + } } } } } } - deleteManyDetails(t, desc, parentBean, manys[i], null); + deleteManyDetails(t, desc, parentBean, manys[i], null, softDelete); } } @@ -1275,23 +1288,25 @@ public final class DefaultPersister implements Persister { *

*/ private void deleteManyDetails(SpiTransaction t, BeanDescriptor desc, EntityBean parentBean, - BeanPropertyAssocMany many, ArrayList excludeDetailIds) { + BeanPropertyAssocMany many, ArrayList excludeDetailIds, boolean softDelete) { if (many.getCascadeInfo().isDelete()) { // cascade delete the beans in the collection BeanDescriptor targetDesc = many.getTargetDescriptor(); - if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) { - // Just delete all the children with one statement - IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds); - SqlUpdate sqlDelete = intRow.createDelete(server); - executeSqlUpdate(sqlDelete, t); + if (!softDelete || targetDesc.isSoftDelete()) { + if (targetDesc.isDeleteRecurseSkippable() && !targetDesc.isBeanCaching()) { + // Just delete all the children with one statement + IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds); + SqlUpdate sqlDelete = intRow.createDelete(server, softDelete); + executeSqlUpdate(sqlDelete, t); - } else { - // Delete recurse using the Id values of the children - Object parentId = desc.getId(parentBean); - List idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds); - if (!idsByParentId.isEmpty()) { - deleteChildrenById(t, targetDesc, idsByParentId); + } else { + // Delete recurse using the Id values of the children + Object parentId = desc.getId(parentBean); + List idsByParentId = many.findIdsByParentId(parentId, null, t, excludeDetailIds); + if (!idsByParentId.isEmpty()) { + deleteChildrenById(t, targetDesc, idsByParentId, softDelete); + } } } } @@ -1302,7 +1317,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) { + private void deleteChildrenById(SpiTransaction t, BeanDescriptor targetDesc, List childIds, boolean softDelete) { if (targetDesc.propertiesManyToMany().length > 0) { // convert into a list of reference objects and perform delete by object @@ -1310,11 +1325,11 @@ public final class DefaultPersister implements Persister { for (Object id : childIds) { refList.add(targetDesc.createReference(null, id)); } - deleteList(refList, t); + deleteList(refList, t, softDelete); } else { // perform delete by statement if possible - delete(targetDesc, null, childIds, t); + delete(targetDesc, null, childIds, t, softDelete); } } @@ -1375,9 +1390,7 @@ public final class DefaultPersister implements Persister { */ private void deleteAssocOne(PersistRequestBean request) { - BeanDescriptor desc = request.getBeanDescriptor(); - BeanPropertyAssocOne[] ones = desc.propertiesOneImportedDelete(); - + BeanPropertyAssocOne[] ones = request.getBeanDescriptor().propertiesOneImportedDelete(); for (int i = 0; i < ones.length; i++) { BeanPropertyAssocOne prop = ones[i]; if (request.isLoadedProperty(prop)) { @@ -1385,7 +1398,7 @@ public final class DefaultPersister implements Persister { if (detailBean != null) { EntityBean detail = (EntityBean) detailBean; if (prop.hasId(detail)) { - deleteRecurse(detail, request.getTransaction()); + deleteRecurse(detail, request.getTransaction(), request.isSoftDelete()); } } } @@ -1426,18 +1439,25 @@ public final class DefaultPersister implements Persister { * perform an insert, update or delete. */ private PersistRequestBean createRequest(T bean, Transaction t, PersistRequest.Type type) { - return createRequest(bean, t, type, false); + return createRequestInternal(bean, t, type, false, false); } /** * Create the Persist Request Object additionally specifying the publish status. */ - private PersistRequestBean createRequest(T bean, Transaction t, PersistRequest.Type type, boolean publish) { + private PersistRequestBean createPublishRequest(T bean, Transaction t, PersistRequest.Type type, boolean publish) { + return createRequestInternal(bean, t, type, false, publish); + } + + /** + * Create the Persist Request Object additionally specifying the publish status. + */ + private PersistRequestBean createRequestInternal(T bean, Transaction t, PersistRequest.Type type, boolean saveRecurse, boolean publish) { BeanManager mgr = getBeanManager(bean); if (mgr == null) { throw new PersistenceException(errNotRegistered(bean.getClass())); } - return createRequest(bean, t, null, mgr, type, false, publish); + return createRequest(bean, t, null, mgr, type, saveRecurse, publish); } /** @@ -1471,6 +1491,10 @@ public final class DefaultPersister implements Persister { private PersistRequestBean createRequest(T bean, Transaction t, Object parentBean, BeanManager mgr, PersistRequest.Type type, boolean saveRecurse, boolean publish) { + if (type == Type.DELETE && mgr.getBeanDescriptor().isSoftDelete()) { + type = Type.SOFT_DELETE; + } + return new PersistRequestBean(server, bean, parentBean, mgr, (SpiTransaction) t, persistExecute, type, saveRecurse, publish); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index c507de345..2919e12d3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -268,6 +268,8 @@ public class CQueryBuilder { SqlTree sqlTree = createSqlTree(request, predicates, getHistorySupport(query), getDraftSupport(query)); if (query.isAsOfQuery()) { sqlTree.addAsOfTableAlias(query); + } else if (SpiQuery.TemporalMode.CURRENT == query.getTemporalMode()) { + sqlTree.addSoftDeletePredicate(query); } SqlLimitResponse res = buildSql(null, request, predicates, sqlTree); @@ -478,6 +480,23 @@ public class CQueryBuilder { } } + if (!query.isIncludeSoftDeletes()) { + List softDeletePredicates = query.getSoftDeletePredicates(); + if (softDeletePredicates != null) { + if (!hasWhere) { + sb.append(" where "); + } else { + sb.append("and "); + } + for (int i = 0; i < softDeletePredicates.size(); i++) { + if (i > 0) { + sb.append(" and "); + } + sb.append(softDeletePredicates.get(i)); + } + } + } + if (dbOrderBy != null) { sb.append(" order by ").append(dbOrderBy); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java index 06b2e6e40..aaca0a912 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTree.java @@ -89,6 +89,13 @@ public class SqlTree { rootNode.addAsOfTableAlias(query); } + /** + * Recurse through the tree adding soft delete predicates as necessary. + */ + public void addSoftDeletePredicate(SpiQuery query) { + rootNode.addSoftDeletePredicate(query); + } + /** * Build a select expression chain for RawSql. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java index dc32662f1..6700f8e16 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java @@ -39,6 +39,11 @@ public interface SqlTreeNode { */ void addAsOfTableAlias(SpiQuery query); + /** + * Recurse through the tree adding soft delete predicates if necessary. + */ + void addSoftDeletePredicate(SpiQuery query); + /** * Load the appropriate information from the SqlSelectReader. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java index 3f68e5f63..a0a20e710 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -480,6 +480,16 @@ public class SqlTreeNodeBean implements SqlTreeNode { ctx.popJoin(); } + public void addSoftDeletePredicate(SpiQuery query) { + + if (desc.isSoftDelete()) { + query.addSoftDeletePredicate(desc.getSoftDeletePredicate(baseTableAlias)); + } + for (int i = 0; i < children.length; i++) { + children[i].addSoftDeletePredicate(query); + } + } + public void addAsOfTableAlias(SpiQuery query) { // if history on this bean type add it's alias // for each alias we add an effect date predicate diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java index f0574d3ce..804b66d8b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java @@ -47,6 +47,11 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode { // nothing to do here } + @Override + public void addSoftDeletePredicate(SpiQuery query) { + // nothing to do here + } + /** * Return true if the extra join is a many join. *

diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java index 396d01891..eaa0996c0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java @@ -44,6 +44,11 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode { // do nothing here ... } + @Override + public void addSoftDeletePredicate(SpiQuery query) { + // do nothing here ... + } + /** * Append to the FROM clause for this node. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 2dfe91490..b3f5fdda2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -180,6 +180,8 @@ public class DefaultOrmQuery implements SpiQuery { private Timestamp versionsStart; private Timestamp versionsEnd; + private List softDeletePredicates; + private boolean disableReadAudit; private int bufferFetchSizeHint; @@ -287,6 +289,19 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + @Override + public void addSoftDeletePredicate(String softDeletePredicate) { + if (softDeletePredicates == null) { + softDeletePredicates = new ArrayList(); + } + softDeletePredicates.add(softDeletePredicate); + } + + @Override + public List getSoftDeletePredicates() { + return softDeletePredicates; + } + /** * This table alias is for a @History entity involved in the query and as * such we need to add a 'as of predicate' to the query using this alias. @@ -317,6 +332,12 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + @Override + public Query includeSoftDeletes() { + this.temporalMode = TemporalMode.SOFT_DELETED; + return this; + } + /** * Set the BeanDescriptor for the root type of this query. */ @@ -660,6 +681,11 @@ public class DefaultOrmQuery implements SpiQuery { return TemporalMode.DRAFT == temporalMode; } + @Override + public boolean isIncludeSoftDeletes() { + return TemporalMode.SOFT_DELETED == temporalMode; + } + public void setMode(Mode mode) { this.mode = mode; } diff --git a/src/test/java/com/avaje/tests/model/softdelete/BaseSoftDelete.java b/src/test/java/com/avaje/tests/model/softdelete/BaseSoftDelete.java new file mode 100644 index 000000000..829dc171c --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/BaseSoftDelete.java @@ -0,0 +1,45 @@ +package com.avaje.tests.model.softdelete; + +import com.avaje.ebean.annotation.SoftDelete; + +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Version; + +@MappedSuperclass +public class BaseSoftDelete { + + @Id + Long id; + + @Version + Long version; + + @SoftDelete + boolean deleted; + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java b/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java new file mode 100644 index 000000000..9058410ee --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java @@ -0,0 +1,27 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.Entity; + +@Entity +public class EBasicSoftDelete extends BaseSoftDelete { + + String name; + + String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java new file mode 100644 index 000000000..efade738c --- /dev/null +++ b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java @@ -0,0 +1,43 @@ +package com.avaje.tests.softdelete; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.SqlQuery; +import com.avaje.ebean.SqlRow; +import com.avaje.tests.model.softdelete.EBasicSoftDelete; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestSoftDeleteBasic extends BaseTestCase { + + @Test + public void test() { + + EBasicSoftDelete bean = new EBasicSoftDelete(); + bean.setName("one"); + + Ebean.save(bean); + + Ebean.delete(bean); + + SqlQuery sqlQuery = Ebean.createSqlQuery("select * from ebasic_soft_delete where id=?"); + sqlQuery.setParameter(1, bean.getId()); + SqlRow sqlRow = sqlQuery.findUnique(); + assertThat(sqlRow).isNotNull(); + + EBasicSoftDelete findNormal = Ebean.find(EBasicSoftDelete.class) + .setId(bean.getId()) + .findUnique(); + + assertThat(findNormal).isNull(); + + EBasicSoftDelete findInclude = Ebean.find(EBasicSoftDelete.class) + .setId(bean.getId()) + .includeSoftDeletes() + .findUnique(); + + assertThat(findInclude).isNotNull(); + + } +} From f39a684c8ef99b361a68069c8e2c50ff8ec99e25 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Wed, 2 Dec 2015 21:38:34 +1300 Subject: [PATCH 2/3] #118 - ENH: Add support for for Soft Deletes (Logical Deletion) ... --- .../avaje/ebean/annotation/SoftDelete.java | 16 ++-- .../server/deploy/BeanProperty.java | 31 +++++-- .../deploy/meta/DeployBeanProperty.java | 36 +------- .../server/deploy/parse/AnnotationFields.java | 2 +- .../server/loadcontext/DLoadBeanContext.java | 17 +--- .../server/loadcontext/DLoadContext.java | 56 ++++++------- .../server/loadcontext/DLoadManyContext.java | 18 +--- .../server/persist/DefaultPersister.java | 7 +- .../server/type/ScalarTypeBoolean.java | 50 +++++++++++ .../model/softdelete/EBasicNoSDChild.java | 72 ++++++++++++++++ .../tests/model/softdelete/EBasicSDChild.java | 48 +++++++++++ .../model/softdelete/EBasicSoftDelete.java | 34 ++++++++ .../tests/model/softdelete/ESoftDelRole.java | 35 ++++++++ .../tests/model/softdelete/ESoftDelUser.java | 38 +++++++++ .../tests/softdelete/TestSoftDeleteBasic.java | 84 +++++++++++++++++++ .../softdelete/TestSoftDeleteManyToMany.java | 43 ++++++++++ 16 files changed, 469 insertions(+), 118 deletions(-) create mode 100644 src/test/java/com/avaje/tests/model/softdelete/EBasicNoSDChild.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/EBasicSDChild.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/ESoftDelRole.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/ESoftDelUser.java create mode 100644 src/test/java/com/avaje/tests/softdelete/TestSoftDeleteManyToMany.java diff --git a/src/main/java/com/avaje/ebean/annotation/SoftDelete.java b/src/main/java/com/avaje/ebean/annotation/SoftDelete.java index 56fde390e..3cc7a1f35 100644 --- a/src/main/java/com/avaje/ebean/annotation/SoftDelete.java +++ b/src/main/java/com/avaje/ebean/annotation/SoftDelete.java @@ -9,19 +9,17 @@ import java.lang.annotation.Target; * Used to indicate a property on an entity bean used to control 'soft delete' * (also known as 'logical delete'). *

- * The property should be of type boolean, int or short. + * The property should be of type boolean. *

+ *
{@code
+ *
+ * @SoftDelete
+ * boolean deleted;
+ *
+ * }
*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface SoftDelete { - /** - * Specify the bind value that matches 'deleted' state. - *

- * If not specified then for boolean this is true and - * for int and short this value is 1. - *

- */ - String value() default ""; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index f7691b92d..0f2e02d52 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -22,6 +22,7 @@ import com.avaje.ebeaninternal.server.text.json.ReadJson; import com.avaje.ebeaninternal.server.text.json.WriteJson; import com.avaje.ebeaninternal.server.type.DataBind; import com.avaje.ebeaninternal.server.type.ScalarType; +import com.avaje.ebeaninternal.server.type.ScalarTypeBoolean; import com.avaje.ebeaninternal.util.ValueUtil; import com.fasterxml.jackson.core.JsonToken; import org.slf4j.Logger; @@ -231,7 +232,9 @@ public class BeanProperty implements ElPropertyValue { final boolean softDelete; - final Object softDeleteValue; + final String softDeleteDbSet; + + final String softDeleteDbPredicate; final boolean indexed; @@ -262,9 +265,6 @@ public class BeanProperty implements ElPropertyValue { this.draftDirty = deploy.isDraftDirty(); this.draftOnly = deploy.isDraftOnly(); this.draftReset = deploy.isDraftReset(); - this.softDelete = deploy.isSoftDelete(); - this.softDeleteValue = deploy.getSoftDeleteValue(); - this.secondaryTable = deploy.isSecondaryTable(); if (secondaryTable) { this.secondaryTableJoin = new TableJoin(deploy.getSecondaryTableJoin()); @@ -308,6 +308,16 @@ public class BeanProperty implements ElPropertyValue { this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), false, null); this.elPlaceHolderEncrypted = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), dbEncrypted, dbColumn); + this.softDelete = deploy.isSoftDelete(); + if (softDelete) { + ScalarTypeBoolean.BooleanBase boolType = (ScalarTypeBoolean.BooleanBase)scalarType; + this.softDeleteDbSet = dbColumn+"="+boolType.getDbTrueLiteral(); + this.softDeleteDbPredicate = dbColumn+"="+boolType.getDbFalseLiteral(); + } else { + this.softDeleteDbSet = null; + this.softDeleteDbPredicate = null; + } + this.jsonSerialize = deploy.isJsonSerialize(); this.jsonDeserialize = deploy.isJsonDeserialize(); } @@ -352,7 +362,8 @@ public class BeanProperty implements ElPropertyValue { this.draftOnly = source.draftOnly; this.draftReset = source.draftReset; this.softDelete = source.softDelete; - this.softDeleteValue = source.softDeleteValue; + this.softDeleteDbSet = source.softDeleteDbSet; + this.softDeleteDbPredicate = source.softDeleteDbPredicate; this.fetchEager = source.fetchEager; this.unidirectionalShadow = source.unidirectionalShadow; this.discriminator = source.discriminator; @@ -624,22 +635,26 @@ public class BeanProperty implements ElPropertyValue { } } + /** + * Return the DB literal expression to set the deleted state to true. + */ public String getSoftDeleteDbSet() { - return dbColumn +"=true"; + return softDeleteDbSet; } /** * Return the DB literal predicate used to filter out soft deleted rows from a query. */ public String getSoftDeleteDbPredicate(String tableAlias) { - return tableAlias+"."+dbColumn+"=false"; + return tableAlias+"."+softDeleteDbPredicate; } /** * Set the soft delete property value on the bean without invoking lazy loading. */ public void setSoftDeleteValue(EntityBean bean) { - setValue(bean, softDeleteValue); + // assumes boolean deleted true being set which is ok limitation for now + setValue(bean, true); bean._ebean_getIntercept().setChangedProperty(propertyIndex); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index b4ab4b93d..30858aac0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.deploy.meta; import com.avaje.ebean.annotation.CreatedTimestamp; +import com.avaje.ebean.annotation.SoftDelete; import com.avaje.ebean.annotation.UpdatedTimestamp; import com.avaje.ebean.annotation.WhenCreated; import com.avaje.ebean.annotation.WhenModified; @@ -190,7 +191,6 @@ public class DeployBeanProperty { private boolean draftReset; private boolean softDelete; - private String softDeleteValue = ""; public DeployBeanProperty(DeployBeanDescriptor desc, Class propertyType, ScalarType scalarType, ScalarTypeConverter typeConverter) { this.desc = desc; @@ -224,6 +224,8 @@ public class DeployBeanProperty { return AUDITCOLUMN_ORDER; } else if (field.getAnnotation(Version.class) != null) { return VERSIONCOLUMN_ORDER; + } else if (field.getAnnotation(SoftDelete.class) != null) { + return VERSIONCOLUMN_ORDER; } return 0; } @@ -876,42 +878,12 @@ public class DeployBeanProperty { return draftReset; } - public void setSoftDelete(String softDeleteValue) { + public void setSoftDelete() { this.softDelete = true; - this.softDeleteValue = softDeleteValue; } public boolean isSoftDelete() { return softDelete; } - public Object getSoftDeleteValue() { - return !softDelete ? null : "".equals(softDeleteValue) ? defaultSoftDeleteValue() : parseSoftDeleteValue(); - } - - private Object parseSoftDeleteValue() { - if (Boolean.class.equals(propertyType) || boolean.class.equals(propertyType)) { - return Boolean.parseBoolean(softDeleteValue); - } - if (Integer.class.equals(propertyType) || int.class.equals(propertyType)) { - return Integer.parseInt(softDeleteValue); - } - if (Short.class.equals(propertyType) || short.class.equals(propertyType)) { - return Short.parseShort(softDeleteValue); - } - throw new IllegalStateException("@SoftDelete on ["+getFullBeanName()+"] mapped to unsupported type propertyType["+propertyType+"]"); - } - - private Object defaultSoftDeleteValue() { - if (Boolean.class.equals(propertyType) || boolean.class.equals(propertyType)) { - return Boolean.TRUE; - } - if (Integer.class.equals(propertyType) || int.class.equals(propertyType)) { - return Integer.valueOf(1); - } - if (Short.class.equals(propertyType) || short.class.equals(propertyType)) { - return Short.valueOf("1"); - } - throw new IllegalStateException("@SoftDelete on ["+getFullBeanName()+"] mapped to unsupported type propertyType["+propertyType+"]"); - } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index 851fdd46f..fde8d0041 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -160,7 +160,7 @@ public class AnnotationFields extends AnnotationParser { } SoftDelete softDelete = get(prop, SoftDelete.class); if (softDelete != null) { - prop.setSoftDelete(softDelete.value()); + prop.setSoftDelete(); } DbJson dbJson = get(prop, DbJson.class); diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java index f591337c7..254fd6d78 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -45,27 +45,12 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex protected void configureQuery(SpiQuery query, String lazyLoadProperty) { - // propagate the readOnly state - if (parent.isReadOnly() != null) { - query.setReadOnly(parent.isReadOnly()); - } - // propagate the asOf and lazy loading mode - query.setDisableLazyLoading(parent.isDisableLazyLoading()); - query.asOf(parent.getAsOf()); + parent.propagateQueryState(query); query.setParentNode(objectGraphNode); query.setLazyLoadProperty(lazyLoadProperty); - if (parent.isAsDraft()) { - query.asDraft(); - } - if (parent.isDisableReadAudit()) { - query.setDisableReadAuditing(); - } if (queryProps != null) { queryProps.configureBeanQuery(query); } - if (parent.isUseAutoTune()) { - query.setAutoTune(true); - } } protected void register(EntityBeanIntercept ebi) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java index 99b563d14..db7f1c366 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java @@ -43,6 +43,7 @@ public class DLoadContext implements LoadContext { private final int defaultBatchSize; private final boolean disableLazyLoading; private final boolean disableReadAudit; + private final boolean includeSoftDeletes; /** * The path relative to the root of the object graph. @@ -67,6 +68,7 @@ public class DLoadContext implements LoadContext { SpiQuery query = request.getQuery(); this.asOf = query.getAsOf(); this.asDraft = query.isAsDraft(); + this.includeSoftDeletes = query.isIncludeSoftDeletes(); this.readOnly = query.isReadOnly(); this.disableReadAudit = query.isDisableReadAudit(); this.disableLazyLoading = query.isDisableLazyLoading(); @@ -201,10 +203,6 @@ public class DLoadContext implements LoadContext { return new ObjectGraphNode(origin, path); } - public boolean isUseAutoTune() { - return useProfiling; - } - protected String getFullPath(String path) { if (relativePath == null) { return path; @@ -225,34 +223,6 @@ public class DLoadContext implements LoadContext { return readOnly; } - /** - * Return the 'as of' timestamp that should propagate to secondary queries. - */ - protected Timestamp getAsOf() { - return asOf; - } - - /** - * Return true if the root query is a 'asDraft' query that should propagate to secondary queries. - */ - protected boolean isAsDraft() { - return asDraft; - } - - /** - * Return true if disable read auditing should propagate to secondary queries. - */ - protected boolean isDisableReadAudit() { - return disableReadAudit; - } - - /** - * Return true if disable lazy loading should propagate to secondary queries. - */ - protected boolean isDisableLazyLoading() { - return disableLazyLoading; - } - public PersistenceContext getPersistenceContext() { return persistenceContext; } @@ -335,4 +305,26 @@ public class DLoadContext implements LoadContext { return desc.getBeanPropertyFromPath(path); } + /** + * Propagate the original query settings (draft, asOf etc) to the secondary queries. + */ + public void propagateQueryState(SpiQuery query) { + if (readOnly != null) { + query.setReadOnly(readOnly); + } + query.setDisableLazyLoading(disableLazyLoading); + query.asOf(asOf); + if (asDraft) { + query.asDraft(); + } + if (includeSoftDeletes) { + query.includeSoftDeletes(); + } + if (disableReadAudit) { + query.setDisableReadAuditing(); + } + if (useProfiling) { + query.setAutoTune(true); + } + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java index 6e5f8a763..6ed015ce0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -56,27 +56,11 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex public void configureQuery(SpiQuery query) { - // propagate the readOnly state - if (parent.isReadOnly() != null) { - query.setReadOnly(parent.isReadOnly()); - } - - // propagate the asOf and lazy loading mode - query.setDisableLazyLoading(parent.isDisableLazyLoading()); - query.asOf(parent.getAsOf()); + parent.propagateQueryState(query); query.setParentNode(objectGraphNode); - if (parent.isAsDraft()) { - query.asDraft(); - } - if (parent.isDisableReadAudit()) { - query.setDisableReadAuditing(); - } if (queryProps != null) { queryProps.configureBeanQuery(query); } - if (parent.isUseAutoTune()) { - query.setAutoTune(true); - } } public BeanPropertyAssocMany getBeanProperty() { diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java index ef6b4b61d..711147526 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java @@ -1245,9 +1245,10 @@ public final class DefaultPersister implements Persister { BeanPropertyAssocMany[] manys = desc.propertiesManyDelete(); for (int i = 0; i < manys.length; i++) { if (manys[i].isManyToMany()) { - // delete associated rows from intersection table - deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish()); - + if (!softDelete) { + // delete associated rows from intersection table (but not during soft delete) + deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish()); + } } else { if (ModifyListenMode.REMOVALS.equals(manys[i].getModifyListenMode())) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBoolean.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBoolean.java index 94d85293e..154d4a331 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBoolean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBoolean.java @@ -29,6 +29,16 @@ public class ScalarTypeBoolean { super(true, Types.BOOLEAN); } + @Override + public String getDbFalseLiteral() { + return "false"; + } + + @Override + public String getDbTrueLiteral() { + return "true"; + } + public Boolean toBeanType(Object value) { return BasicTypeConverter.toBoolean(value); } @@ -68,6 +78,16 @@ public class ScalarTypeBoolean { super(true, Types.BIT); } + @Override + public String getDbFalseLiteral() { + return "0"; + } + + @Override + public String getDbTrueLiteral() { + return "1"; + } + public Boolean toBeanType(Object value) { return BasicTypeConverter.toBoolean(value); } @@ -106,6 +126,16 @@ public class ScalarTypeBoolean { this.falseValue = falseValue; } + @Override + public String getDbFalseLiteral() { + return falseValue.toString(); + } + + @Override + public String getDbTrueLiteral() { + return trueValue.toString(); + } + @Override public int getLength() { return 1; @@ -179,6 +209,16 @@ public class ScalarTypeBoolean { this.falseValue = falseValue; } + @Override + public String getDbFalseLiteral() { + return "'"+falseValue+"'"; + } + + @Override + public String getDbTrueLiteral() { + return "'"+trueValue+"'"; + } + @Override public int getLength() { // typically this will return 1 @@ -245,6 +285,16 @@ public class ScalarTypeBoolean { super(Boolean.class, jdbcNative, jdbcType); } + /** + * Return the DB literal value for false. + */ + public abstract String getDbFalseLiteral(); + + /** + * Return the DB literal value for true. + */ + public abstract String getDbTrueLiteral(); + public String formatValue(Boolean t) { return t.toString(); } diff --git a/src/test/java/com/avaje/tests/model/softdelete/EBasicNoSDChild.java b/src/test/java/com/avaje/tests/model/softdelete/EBasicNoSDChild.java new file mode 100644 index 000000000..5b5c51f6a --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/EBasicNoSDChild.java @@ -0,0 +1,72 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Version; + +@Entity +public class EBasicNoSDChild { + + @Id + Long id; + + @Version + Long version; + + @ManyToOne(optional = false) + EBasicSoftDelete owner; + + String childName; + + long amount; + + public EBasicNoSDChild(EBasicSoftDelete owner, String childName, long amount) { + this.owner = owner; + this.childName = childName; + this.amount = amount; + } + + public EBasicNoSDChild() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public EBasicSoftDelete getOwner() { + return owner; + } + + public void setOwner(EBasicSoftDelete owner) { + this.owner = owner; + } + + public String getChildName() { + return childName; + } + + public void setChildName(String childName) { + this.childName = childName; + } + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/EBasicSDChild.java b/src/test/java/com/avaje/tests/model/softdelete/EBasicSDChild.java new file mode 100644 index 000000000..ceee82f97 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/EBasicSDChild.java @@ -0,0 +1,48 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.Entity; +import javax.persistence.ManyToOne; + +@Entity +public class EBasicSDChild extends BaseSoftDelete { + + @ManyToOne(optional = false) + EBasicSoftDelete owner; + + String childName; + + long amount; + + public EBasicSDChild(EBasicSoftDelete owner, String childName, long amount) { + this.owner = owner; + this.childName = childName; + this.amount = amount; + } + + public EBasicSDChild() { + } + + public EBasicSoftDelete getOwner() { + return owner; + } + + public void setOwner(EBasicSoftDelete owner) { + this.owner = owner; + } + + public String getChildName() { + return childName; + } + + public void setChildName(String childName) { + this.childName = childName; + } + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java b/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java index 9058410ee..640d4296a 100644 --- a/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java +++ b/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java @@ -1,6 +1,9 @@ package com.avaje.tests.model.softdelete; +import javax.persistence.CascadeType; import javax.persistence.Entity; +import javax.persistence.OneToMany; +import java.util.List; @Entity public class EBasicSoftDelete extends BaseSoftDelete { @@ -9,6 +12,13 @@ public class EBasicSoftDelete extends BaseSoftDelete { String description; + @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL) + List children; + + @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL) + List nosdChildren; + + public String getName() { return name; } @@ -24,4 +34,28 @@ public class EBasicSoftDelete extends BaseSoftDelete { public void setDescription(String description) { this.description = description; } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(String childName, long amount) { + getChildren().add(new EBasicSDChild(this, childName, amount)); + } + + public List getNosdChildren() { + return nosdChildren; + } + + public void setNosdChildren(List nosdChildren) { + this.nosdChildren = nosdChildren; + } + + public void addNoSoftDeleteChild(String childName, long amount) { + getNosdChildren().add(new EBasicNoSDChild(this, childName, amount)); + } } diff --git a/src/test/java/com/avaje/tests/model/softdelete/ESoftDelRole.java b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelRole.java new file mode 100644 index 000000000..44f29ca4f --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelRole.java @@ -0,0 +1,35 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.ManyToMany; +import java.util.List; + +@Entity +public class ESoftDelRole extends BaseSoftDelete { + + String roleName; + + @ManyToMany(cascade = CascadeType.ALL) + List users; + + public ESoftDelRole(String roleName) { + this.roleName = roleName; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/ESoftDelUser.java b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelUser.java new file mode 100644 index 000000000..e1b41101d --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelUser.java @@ -0,0 +1,38 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.ManyToMany; +import java.util.List; + +@Entity +public class ESoftDelUser extends BaseSoftDelete { + + String userName; + + @ManyToMany(cascade = CascadeType.ALL) + List roles; + + public ESoftDelUser(String userName) { + this.userName = userName; + } + + public ESoftDelUser() { + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } +} diff --git a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java index efade738c..4954ac54d 100644 --- a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java +++ b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java @@ -2,11 +2,15 @@ package com.avaje.tests.softdelete; import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; import com.avaje.ebean.SqlQuery; import com.avaje.ebean.SqlRow; import com.avaje.tests.model.softdelete.EBasicSoftDelete; +import org.avaje.ebeantest.LoggedSqlCollector; import org.junit.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; public class TestSoftDeleteBasic extends BaseTestCase { @@ -40,4 +44,84 @@ public class TestSoftDeleteBasic extends BaseTestCase { assertThat(findInclude).isNotNull(); } + + @Test + public void testCascadeSaveDelete() { + + EBasicSoftDelete bean = new EBasicSoftDelete(); + bean.setName("cascadeOne"); + bean.addChild("child1", 10); + bean.addChild("child2", 20); + bean.addChild("child3", 30); + bean.addNoSoftDeleteChild("nsd1", 101); + bean.addNoSoftDeleteChild("nsd2", 102); + + Ebean.save(bean); + + LoggedSqlCollector.start(); + + Ebean.delete(bean); + + List loggedSql = LoggedSqlCollector.stop(); + + // The children without SoftDelete are left as is (so no third statement) + assertThat(loggedSql).hasSize(2); + + // first statement is a single bulk update of the children with SoftDelete + assertThat(loggedSql.get(0)).contains("update ebasic_sdchild set deleted="); + assertThat(loggedSql.get(0)).contains("where owner_id = ?"); + + // second statement is the top level bean + assertThat(loggedSql.get(1)).contains("update ebasic_soft_delete set version=?, deleted=? where id=? and version=?"); + + } + + + @Test + public void testFetch() { + + EBasicSoftDelete bean = new EBasicSoftDelete(); + bean.setName("cascadeOne"); + bean.addChild("child1", 10); + bean.addChild("child2", 20); + bean.addChild("child3", 30); + bean.addNoSoftDeleteChild("nsd1", 101); + bean.addNoSoftDeleteChild("nsd2", 102); + + Ebean.save(bean); + + Ebean.delete(bean.getChildren().get(1)); + + LoggedSqlCollector.start(); + + Query query1 = + Ebean.find(EBasicSoftDelete.class) + .fetch("children") + .where().eq("id", bean.getId()) + .query(); + + List fetch1 = query1.findList(); + String generatedSql = query1.getGeneratedSql(); + + // first statement is a single bulk update of the children with SoftDelete + assertThat(generatedSql).contains("t0.deleted="); + assertThat(generatedSql).contains("t1.deleted="); + assertThat(fetch1.get(0).getChildren()).hasSize(2); + + assertThat(fetch1.get(0).getNosdChildren()).hasSize(2); + + + // fetch again using lazy loading + EBasicSoftDelete fetchWithLazy = Ebean.find(EBasicSoftDelete.class, bean.getId()); + assertThat(fetchWithLazy.getChildren()).hasSize(2); + + // fetch includeSoftDeletes using lazy loading + EBasicSoftDelete fetchAllWithLazy = + Ebean.find(EBasicSoftDelete.class) + .setId(bean.getId()) + .includeSoftDeletes() + .findUnique(); + + assertThat(fetchAllWithLazy.getChildren()).hasSize(3); + } } diff --git a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteManyToMany.java b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteManyToMany.java new file mode 100644 index 000000000..b6852d5ce --- /dev/null +++ b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteManyToMany.java @@ -0,0 +1,43 @@ +package com.avaje.tests.softdelete; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.softdelete.ESoftDelRole; +import com.avaje.tests.model.softdelete.ESoftDelUser; +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestSoftDeleteManyToMany extends BaseTestCase { + + @Test + public void test() { + + ESoftDelRole role1 = new ESoftDelRole("role1"); + ESoftDelRole role2 = new ESoftDelRole("role2"); + + Ebean.save(role1); + Ebean.save(role2); + + ESoftDelUser user1 = new ESoftDelUser("user1"); + user1.getRoles().add(role1); + user1.getRoles().add(role2); + + Ebean.save(user1); + + + LoggedSqlCollector.start(); + Ebean.delete(user1); + + List loggedSql = LoggedSqlCollector.stop(); + + // No Delete from the relationship table + assertThat(loggedSql).hasSize(1); + assertThat(loggedSql.get(0)).contains("update esoft_del_user set version=?, deleted=? where id=? and version=?;"); + + } + +} From aea7a7fd65929fc0a0e0e11740c1816a07bde375 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Wed, 2 Dec 2015 23:46:26 +1300 Subject: [PATCH 3/3] #118 - ENH: Add support for for Soft Deletes (Logical Deletion) ... --- src/main/java/com/avaje/ebean/Ebean.java | 18 ++++ .../java/com/avaje/ebean/EbeanServer.java | 20 ++++ .../server/core/DefaultServer.java | 33 +++++-- .../server/core/PersistRequest.java | 2 +- .../server/core/PersistRequestBean.java | 6 ++ .../ebeaninternal/server/core/Persister.java | 2 +- .../server/deploy/BeanDescriptor.java | 1 + .../server/persist/BatchedBeanHolder.java | 1 + .../server/persist/DefaultPersister.java | 28 ++++-- .../server/transaction/BeanPersistIds.java | 1 + .../ebeaninternal/api/TDSpiEbeanServer.java | 20 ++++ .../tests/model/softdelete/ESoftDelDown.java | 21 ++++ .../tests/model/softdelete/ESoftDelMid.java | 64 ++++++++++++ .../tests/model/softdelete/ESoftDelTop.java | 44 +++++++++ .../tests/model/softdelete/ESoftDelUp.java | 21 ++++ .../tests/softdelete/TestSoftDeleteBasic.java | 33 ++++++- .../tests/softdelete/TestSoftDeleteTop.java | 97 +++++++++++++++++++ 17 files changed, 392 insertions(+), 20 deletions(-) create mode 100644 src/test/java/com/avaje/tests/model/softdelete/ESoftDelDown.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/ESoftDelMid.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/ESoftDelTop.java create mode 100644 src/test/java/com/avaje/tests/model/softdelete/ESoftDelUp.java create mode 100644 src/test/java/com/avaje/tests/softdelete/TestSoftDeleteTop.java diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java index c478e90fe..1255f3a2c 100644 --- a/src/main/java/com/avaje/ebean/Ebean.java +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -704,6 +704,10 @@ public final class Ebean { * you automatically. *

*

+ * If the bean is configured with @SoftDelete then this will perform a soft + * delete rather than a hard/permanent delete. + *

+ *

* If the Bean does not have a version property (or loaded version property) and * the bean does not exist then this returns false indicating that nothing was * deleted. Note that, if JDBC batch mode is used then this always returns true. @@ -713,6 +717,13 @@ public final class Ebean { return serverMgr.getDefaultServer().delete(bean); } + /** + * Delete the bean in permanent fashion (will not use soft delete). + */ + public static boolean deletePermanent(Object bean) throws OptimisticLockException { + return serverMgr.getDefaultServer().deletePermanent(bean); + } + /** * Delete the bean given its type and id. */ @@ -734,6 +745,13 @@ public final class Ebean { return serverMgr.getDefaultServer().deleteAll(beans); } + /** + * Delete permanent all the beans in the Collection (will not use soft delete). + */ + public static int deleteAllPermanent(Collection beans) throws OptimisticLockException { + return serverMgr.getDefaultServer().deleteAllPermanent(beans); + } + /** * Refresh the values of a bean. *

diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index 1f8b809dd..459ab6b4d 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -1273,6 +1273,26 @@ public interface EbeanServer { */ boolean delete(Object bean, Transaction transaction) throws OptimisticLockException; + /** + * Delete a bean permanently without soft delete. + */ + boolean deletePermanent(Object bean) throws OptimisticLockException; + + /** + * Delete a bean permanently without soft delete using an explicit transaction. + */ + boolean deletePermanent(Object bean, Transaction transaction) throws OptimisticLockException; + + /** + * Delete all the beans in the collection permanently without soft delete. + */ + int deleteAllPermanent(Collection beans) throws OptimisticLockException; + + /** + * Delete all the beans in the collection permanently without soft delete using an explicit transaction. + */ + int deleteAllPermanent(Collection beans, Transaction transaction) throws OptimisticLockException; + /** * Delete the bean given its type and id. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index d9ec8164c..3b0421ec6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -1883,9 +1883,28 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { /** * Delete the bean with the explicit transaction. */ - public boolean delete(Object bean, Transaction t) { - - return persister.delete(checkEntityBean(bean), t); + public boolean delete(Object bean, Transaction t) throws OptimisticLockException { + return persister.delete(checkEntityBean(bean), t, false); + } + + @Override + public boolean deletePermanent(Object bean) throws OptimisticLockException { + return deletePermanent(bean, null); + } + + @Override + public boolean deletePermanent(Object bean, Transaction t) throws OptimisticLockException { + return persister.delete(checkEntityBean(bean), t, true); + } + + @Override + public int deleteAllPermanent(Collection beans) { + return deleteAllInternal(beans.iterator(), null, true); + } + + @Override + public int deleteAllPermanent(Collection beans, Transaction t) { + return deleteAllInternal(beans.iterator(), t, true); } /** @@ -1893,7 +1912,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { */ @Override public int deleteAll(Collection beans) { - return deleteAllInternal(beans.iterator(), null); + return deleteAllInternal(beans.iterator(), null, false); } /** @@ -1901,13 +1920,13 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { */ @Override public int deleteAll(Collection beans, Transaction t) { - return deleteAllInternal(beans.iterator(), t); + return deleteAllInternal(beans.iterator(), t, false); } /** * Delete all the beans in the iterator with an explicit transaction. */ - private int deleteAllInternal(Iterator it, Transaction t) { + private int deleteAllInternal(Iterator it, Transaction t, boolean permanent) { TransWrapper wrap = initTransIfRequired(t); @@ -1917,7 +1936,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { int deleteCount = 0; while (it.hasNext()) { EntityBean bean = checkEntityBean(it.next()); - persister.delete(bean, trans); + persister.delete(bean, trans, permanent); deleteCount++; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java index 0d1eeb9ae..faa8d432b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequest.java @@ -12,7 +12,7 @@ import com.avaje.ebeaninternal.server.persist.PersistExecute; public abstract class PersistRequest extends BeanRequest implements BatchPostExecute { public enum Type { - INSERT, UPDATE, DELETE, SOFT_DELETE, UPDATESQL, CALLABLESQL + INSERT, UPDATE, DELETE, SOFT_DELETE, DELETE_PERMANENT, UPDATESQL, CALLABLESQL } protected boolean persistCascade; diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java index 00b69ba60..ceb1f211d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java @@ -256,6 +256,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP beanDescriptor.cacheHandleUpdate(idValue, this); break; case DELETE: + case SOFT_DELETE: // Bean deleted from cache early via postDelete() break; default: @@ -546,6 +547,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP } switch (type) { case DELETE: + case SOFT_DELETE: postDelete(); break; case UPDATE: @@ -614,6 +616,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP controller.postUpdate(this); break; case DELETE: + case SOFT_DELETE: controller.postDelete(this); break; default: @@ -635,6 +638,9 @@ public final class PersistRequestBean extends PersistRequest implements BeanP case DELETE: transaction.logSummary("Deleted [" + name + "] [" + idValue + "]" + draft); break; + case SOFT_DELETE: + transaction.logSummary("SoftDelete [" + name + "] [" + idValue + "]" + draft); + break; default: break; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java b/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java index df79b7ae2..fac5f767b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/Persister.java @@ -66,7 +66,7 @@ public interface Persister { /** * Delete the bean. */ - boolean delete(EntityBean entityBean, Transaction t); + boolean delete(EntityBean entityBean, Transaction t, boolean permanent); /** * Delete multiple beans given a collection of Id values. diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 30fc2d8ac..991ad293c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -683,6 +683,7 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { case INSERT: return changeLogFilter.includeInsert(request) ? insertBeanChange(request): null; case UPDATE: + case SOFT_DELETE: return changeLogFilter.includeUpdate(request) ? updateBeanChange(request): null; case DELETE: return changeLogFilter.includeDelete(request) ? deleteBeanChange(request) :null; diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/BatchedBeanHolder.java b/src/main/java/com/avaje/ebeaninternal/server/persist/BatchedBeanHolder.java index 7e761ce47..df18d3a93 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/BatchedBeanHolder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/BatchedBeanHolder.java @@ -138,6 +138,7 @@ public class BatchedBeanHolder { return inserts.size(); case UPDATE: + case SOFT_DELETE: if (updates == null) { updates = new ArrayList(); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java index 711147526..46d3e476a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java @@ -336,7 +336,7 @@ public final class DefaultPersister implements Persister { */ private void deleteRecurse(EntityBean detailBean, Transaction t, boolean softDelete) { - Type deleteType = softDelete ? Type.SOFT_DELETE : Type.DELETE; + Type deleteType = softDelete ? Type.SOFT_DELETE : Type.DELETE_PERMANENT; deleteRequest(createRequest(detailBean, t, deleteType)); } @@ -498,15 +498,16 @@ public final class DefaultPersister implements Persister { * Delete the bean with the explicit transaction. * Return false if the delete is executed without OCC and 0 rows were deleted. */ - public boolean delete(EntityBean bean, Transaction t) { + public boolean delete(EntityBean bean, Transaction t, boolean permanent) { - PersistRequestBean request = createRequest(bean, t, Type.DELETE); + Type deleteType = permanent ? Type.DELETE_PERMANENT : Type.DELETE; + PersistRequestBean request = createRequest(bean, t, deleteType); boolean deleted = deleteRequest(request); - if (request.isDraftable()) { + if (request.isDraftable() && request.getType() == Type.DELETE) { // we have just deleting a draft bean so now we need to delete the // associated 'live' bean. This is effectively an 'automatic publish'. - deleteRequest(createPublishRequest(request.createReference(), t, Type.DELETE, true)); + deleteRequest(createPublishRequest(request.createReference(), t, Type.DELETE_PERMANENT, true)); } return deleted; } @@ -541,8 +542,8 @@ public final class DefaultPersister implements Persister { private void deleteList(List beanList, Transaction t, boolean softDelete) { for (int i = 0; i < beanList.size(); i++) { -// deleteRecurse((EntityBean) beanList.get(i), t, softDelete); - delete((EntityBean) beanList.get(i), t); + deleteRecurse((EntityBean) beanList.get(i), t, softDelete); + //delete((EntityBean) beanList.get(i), t); } } @@ -590,7 +591,7 @@ 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); + Query q = deleteRequiresQuery(descriptor, propImportDelete, softDelete); if (idList != null) { q.where().idIn(idList); if (t.isLogSummary()) { @@ -701,7 +702,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) { + private Query deleteRequiresQuery(BeanDescriptor desc, BeanPropertyAssocOne[] propImportDelete, boolean softDelete) { Query q = server.createQuery(desc.getBeanType()); StringBuilder sb = new StringBuilder(30); @@ -710,6 +711,10 @@ public final class DefaultPersister implements Persister { } q.setAutoTune(false); q.select(sb.toString()); + if (!softDelete) { + // hard delete so we want this query to include logically deleted rows (if any) + q.includeSoftDeletes(); + } return q; } @@ -1492,7 +1497,10 @@ public final class DefaultPersister implements Persister { private PersistRequestBean createRequest(T bean, Transaction t, Object parentBean, BeanManager mgr, PersistRequest.Type type, boolean saveRecurse, boolean publish) { - if (type == Type.DELETE && mgr.getBeanDescriptor().isSoftDelete()) { + if (type == Type.DELETE_PERMANENT) { + 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; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/BeanPersistIds.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/BeanPersistIds.java index 6ef1bdf20..f44dfdefc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/BeanPersistIds.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/BeanPersistIds.java @@ -183,6 +183,7 @@ public class BeanPersistIds implements Serializable { addUpdateId(id); break; case DELETE: + case SOFT_DELETE: addDeleteId(id); break; diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 467d416b0..172601c66 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -667,6 +667,26 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return false; } + @Override + public boolean deletePermanent(Object bean) throws OptimisticLockException { + return false; + } + + @Override + public boolean deletePermanent(Object bean, Transaction transaction) throws OptimisticLockException { + return false; + } + + @Override + public int deleteAllPermanent(Collection beans) throws OptimisticLockException { + return 0; + } + + @Override + public int deleteAllPermanent(Collection beans, Transaction transaction) throws OptimisticLockException { + return 0; + } + @Override public int execute(SqlUpdate updSql, Transaction t) { return 0; diff --git a/src/test/java/com/avaje/tests/model/softdelete/ESoftDelDown.java b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelDown.java new file mode 100644 index 000000000..ac36bd71e --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelDown.java @@ -0,0 +1,21 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.Entity; + +@Entity +public class ESoftDelDown extends BaseSoftDelete { + + String down; + + public ESoftDelDown(String up) { + this.down = down; + } + + public String getDown() { + return down; + } + + public void setDown(String down) { + this.down = down; + } +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/ESoftDelMid.java b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelMid.java new file mode 100644 index 000000000..499d33e40 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelMid.java @@ -0,0 +1,64 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import java.util.List; + +@Entity +public class ESoftDelMid extends BaseSoftDelete { + + @ManyToOne(optional = false) + ESoftDelTop top; + + String mid; + + @ManyToOne(cascade = CascadeType.ALL) + ESoftDelUp up; + + @OneToMany(cascade = CascadeType.ALL) + List downs; + + public ESoftDelMid(ESoftDelTop top, String mid) { + this.top = top; + this.mid = mid; + } + + public ESoftDelTop getTop() { + return top; + } + + public void setTop(ESoftDelTop top) { + this.top = top; + } + + public ESoftDelUp getUp() { + return up; + } + + public void setUp(ESoftDelUp up) { + this.up = up; + } + + public String getMid() { + return mid; + } + + public void setMid(String mid) { + this.mid = mid; + } + + public List getDowns() { + return downs; + } + + public void setDowns(List downs) { + this.downs = downs; + } + + public void addDown(String down) { + getDowns().add(new ESoftDelDown(down)); + } +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/ESoftDelTop.java b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelTop.java new file mode 100644 index 000000000..d17b6ec30 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelTop.java @@ -0,0 +1,44 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import java.util.List; + +@Entity +public class ESoftDelTop extends BaseSoftDelete { + + String top; + + @OneToMany(mappedBy = "top", cascade = CascadeType.ALL) + List mids; + + public ESoftDelTop(String top) { + this.top = top; + } + + public String getTop() { + return top; + } + + public void setTop(String top) { + this.top = top; + } + + public List getMids() { + return mids; + } + + public void setMids(List mids) { + this.mids = mids; + } + + public ESoftDelMid addMids(String mid) { + + ESoftDelMid bean = new ESoftDelMid(this, mid); + getMids().add(bean); + return bean; + } + +} diff --git a/src/test/java/com/avaje/tests/model/softdelete/ESoftDelUp.java b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelUp.java new file mode 100644 index 000000000..f53a2215b --- /dev/null +++ b/src/test/java/com/avaje/tests/model/softdelete/ESoftDelUp.java @@ -0,0 +1,21 @@ +package com.avaje.tests.model.softdelete; + +import javax.persistence.Entity; + +@Entity +public class ESoftDelUp extends BaseSoftDelete { + + String up; + + public ESoftDelUp(String up) { + this.up = up; + } + + public String getUp() { + return up; + } + + public void setUp(String up) { + this.up = up; + } +} diff --git a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java index 4954ac54d..fe9133196 100644 --- a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java +++ b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java @@ -20,7 +20,6 @@ public class TestSoftDeleteBasic extends BaseTestCase { EBasicSoftDelete bean = new EBasicSoftDelete(); bean.setName("one"); - Ebean.save(bean); Ebean.delete(bean); @@ -42,7 +41,39 @@ public class TestSoftDeleteBasic extends BaseTestCase { .findUnique(); assertThat(findInclude).isNotNull(); + } + @Test + public void testDeleteById() { + + EBasicSoftDelete bean = new EBasicSoftDelete(); + bean.setName("two"); + Ebean.save(bean); + + Ebean.delete(EBasicSoftDelete.class, bean.getId()); + } + + @Test + public void testDeletePartial() { + + EBasicSoftDelete bean = new EBasicSoftDelete(); + bean.setName("partial"); + Ebean.save(bean); + + // partially loaded bean without deleted state loaded + EBasicSoftDelete partial = Ebean.find(EBasicSoftDelete.class) + .select("id") + .setId(bean.getId()) + .findUnique(); + + LoggedSqlCollector.start(); + Ebean.delete(partial); + + // check lazy loading isn't invoked (deleted set to true without invoking lazy loading) + List loggedSql = LoggedSqlCollector.stop(); + assertThat(loggedSql).hasSize(2); + assertThat(loggedSql.get(0)).contains("update ebasic_sdchild set deleted="); + assertThat(loggedSql.get(1)).contains("update ebasic_soft_delete set deleted=? where id=?"); } @Test diff --git a/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteTop.java b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteTop.java new file mode 100644 index 000000000..ecbaa79a6 --- /dev/null +++ b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteTop.java @@ -0,0 +1,97 @@ +package com.avaje.tests.softdelete; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Transaction; +import com.avaje.ebean.config.PersistBatch; +import com.avaje.tests.model.softdelete.ESoftDelMid; +import com.avaje.tests.model.softdelete.ESoftDelRole; +import com.avaje.tests.model.softdelete.ESoftDelTop; +import com.avaje.tests.model.softdelete.ESoftDelUp; +import com.avaje.tests.model.softdelete.ESoftDelUser; +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestSoftDeleteTop extends BaseTestCase { + + @Test + public void testDeletePermanent() { + + ESoftDelUp up1 = new ESoftDelUp("up1"); + Ebean.save(up1); + + Ebean.delete(up1); + Ebean.deletePermanent(up1); + } + + + @Test + public void testSoftDeleteJdbcBatch() { + + ESoftDelUp up1 = new ESoftDelUp("upBatch1"); + Ebean.save(up1); + ESoftDelUp up2 = new ESoftDelUp("upBatch2"); + Ebean.save(up2); + + Transaction transaction = Ebean.beginTransaction(); + try { + transaction.setBatch(PersistBatch.ALL); + Ebean.delete(up1); + Ebean.delete(up2); + transaction.commit(); + } finally { + transaction.end(); + } + + List list = new ArrayList(); + list.add(up1); + list.add(up2); + + Ebean.deleteAllPermanent(list); + } + + @Test + public void testSoftDeleteAll() { + + ESoftDelUp up1 = new ESoftDelUp("upBatchX"); + ESoftDelUp up2 = new ESoftDelUp("upBatchY"); + + List list = new ArrayList(); + list.add(up1); + list.add(up2); + + // by default uses JDBC for the 'all' methods + Ebean.saveAll(list); + Ebean.deleteAll(list); + Ebean.deleteAllPermanent(list); + + } + + @Test + public void test() { + + ESoftDelUp up1 = new ESoftDelUp("up1"); + + ESoftDelTop top1 = new ESoftDelTop("top1"); + ESoftDelMid mid1 = top1.addMids("mid1"); + mid1.addDown("down1"); + mid1.addDown("down2"); + mid1.setUp(up1); + + ESoftDelMid mid2 = top1.addMids("mid2"); + mid2.addDown("down3"); + mid2.addDown("down4"); + + Ebean.save(top1); + + Ebean.delete(top1); + + Ebean.deletePermanent(top1); + } + +}