diff --git a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index 3cfca39d7..6160691c2 100644 --- a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -11,8 +11,8 @@ import io.ebean.event.BeanPersistRequest; import io.ebean.event.changelog.BeanChange; import io.ebeaninternal.api.ConcurrencyMode; import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.api.SpiProfileTransactionEvent; +import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.api.TransactionEvent; import io.ebeaninternal.server.cache.CacheChangeSet; import io.ebeaninternal.server.deploy.BeanDescriptor; @@ -713,6 +713,20 @@ public final class PersistRequestBean extends PersistRequest implements BeanP return intercept.isLoadedProperty(prop.getPropertyIndex()); } + /** + * Return true if the property is dirty. + */ + public boolean isDirtyProperty(BeanProperty prop) { + return intercept.isDirtyProperty(prop.getPropertyIndex()); + } + + /** + * Return the original / old value for the given property. + */ + public Object getOrigValue(BeanProperty prop) { + return intercept.getOrigValue(prop.getPropertyIndex()); + } + @Override public int executeNow() { if (getterCallback) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index 04e953aca..df0a7b8ae 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -37,6 +37,8 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { private final boolean oneToOneExported; + private final boolean orphanRemoval; + private final boolean importedPrimaryKey; private final boolean primaryKeyExport; @@ -76,6 +78,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { importedPrimaryKey = deploy.isImportedPrimaryKey(); oneToOne = deploy.isOneToOne(); oneToOneExported = deploy.isOneToOneExported(); + orphanRemoval = deploy.isOrphanRemoval(); if (embedded) { // Overriding of the columns and use table alias of owning BeanDescriptor @@ -338,6 +341,10 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { return oneToOneExported; } + public boolean isOrphanRemoval() { + return orphanRemoval; + } + /** * If true this bean maps to the primary key. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java index 93c57efb7..021f5e2f8 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java @@ -25,6 +25,8 @@ public class DeployBeanPropertyAssocOne extends DeployBeanPropertyAssoc { private PropertyForeignKey foreignKey; + private boolean orphanRemoval; + /** * Create the property. */ @@ -160,4 +162,12 @@ public class DeployBeanPropertyAssocOne extends DeployBeanPropertyAssoc { public PropertyForeignKey getForeignKey() { return foreignKey; } + + public void setOrphanRemoval(boolean orphanRemoval) { + this.orphanRemoval = orphanRemoval; + } + + public boolean isOrphanRemoval() { + return orphanRemoval; + } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java index 15a3ed4de..872d97c0d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -216,6 +216,7 @@ public class AnnotationAssocOnes extends AnnotationParser { prop.setMappedBy(propAnn.mappedBy()); if (!"".equals(propAnn.mappedBy())) { prop.setOneToOneExported(); + prop.setOrphanRemoval(propAnn.orphanRemoval()); } setCascadeTypes(propAnn.cascade(), prop.getCascadeInfo()); diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 41cce82bf..48538582a 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -1173,6 +1173,15 @@ public final class DefaultPersister implements Persister { } } } + + for (BeanPropertyAssocOne prop : desc.propertiesOneExportedSave()) { + if (prop.isOrphanRemoval() && request.isDirtyProperty(prop)) { + Object origValue = request.getOrigValue(prop); + if (origValue instanceof EntityBean) { + delete((EntityBean) origValue, request.getTransaction(), true); + } + } + } } /** diff --git a/src/test/java/org/tests/model/onetoone/OtoCust.java b/src/test/java/org/tests/model/onetoone/OtoCust.java new file mode 100644 index 000000000..50f5efad6 --- /dev/null +++ b/src/test/java/org/tests/model/onetoone/OtoCust.java @@ -0,0 +1,61 @@ +package org.tests.model.onetoone; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.Version; + +@Entity +public class OtoCust { + + @Id + long cid; + + String name; + + /** + * Orphan removal so must delete 'old' address when it has been replace or set to null. + */ + @OneToOne(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) + OtoCustAddress address; + + @Version + long version; + + public OtoCust(String name) { + this.name = name; + } + + public long getCid() { + return cid; + } + + public void setCid(long cid) { + this.cid = cid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OtoCustAddress getAddress() { + return address; + } + + public void setAddress(OtoCustAddress address) { + this.address = address; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/model/onetoone/OtoCustAddress.java b/src/test/java/org/tests/model/onetoone/OtoCustAddress.java new file mode 100644 index 000000000..25bba8431 --- /dev/null +++ b/src/test/java/org/tests/model/onetoone/OtoCustAddress.java @@ -0,0 +1,76 @@ +package org.tests.model.onetoone; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.Version; + +@Entity +public class OtoCustAddress { + + @Id + long aid; + + String line1; + String line2; + String line3; + + @OneToOne + OtoCust customer; + + @Version + long version; + + public OtoCustAddress(String line1, String line2) { + this.line1 = line1; + this.line2 = line2; + } + + public long getAid() { + return aid; + } + + public void setAid(long aid) { + this.aid = aid; + } + + public OtoCust getCustomer() { + return customer; + } + + public void setCustomer(OtoCust customer) { + this.customer = customer; + } + + public String getLine1() { + return line1; + } + + public void setLine1(String line1) { + this.line1 = line1; + } + + public String getLine2() { + return line2; + } + + public void setLine2(String line2) { + this.line2 = line2; + } + + public String getLine3() { + return line3; + } + + public void setLine3(String line3) { + this.line3 = line3; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/model/onetoone/TestOneToOneOrphanRemove.java b/src/test/java/org/tests/model/onetoone/TestOneToOneOrphanRemove.java new file mode 100644 index 000000000..8ab94707e --- /dev/null +++ b/src/test/java/org/tests/model/onetoone/TestOneToOneOrphanRemove.java @@ -0,0 +1,50 @@ +package org.tests.model.onetoone; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestOneToOneOrphanRemove extends BaseTestCase { + + @Test + public void base() { + + OtoCust jack = new OtoCust("Jack"); + + OtoCustAddress address = new OtoCustAddress("line1", "line2"); + + jack.setAddress(address); + Ebean.save(jack); + + // set new address + OtoCustAddress address2 = new OtoCustAddress("other1", "other2"); + jack.setAddress(address2); + + // Fail do to uniqueness constraint + LoggedSqlCollector.start(); + Ebean.save(jack); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("delete from oto_cust_address where aid=? and version=?"); + assertThat(sql.get(1)).contains("update oto_cust set version=? where cid=? and version=?"); + assertThat(sql.get(2)).contains("insert into oto_cust_address (line1, line2, line3, version, customer_cid)"); + + jack.setAddress(null); + Ebean.save(jack); + + sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from oto_cust_address where aid=? and version=?"); + assertThat(sql.get(1)).contains("update oto_cust set version=? where cid=? and version=?"); + + OtoCustAddress foundAddress = Ebean.find(OtoCustAddress.class, address2.getAid()); + assertThat(foundAddress).isNull(); + } + +}