diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java index c478e90fe..1255f3a2c 100644 --- a/src/main/java/com/avaje/ebean/Ebean.java +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -704,6 +704,10 @@ public final class Ebean { * you automatically. *

*

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

+ *

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

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