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/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..3cc7a1f35
--- /dev/null
+++ b/src/main/java/com/avaje/ebean/annotation/SoftDelete.java
@@ -0,0 +1,25 @@
+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').
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface SoftDelete {
+
+}
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/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 baecf7470..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, 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 324fee809..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:
@@ -479,6 +480,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 +493,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() {
@@ -533,6 +547,7 @@ public final class PersistRequestBean extends PersistRequest implements BeanP
}
switch (type) {
case DELETE:
+ case SOFT_DELETE:
postDelete();
break;
case UPDATE:
@@ -601,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:
@@ -622,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;
}
@@ -801,4 +820,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/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 3f0320b30..991ad293c 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()) {
@@ -669,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;
@@ -710,11 +725,11 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType {
cacheHelp.initialise();
}
- public SqlUpdate deleteById(Object id, List
*/
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 +1323,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 +1331,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 +1396,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 +1404,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 +1445,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 +1497,13 @@ 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_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;
+ }
+
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/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/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/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/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/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
new file mode 100644
index 000000000..640d4296a
--- /dev/null
+++ b/src/test/java/com/avaje/tests/model/softdelete/EBasicSoftDelete.java
@@ -0,0 +1,61 @@
+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 {
+
+ String name;
+
+ String description;
+
+ @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
+ List children;
+
+ @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
+ List nosdChildren;
+
+
+ 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;
+ }
+
+ 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/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/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/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/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
new file mode 100644
index 000000000..fe9133196
--- /dev/null
+++ b/src/test/java/com/avaje/tests/softdelete/TestSoftDeleteBasic.java
@@ -0,0 +1,158 @@
+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 {
+
+ @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();
+ }
+
+ @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
+ 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=?;");
+
+ }
+
+}
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);
+ }
+
+}