#118 - ENH: Add support for for Soft Deletes (Logical Deletion) ...

This commit is contained in:
Robin Bygrave
2015-12-02 21:38:34 +13:00
parent 0ad52cf6a7
commit f39a684c8e
16 changed files with 469 additions and 118 deletions
@@ -9,19 +9,17 @@ import java.lang.annotation.Target;
* Used to indicate a property on an entity bean used to control 'soft delete'
* (also known as 'logical delete').
* <p>
* The property should be of type boolean, int or short.
* The property should be of type boolean.
* </p>
* <pre>{@code
*
* @SoftDelete
* boolean deleted;
*
* }</pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SoftDelete {
/**
* Specify the bind value that matches 'deleted' state.
* <p>
* If not specified then for boolean this is <code>true</code> and
* for int and short this value is <code>1</code>.
* </p>
*/
String value() default "";
}
@@ -22,6 +22,7 @@ import com.avaje.ebeaninternal.server.text.json.ReadJson;
import com.avaje.ebeaninternal.server.text.json.WriteJson;
import com.avaje.ebeaninternal.server.type.DataBind;
import com.avaje.ebeaninternal.server.type.ScalarType;
import com.avaje.ebeaninternal.server.type.ScalarTypeBoolean;
import com.avaje.ebeaninternal.util.ValueUtil;
import com.fasterxml.jackson.core.JsonToken;
import org.slf4j.Logger;
@@ -231,7 +232,9 @@ public class BeanProperty implements ElPropertyValue {
final boolean softDelete;
final Object softDeleteValue;
final String softDeleteDbSet;
final String softDeleteDbPredicate;
final boolean indexed;
@@ -262,9 +265,6 @@ public class BeanProperty implements ElPropertyValue {
this.draftDirty = deploy.isDraftDirty();
this.draftOnly = deploy.isDraftOnly();
this.draftReset = deploy.isDraftReset();
this.softDelete = deploy.isSoftDelete();
this.softDeleteValue = deploy.getSoftDeleteValue();
this.secondaryTable = deploy.isSecondaryTable();
if (secondaryTable) {
this.secondaryTableJoin = new TableJoin(deploy.getSecondaryTableJoin());
@@ -308,6 +308,16 @@ public class BeanProperty implements ElPropertyValue {
this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), false, null);
this.elPlaceHolderEncrypted = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), dbEncrypted, dbColumn);
this.softDelete = deploy.isSoftDelete();
if (softDelete) {
ScalarTypeBoolean.BooleanBase boolType = (ScalarTypeBoolean.BooleanBase)scalarType;
this.softDeleteDbSet = dbColumn+"="+boolType.getDbTrueLiteral();
this.softDeleteDbPredicate = dbColumn+"="+boolType.getDbFalseLiteral();
} else {
this.softDeleteDbSet = null;
this.softDeleteDbPredicate = null;
}
this.jsonSerialize = deploy.isJsonSerialize();
this.jsonDeserialize = deploy.isJsonDeserialize();
}
@@ -352,7 +362,8 @@ public class BeanProperty implements ElPropertyValue {
this.draftOnly = source.draftOnly;
this.draftReset = source.draftReset;
this.softDelete = source.softDelete;
this.softDeleteValue = source.softDeleteValue;
this.softDeleteDbSet = source.softDeleteDbSet;
this.softDeleteDbPredicate = source.softDeleteDbPredicate;
this.fetchEager = source.fetchEager;
this.unidirectionalShadow = source.unidirectionalShadow;
this.discriminator = source.discriminator;
@@ -624,22 +635,26 @@ public class BeanProperty implements ElPropertyValue {
}
}
/**
* Return the DB literal expression to set the deleted state to true.
*/
public String getSoftDeleteDbSet() {
return dbColumn +"=true";
return softDeleteDbSet;
}
/**
* Return the DB literal predicate used to filter out soft deleted rows from a query.
*/
public String getSoftDeleteDbPredicate(String tableAlias) {
return tableAlias+"."+dbColumn+"=false";
return tableAlias+"."+softDeleteDbPredicate;
}
/**
* Set the soft delete property value on the bean without invoking lazy loading.
*/
public void setSoftDeleteValue(EntityBean bean) {
setValue(bean, softDeleteValue);
// assumes boolean deleted true being set which is ok limitation for now
setValue(bean, true);
bean._ebean_getIntercept().setChangedProperty(propertyIndex);
}
@@ -1,6 +1,7 @@
package com.avaje.ebeaninternal.server.deploy.meta;
import com.avaje.ebean.annotation.CreatedTimestamp;
import com.avaje.ebean.annotation.SoftDelete;
import com.avaje.ebean.annotation.UpdatedTimestamp;
import com.avaje.ebean.annotation.WhenCreated;
import com.avaje.ebean.annotation.WhenModified;
@@ -190,7 +191,6 @@ public class DeployBeanProperty {
private boolean draftReset;
private boolean softDelete;
private String softDeleteValue = "";
public DeployBeanProperty(DeployBeanDescriptor<?> desc, Class<?> propertyType, ScalarType<?> scalarType, ScalarTypeConverter<?, ?> typeConverter) {
this.desc = desc;
@@ -224,6 +224,8 @@ public class DeployBeanProperty {
return AUDITCOLUMN_ORDER;
} else if (field.getAnnotation(Version.class) != null) {
return VERSIONCOLUMN_ORDER;
} else if (field.getAnnotation(SoftDelete.class) != null) {
return VERSIONCOLUMN_ORDER;
}
return 0;
}
@@ -876,42 +878,12 @@ public class DeployBeanProperty {
return draftReset;
}
public void setSoftDelete(String softDeleteValue) {
public void setSoftDelete() {
this.softDelete = true;
this.softDeleteValue = softDeleteValue;
}
public boolean isSoftDelete() {
return softDelete;
}
public Object getSoftDeleteValue() {
return !softDelete ? null : "".equals(softDeleteValue) ? defaultSoftDeleteValue() : parseSoftDeleteValue();
}
private Object parseSoftDeleteValue() {
if (Boolean.class.equals(propertyType) || boolean.class.equals(propertyType)) {
return Boolean.parseBoolean(softDeleteValue);
}
if (Integer.class.equals(propertyType) || int.class.equals(propertyType)) {
return Integer.parseInt(softDeleteValue);
}
if (Short.class.equals(propertyType) || short.class.equals(propertyType)) {
return Short.parseShort(softDeleteValue);
}
throw new IllegalStateException("@SoftDelete on ["+getFullBeanName()+"] mapped to unsupported type propertyType["+propertyType+"]");
}
private Object defaultSoftDeleteValue() {
if (Boolean.class.equals(propertyType) || boolean.class.equals(propertyType)) {
return Boolean.TRUE;
}
if (Integer.class.equals(propertyType) || int.class.equals(propertyType)) {
return Integer.valueOf(1);
}
if (Short.class.equals(propertyType) || short.class.equals(propertyType)) {
return Short.valueOf("1");
}
throw new IllegalStateException("@SoftDelete on ["+getFullBeanName()+"] mapped to unsupported type propertyType["+propertyType+"]");
}
}
@@ -160,7 +160,7 @@ public class AnnotationFields extends AnnotationParser {
}
SoftDelete softDelete = get(prop, SoftDelete.class);
if (softDelete != null) {
prop.setSoftDelete(softDelete.value());
prop.setSoftDelete();
}
DbJson dbJson = get(prop, DbJson.class);
@@ -45,27 +45,12 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex
protected void configureQuery(SpiQuery<?> query, String lazyLoadProperty) {
// propagate the readOnly state
if (parent.isReadOnly() != null) {
query.setReadOnly(parent.isReadOnly());
}
// propagate the asOf and lazy loading mode
query.setDisableLazyLoading(parent.isDisableLazyLoading());
query.asOf(parent.getAsOf());
parent.propagateQueryState(query);
query.setParentNode(objectGraphNode);
query.setLazyLoadProperty(lazyLoadProperty);
if (parent.isAsDraft()) {
query.asDraft();
}
if (parent.isDisableReadAudit()) {
query.setDisableReadAuditing();
}
if (queryProps != null) {
queryProps.configureBeanQuery(query);
}
if (parent.isUseAutoTune()) {
query.setAutoTune(true);
}
}
protected void register(EntityBeanIntercept ebi) {
@@ -43,6 +43,7 @@ public class DLoadContext implements LoadContext {
private final int defaultBatchSize;
private final boolean disableLazyLoading;
private final boolean disableReadAudit;
private final boolean includeSoftDeletes;
/**
* The path relative to the root of the object graph.
@@ -67,6 +68,7 @@ public class DLoadContext implements LoadContext {
SpiQuery<?> query = request.getQuery();
this.asOf = query.getAsOf();
this.asDraft = query.isAsDraft();
this.includeSoftDeletes = query.isIncludeSoftDeletes();
this.readOnly = query.isReadOnly();
this.disableReadAudit = query.isDisableReadAudit();
this.disableLazyLoading = query.isDisableLazyLoading();
@@ -201,10 +203,6 @@ public class DLoadContext implements LoadContext {
return new ObjectGraphNode(origin, path);
}
public boolean isUseAutoTune() {
return useProfiling;
}
protected String getFullPath(String path) {
if (relativePath == null) {
return path;
@@ -225,34 +223,6 @@ public class DLoadContext implements LoadContext {
return readOnly;
}
/**
* Return the 'as of' timestamp that should propagate to secondary queries.
*/
protected Timestamp getAsOf() {
return asOf;
}
/**
* Return true if the root query is a 'asDraft' query that should propagate to secondary queries.
*/
protected boolean isAsDraft() {
return asDraft;
}
/**
* Return true if disable read auditing should propagate to secondary queries.
*/
protected boolean isDisableReadAudit() {
return disableReadAudit;
}
/**
* Return true if disable lazy loading should propagate to secondary queries.
*/
protected boolean isDisableLazyLoading() {
return disableLazyLoading;
}
public PersistenceContext getPersistenceContext() {
return persistenceContext;
}
@@ -335,4 +305,26 @@ public class DLoadContext implements LoadContext {
return desc.getBeanPropertyFromPath(path);
}
/**
* Propagate the original query settings (draft, asOf etc) to the secondary queries.
*/
public void propagateQueryState(SpiQuery<?> query) {
if (readOnly != null) {
query.setReadOnly(readOnly);
}
query.setDisableLazyLoading(disableLazyLoading);
query.asOf(asOf);
if (asDraft) {
query.asDraft();
}
if (includeSoftDeletes) {
query.includeSoftDeletes();
}
if (disableReadAudit) {
query.setDisableReadAuditing();
}
if (useProfiling) {
query.setAutoTune(true);
}
}
}
@@ -56,27 +56,11 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex
public void configureQuery(SpiQuery<?> query) {
// propagate the readOnly state
if (parent.isReadOnly() != null) {
query.setReadOnly(parent.isReadOnly());
}
// propagate the asOf and lazy loading mode
query.setDisableLazyLoading(parent.isDisableLazyLoading());
query.asOf(parent.getAsOf());
parent.propagateQueryState(query);
query.setParentNode(objectGraphNode);
if (parent.isAsDraft()) {
query.asDraft();
}
if (parent.isDisableReadAudit()) {
query.setDisableReadAuditing();
}
if (queryProps != null) {
queryProps.configureBeanQuery(query);
}
if (parent.isUseAutoTune()) {
query.setAutoTune(true);
}
}
public BeanPropertyAssocMany<?> getBeanProperty() {
@@ -1245,9 +1245,10 @@ public final class DefaultPersister implements Persister {
BeanPropertyAssocMany<?>[] manys = desc.propertiesManyDelete();
for (int i = 0; i < manys.length; i++) {
if (manys[i].isManyToMany()) {
// delete associated rows from intersection table
deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish());
if (!softDelete) {
// delete associated rows from intersection table (but not during soft delete)
deleteAssocManyIntersection(parentBean, manys[i], t, request.isPublish());
}
} else {
if (ModifyListenMode.REMOVALS.equals(manys[i].getModifyListenMode())) {
@@ -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();
}
@@ -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;
}
}
@@ -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;
}
}
@@ -1,6 +1,9 @@
package com.avaje.tests.model.softdelete;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import java.util.List;
@Entity
public class EBasicSoftDelete extends BaseSoftDelete {
@@ -9,6 +12,13 @@ public class EBasicSoftDelete extends BaseSoftDelete {
String description;
@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
List<EBasicSDChild> children;
@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
List<EBasicNoSDChild> nosdChildren;
public String getName() {
return name;
}
@@ -24,4 +34,28 @@ public class EBasicSoftDelete extends BaseSoftDelete {
public void setDescription(String description) {
this.description = description;
}
public List<EBasicSDChild> getChildren() {
return children;
}
public void setChildren(List<EBasicSDChild> children) {
this.children = children;
}
public void addChild(String childName, long amount) {
getChildren().add(new EBasicSDChild(this, childName, amount));
}
public List<EBasicNoSDChild> getNosdChildren() {
return nosdChildren;
}
public void setNosdChildren(List<EBasicNoSDChild> nosdChildren) {
this.nosdChildren = nosdChildren;
}
public void addNoSoftDeleteChild(String childName, long amount) {
getNosdChildren().add(new EBasicNoSDChild(this, childName, amount));
}
}
@@ -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<ESoftDelUser> users;
public ESoftDelRole(String roleName) {
this.roleName = roleName;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public List<ESoftDelUser> getUsers() {
return users;
}
public void setUsers(List<ESoftDelUser> users) {
this.users = users;
}
}
@@ -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<ESoftDelRole> 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<ESoftDelRole> getRoles() {
return roles;
}
public void setRoles(List<ESoftDelRole> roles) {
this.roles = roles;
}
}
@@ -2,11 +2,15 @@ package com.avaje.tests.softdelete;
import com.avaje.ebean.BaseTestCase;
import com.avaje.ebean.Ebean;
import com.avaje.ebean.Query;
import com.avaje.ebean.SqlQuery;
import com.avaje.ebean.SqlRow;
import com.avaje.tests.model.softdelete.EBasicSoftDelete;
import org.avaje.ebeantest.LoggedSqlCollector;
import org.junit.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestSoftDeleteBasic extends BaseTestCase {
@@ -40,4 +44,84 @@ public class TestSoftDeleteBasic extends BaseTestCase {
assertThat(findInclude).isNotNull();
}
@Test
public void testCascadeSaveDelete() {
EBasicSoftDelete bean = new EBasicSoftDelete();
bean.setName("cascadeOne");
bean.addChild("child1", 10);
bean.addChild("child2", 20);
bean.addChild("child3", 30);
bean.addNoSoftDeleteChild("nsd1", 101);
bean.addNoSoftDeleteChild("nsd2", 102);
Ebean.save(bean);
LoggedSqlCollector.start();
Ebean.delete(bean);
List<String> 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<EBasicSoftDelete> query1 =
Ebean.find(EBasicSoftDelete.class)
.fetch("children")
.where().eq("id", bean.getId())
.query();
List<EBasicSoftDelete> 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);
}
}
@@ -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<String> 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=?;");
}
}