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 d48b5e755..02a390349 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java @@ -562,6 +562,13 @@ public final class PersistRequestBean extends PersistRequest implements BeanP return false; } + /** + * Return true if this was a hard/permanent delete request (and should cascade as such). + */ + public boolean isHardDeleteCascade() { + return (type == Type.DELETE && beanDescriptor.isSoftDelete()); + } + /** * Checks for @Draftable entity beans with @Draft property that the bean is a 'draft'. * Save or Update is not allowed to execute using 'live' beans - must use publish(). diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/DeleteUnloadedForeignKeys.java b/src/main/java/com/avaje/ebeaninternal/server/persist/DeleteUnloadedForeignKeys.java index 0a5735344..6b5093d25 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/DeleteUnloadedForeignKeys.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/DeleteUnloadedForeignKeys.java @@ -1,8 +1,5 @@ package com.avaje.ebeaninternal.server.persist; -import java.util.ArrayList; -import java.util.List; - import com.avaje.ebean.PersistenceContextScope; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebeaninternal.api.SpiEbeanServer; @@ -13,6 +10,9 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocOne; import com.avaje.ebeaninternal.server.transaction.DefaultPersistenceContext; +import java.util.ArrayList; +import java.util.List; + /** * Used for deletion of a partially populated bean where some cascade delete * properties where not loaded. @@ -20,10 +20,8 @@ import com.avaje.ebeaninternal.server.transaction.DefaultPersistenceContext; * This bean effectively holds the foreign properties that where not loaded, and * helps fetch the foreign keys and delete the appropriate rows. *

- * - * @author rbygrave */ -public class DeleteUnloadedForeignKeys { +class DeleteUnloadedForeignKeys { private final List> propList = new ArrayList>(4); @@ -31,11 +29,14 @@ public class DeleteUnloadedForeignKeys { private final PersistRequestBean request; + private final boolean deletePermanent; + private EntityBean beanWithForeignKeys; - public DeleteUnloadedForeignKeys(SpiEbeanServer server, PersistRequestBean request) { + DeleteUnloadedForeignKeys(SpiEbeanServer server, PersistRequestBean request) { this.server = server; this.request = request; + this.deletePermanent = request.isHardDeleteCascade(); } public boolean isEmpty() { @@ -50,7 +51,7 @@ public class DeleteUnloadedForeignKeys { * Execute a query fetching the missing (unloaded) foreign keys. We need to * fetch these key values before the parent bean is deleted. */ - public void queryForeignKeys() { + void queryForeignKeys() { BeanDescriptor descriptor = request.getBeanDescriptor(); SpiQuery q = (SpiQuery) server.createQuery(descriptor.getBeanType()); @@ -67,6 +68,7 @@ public class DeleteUnloadedForeignKeys { q.setPersistenceContextScope(PersistenceContextScope.QUERY); q.setAutoTune(false); q.select(sb.toString()); + q.includeSoftDeletes(); q.where().idEq(id); SpiTransaction t = request.getTransaction(); @@ -80,7 +82,7 @@ public class DeleteUnloadedForeignKeys { * Delete the rows relating to the foreign keys. These deletions occur after * the parent bean has been deleted. */ - public void deleteCascade() { + void deleteCascade() { for (int i = 0; i < propList.size(); i++) { BeanPropertyAssocOne prop = propList.get(i); @@ -88,7 +90,11 @@ public class DeleteUnloadedForeignKeys { // if bean exists with a unique id then delete it if (detailBean != null && prop.hasId((EntityBean) detailBean)) { - server.delete(detailBean, request.getTransaction()); + if (deletePermanent) { + server.deletePermanent(detailBean, request.getTransaction()); + } else { + server.delete(detailBean, request.getTransaction()); + } } } } diff --git a/src/test/java/com/avaje/tests/model/onetoone/album/Album.java b/src/test/java/com/avaje/tests/model/onetoone/album/Album.java new file mode 100644 index 000000000..4b4f195a1 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/onetoone/album/Album.java @@ -0,0 +1,35 @@ +package com.avaje.tests.model.onetoone.album; + +import com.avaje.ebean.Model; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.OneToOne; + +@Entity +public class Album extends BaseModel { + + public static final Model.Finder find = new Model.Finder(Album.class); + + private String name; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + private Cover cover; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Cover getCover() { + return this.cover; + } + + public void setCover(Cover cover) { + this.cover = cover; + } +} diff --git a/src/test/java/com/avaje/tests/model/onetoone/album/BaseModel.java b/src/test/java/com/avaje/tests/model/onetoone/album/BaseModel.java new file mode 100644 index 000000000..b3b6fc543 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/onetoone/album/BaseModel.java @@ -0,0 +1,57 @@ +package com.avaje.tests.model.onetoone.album; + +import com.avaje.ebean.Model; +import com.avaje.ebean.annotation.SoftDelete; +import com.avaje.ebean.annotation.WhenCreated; +import com.avaje.ebean.annotation.WhenModified; +import org.joda.time.DateTime; + +import javax.persistence.Column; +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class BaseModel extends Model { + + @Id + protected Long id; + + @SoftDelete + @Column(name = "deleted", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE") + protected boolean deleted; + + @WhenCreated + protected DateTime createdAt; + + @WhenModified + protected DateTime lastUpdate; + + protected BaseModel() { + } + + public Long getId() { + return this.id; + } + + public DateTime getCreatedAt() { + return this.createdAt; + } + + public DateTime getLastUpdate() { + return this.lastUpdate; + } + + /** + * Check if this entry is soft deleted. + */ + public boolean isDeleted() { + return this.deleted; + } + + /** + * Delete this entry permanently. + */ + public boolean deletePermanent() { + return db().deletePermanent(this); + } +} diff --git a/src/test/java/com/avaje/tests/model/onetoone/album/Cover.java b/src/test/java/com/avaje/tests/model/onetoone/album/Cover.java new file mode 100644 index 000000000..3240dde0e --- /dev/null +++ b/src/test/java/com/avaje/tests/model/onetoone/album/Cover.java @@ -0,0 +1,50 @@ +package com.avaje.tests.model.onetoone.album; + +import com.avaje.ebean.Model; +import com.avaje.ebean.annotation.SoftDelete; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.PreRemove; + +@Entity +public class Cover extends Model { + + private static final Logger logger = LoggerFactory.getLogger(Cover.class); + + public static final Finder find = new Finder(Cover.class); + + @Id + protected Long id; + + @SoftDelete + @Column(name = "deleted", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE") + protected boolean deleted; + + protected String s3Url; + + public String getS3Url() { + return this.s3Url; + } + + public void setS3Url(String s3Url) { + this.s3Url = s3Url; + } + + @PreRemove + public void deleteRemoteFile() { + logger.debug("Cover::deleteRemoteFile() --> Remove file from Amazon S3"); + } + + public boolean isDeleted() { + return this.deleted; + } + + public Long getId() { + return this.id; + } + +} diff --git a/src/test/java/com/avaje/tests/model/onetoone/album/TestOneToOneHardDelete.java b/src/test/java/com/avaje/tests/model/onetoone/album/TestOneToOneHardDelete.java new file mode 100644 index 000000000..97228d2fd --- /dev/null +++ b/src/test/java/com/avaje/tests/model/onetoone/album/TestOneToOneHardDelete.java @@ -0,0 +1,59 @@ +package com.avaje.tests.model.onetoone.album; + + +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestOneToOneHardDelete { + + @Test + public void test() { + + Cover cover = new Cover(); + cover.setS3Url("http://foo"); + cover.save(); + + Album album = new Album(); + album.setName("BlackWhite"); + album.setCover(cover); + album.save(); + + Album found1 = Album.find.where().findUnique(); + + LoggedSqlCollector.start(); + + // --------------------- + // SOFT DELETE + // --------------------- + found1.delete(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(3); + // assert we loaded the missing/unloaded foreign key + assertThat(sql.get(0)).contains("select t0.id c0, t0.cover_id c1 from album t0 where t0.id = ?"); + // assert soft delete cascaded + assertThat(sql.get(1)).contains("update album set deleted=?, last_update=? where id=?"); + assertThat(sql.get(2)).contains("update cover set deleted=? where id=?"); + + Album found2 = Album.find.where().includeSoftDeletes().findUnique(); + + LoggedSqlCollector.start(); + + // --------------------- + // HARD DELETE + // --------------------- + found2.deletePermanent(); + + sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(3); + // assert we loaded the missing/unloaded foreign key + assertThat(sql.get(0)).contains("select t0.id c0, t0.cover_id c1 from album t0 where t0.id = ?"); + // assert hard delete cascaded + assertThat(sql.get(1)).contains("delete from album where"); + assertThat(sql.get(2)).contains("delete from cover where"); + } +}