From 0ad52cf6a7fa3cdbd6cdfd420f614f94af209ed4 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Tue, 1 Dec 2015 23:21:11 +1300 Subject: [PATCH] #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(); + + } +}