From fcadc63338fecaf4af398aec73e93c00de25e2d2 Mon Sep 17 00:00:00 2001 From: rob bygrave Date: Fri, 20 Sep 2019 17:12:32 +1200 Subject: [PATCH] #1656 #1824 Stateless updates - Remove update deleteMissingChildren option, instead always use orphanRemoval --- src/main/java/io/ebean/DB.java | 12 ---- src/main/java/io/ebean/Database.java | 13 ---- .../java/io/ebean/config/ServerConfig.java | 24 ------- .../server/core/DefaultServer.java | 14 ---- .../server/core/PersistRequestBean.java | 20 ++---- .../ebeaninternal/server/core/Persister.java | 5 -- .../server/deploy/BeanPropertyAssocMany.java | 6 ++ .../server/deploy/BeanPropertyAssocOne.java | 4 -- .../deploy/meta/DeployBeanPropertyAssoc.java | 10 +++ .../meta/DeployBeanPropertyAssocOne.java | 10 --- .../deploy/parse/AnnotationAssocManys.java | 2 + .../deploy/parse/AnnotationAssocOnes.java | 4 +- .../server/persist/DefaultPersister.java | 16 +---- .../ebeaninternal/server/persist/Flags.java | 8 ++- .../server/persist/SaveManyBeans.java | 31 ++++----- .../ebeaninternal/api/TDSpiEbeanServer.java | 4 -- src/test/java/org/tests/cascade/COOne.java | 37 +++++++++++ .../java/org/tests/cascade/COOneMany.java | 34 ++++++++++ src/test/java/org/tests/cascade/CORoot.java | 36 ++++++++++ .../TestCascadeOrphanStatelessUpdate.java | 66 +++++++++++++++++++ .../org/tests/model/softdelete/EsdMaster.java | 5 +- .../TestSoftDeleteStatelessUpdate.java | 11 ++-- .../org/tests/update/TestStatelessUpdate.java | 6 +- 23 files changed, 227 insertions(+), 151 deletions(-) create mode 100644 src/test/java/org/tests/cascade/COOne.java create mode 100644 src/test/java/org/tests/cascade/COOneMany.java create mode 100644 src/test/java/org/tests/cascade/CORoot.java create mode 100644 src/test/java/org/tests/cascade/TestCascadeOrphanStatelessUpdate.java diff --git a/src/main/java/io/ebean/DB.java b/src/main/java/io/ebean/DB.java index 0379bae6d..0e336ec41 100644 --- a/src/main/java/io/ebean/DB.java +++ b/src/main/java/io/ebean/DB.java @@ -396,15 +396,6 @@ public class DB { * called then no optimistic locking is performed (internally ConcurrencyMode.NONE is used). *

*

- * {@link DatabaseConfig#setUpdatesDeleteMissingChildren(boolean)}: When cascade saving to a - * OneToMany or ManyToMany the updatesDeleteMissingChildren setting controls if any other children - * that are in the database but are not in the collection are deleted. - *

- *

- * {@link DatabaseConfig#setUpdateChangesOnly(boolean)}: The updateChangesOnly setting - * controls if only the changed properties are included in the update or if all the loaded - * properties are included instead. - *

*
{@code
    *
    *   // A 'stateless update' example
@@ -414,9 +405,6 @@ public class DB {
    *   database.update(customer);
    *
    * }
- * - * @see DatabaseConfig#setUpdatesDeleteMissingChildren(boolean) - * @see DatabaseConfig#setUpdateChangesOnly(boolean) */ public static void update(Object bean) throws OptimisticLockException { getDefault().update(bean); diff --git a/src/main/java/io/ebean/Database.java b/src/main/java/io/ebean/Database.java index ee9ece29d..25d6e6480 100644 --- a/src/main/java/io/ebean/Database.java +++ b/src/main/java/io/ebean/Database.java @@ -1216,9 +1216,6 @@ public interface Database { * database.update(customer); * * } - * - * @see ServerConfig#setUpdatesDeleteMissingChildren(boolean) - * @see ServerConfig#setUpdateChangesOnly(boolean) */ void update(Object bean) throws OptimisticLockException; @@ -1227,16 +1224,6 @@ public interface Database { */ void update(Object bean, Transaction transaction) throws OptimisticLockException; - /** - * Update a bean additionally specifying a transaction and the deleteMissingChildren setting. - * - * @param bean the bean to update - * @param transaction the transaction to use (can be null). - * @param deleteMissingChildren specify false if you do not want 'missing children' of a OneToMany - * or ManyToMany to be automatically deleted. - */ - void update(Object bean, Transaction transaction, boolean deleteMissingChildren) throws OptimisticLockException; - /** * Update a collection of beans. If there is no current transaction one is created and used to * update all the beans in the collection. diff --git a/src/main/java/io/ebean/config/ServerConfig.java b/src/main/java/io/ebean/config/ServerConfig.java index e8534e30e..43fda49a1 100644 --- a/src/main/java/io/ebean/config/ServerConfig.java +++ b/src/main/java/io/ebean/config/ServerConfig.java @@ -355,11 +355,6 @@ public class ServerConfig { */ private boolean updateAllPropertiesInBatch; - /** - * Default behaviour for updates when cascade save on a O2M or M2M to delete any missing children. - */ - private boolean updatesDeleteMissingChildren = true; - /** * Database platform configuration. */ @@ -2423,22 +2418,6 @@ public class ServerConfig { this.updateAllPropertiesInBatch = updateAllPropertiesInBatch; } - /** - * Return true if updates by default delete missing children when cascading save to a OneToMany or - * ManyToMany. When not set this defaults to true. - */ - public boolean isUpdatesDeleteMissingChildren() { - return updatesDeleteMissingChildren; - } - - /** - * Set if updates by default delete missing children when cascading save to a OneToMany or - * ManyToMany. When not set this defaults to true. - */ - public void setUpdatesDeleteMissingChildren(boolean updatesDeleteMissingChildren) { - this.updatesDeleteMissingChildren = updatesDeleteMissingChildren; - } - /** * Return true if query statistics should be collected by ObjectGraphNode. */ @@ -2948,9 +2927,6 @@ public class ServerConfig { skipCacheAfterWrite = p.getBoolean("skipCacheAfterWrite", skipCacheAfterWrite); updateAllPropertiesInBatch = p.getBoolean("updateAllPropertiesInBatch", updateAllPropertiesInBatch); - boolean defaultDeleteMissingChildren = p.getBoolean("defaultDeleteMissingChildren", updatesDeleteMissingChildren); - updatesDeleteMissingChildren = p.getBoolean("updatesDeleteMissingChildren", defaultDeleteMissingChildren); - if (p.get("batch.mode") != null || p.get("persistBatching") != null) { throw new IllegalArgumentException("Property 'batch.mode' or 'persistBatching' is being set but no longer used. Please change to use 'persistBatchMode'"); } diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 87adc2066..04ce0a268 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -1736,30 +1736,16 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { ((EntityBean) bean)._ebean_getIntercept().setDirty(true); } - /** - * Update the bean using the default 'updatesDeleteMissingChildren' setting. - */ @Override public void update(Object bean) { update(bean, null); } - /** - * Update the bean using the default 'updatesDeleteMissingChildren' setting. - */ @Override public void update(Object bean, Transaction t) { persister.update(checkEntityBean(bean), t); } - /** - * Update the bean specifying the deleteMissingChildren option. - */ - @Override - public void update(Object bean, Transaction t, boolean deleteMissingChildren) { - persister.update(checkEntityBean(bean), t, deleteMissingChildren); - } - @Override public void updateAll(Collection beans) throws OptimisticLockException { updateAll(beans, null); diff --git a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index b892f21c3..67cdc9324 100644 --- a/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -104,8 +104,6 @@ public final class PersistRequestBean extends PersistRequest implements BeanP private boolean notifyCache; - private boolean deleteMissingChildren; - /** * Flag used to detect when only many properties where updated via a cascade. Used to ensure * appropriate caches are updated in that case. @@ -629,20 +627,6 @@ public final class PersistRequestBean extends PersistRequest implements BeanP return beanDescriptor; } - /** - * Return true if a stateless update should also delete any missing details beans. - */ - public boolean isDeleteMissingChildren() { - return deleteMissingChildren; - } - - /** - * Set if deleteMissingChildren occurs on cascade save to OneToMany or ManyToMany. - */ - public void setDeleteMissingChildren(boolean deleteMissingChildren) { - this.deleteMissingChildren = deleteMissingChildren; - } - /** * Prepare the update after potential modifications in a BeanPersistController. */ @@ -1476,4 +1460,8 @@ public final class PersistRequestBean extends PersistRequest implements BeanP } this.saveManyIntersections.add(saveManyIntersection); } + + public boolean isForcedUpdate() { + return Flags.isUpdateForce(flags); + } } diff --git a/src/main/java/io/ebeaninternal/server/core/Persister.java b/src/main/java/io/ebeaninternal/server/core/Persister.java index a0bdf1bb5..2bcda4b88 100644 --- a/src/main/java/io/ebeaninternal/server/core/Persister.java +++ b/src/main/java/io/ebeaninternal/server/core/Persister.java @@ -30,11 +30,6 @@ public interface Persister { */ void update(EntityBean entityBean, Transaction t); - /** - * Update the bean specifying deleteMissingChildren. - */ - void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren); - /** * Force an Insert using the given bean. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index f884bb900..353fc5195 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -49,6 +49,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST private final TableJoin intersectionJoin; private final String intersectionPublishTable; private final String intersectionDraftTable; + private final boolean orphanRemoval; private IntersectionTable intersectionTable; @@ -123,6 +124,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST public BeanPropertyAssocMany(BeanDescriptor descriptor, DeployBeanPropertyAssocMany deploy) { super(descriptor, deploy); this.unidirectional = deploy.isUnidirectional(); + this.orphanRemoval = deploy.isOrphanRemoval(); this.o2mJoinTable = deploy.isO2mJoinTable(); this.hasOrderColumn = deploy.hasOrderColumn(); this.manyToMany = deploy.isManyToMany(); @@ -510,6 +512,10 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST return hasOrderColumn; } + public boolean isOrphanRemoval() { + return orphanRemoval; + } + @Override public boolean isAssocMany() { return true; diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index 718a2d23b..aa41e95ab 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -39,11 +39,8 @@ import java.util.Map; public class BeanPropertyAssocOne extends BeanPropertyAssoc implements STreePropertyAssocOne { private final boolean oneToOne; - private final boolean oneToOneExported; - private final boolean orphanRemoval; - private final boolean primaryKeyExport; private final boolean primaryKeyJoin; @@ -89,7 +86,6 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc implements STr for (BeanProperty embeddedProp : embeddedProps) { embeddedPropsMap.put(embeddedProp.getName(), embeddedProp); } - } else { embeddedProps = null; embeddedPropsMap = null; diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java index bd2ccc6be..a7e060004 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java @@ -45,6 +45,8 @@ public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty { private PropertyForeignKey foreignKey; + boolean orphanRemoval; + /** * Construct the property. */ @@ -148,6 +150,14 @@ public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty { } } + public void setOrphanRemoval() { + orphanRemoval = true; + } + + public boolean isOrphanRemoval() { + return orphanRemoval; + } + /** * Set DocStoreEmbedded deployment information. */ 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 4dd42a503..79f253b86 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocOne.java @@ -21,8 +21,6 @@ public class DeployBeanPropertyAssocOne extends DeployBeanPropertyAssoc { private String columnPrefix; - private boolean orphanRemoval; - /** * Create the property. */ @@ -145,14 +143,6 @@ public class DeployBeanPropertyAssocOne extends DeployBeanPropertyAssoc { } } - public void setOrphanRemoval(boolean orphanRemoval) { - this.orphanRemoval = orphanRemoval; - } - - public boolean isOrphanRemoval() { - return orphanRemoval; - } - public void setJoinType(boolean outerJoin) { tableJoin.setType(outerJoin ? SqlJoinType.OUTER : SqlJoinType.INNER); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java index 3550fc192..55640603b 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -84,11 +84,13 @@ class AnnotationAssocManys extends AnnotationParser { if (oneToMany != null) { readToOne(oneToMany, prop); if (readOrphanRemoval(oneToMany)) { + prop.setOrphanRemoval(); prop.setModifyListenMode(ModifyListenMode.REMOVALS); prop.getCascadeInfo().setDelete(true); } PrivateOwned privateOwned = get(prop, PrivateOwned.class); if (privateOwned != null) { + prop.setOrphanRemoval(); prop.setModifyListenMode(ModifyListenMode.REMOVALS); prop.getCascadeInfo().setDelete(privateOwned.cascadeRemove()); } 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 dda924752..4e2084100 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -225,7 +225,9 @@ public class AnnotationAssocOnes extends AnnotationParser { prop.setNullable(propAnn.optional()); prop.setFetchType(propAnn.fetch()); prop.setMappedBy(propAnn.mappedBy()); - prop.setOrphanRemoval(readOrphanRemoval(propAnn)); + if (readOrphanRemoval(propAnn)) { + prop.setOrphanRemoval(); + } if (!"".equals(propAnn.mappedBy())) { prop.setOneToOneExported(); } diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index e3a93cfad..24541a636 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -74,11 +74,8 @@ public final class DefaultPersister implements Persister { private final BeanDescriptorManager beanDescriptorManager; - private final boolean updatesDeleteMissingChildren; - public DefaultPersister(SpiEbeanServer server, Binder binder, BeanDescriptorManager descMgr) { this.server = server; - this.updatesDeleteMissingChildren = server.getServerConfig().isUpdatesDeleteMissingChildren(); this.beanDescriptorManager = descMgr; this.persistExecute = new DefaultPersistExecute(binder, server.getServerConfig().getPersistBatchSize()); } @@ -407,17 +404,7 @@ public final class DefaultPersister implements Persister { */ @Override public void update(EntityBean entityBean, Transaction t) { - update(entityBean, t, updatesDeleteMissingChildren); - } - - /** - * Update the bean specifying deleteMissingChildren. - */ - @Override - public void update(EntityBean entityBean, Transaction t, boolean deleteMissingChildren) { - PersistRequestBean req = createRequest(entityBean, t, PersistRequest.Type.UPDATE); - req.setDeleteMissingChildren(deleteMissingChildren); req.checkDraft(); try { req.initTransIfRequiredWithBatchCascade(); @@ -448,8 +435,7 @@ public final class DefaultPersister implements Persister { @Override public void save(EntityBean bean, Transaction t) { if (bean._ebean_getIntercept().isUpdate()) { - // deleteMissingChildren is false when using 'save' on 'loaded' beans - update(bean, t, false); + update(bean, t); } else { insert(bean, t); } diff --git a/src/main/java/io/ebeaninternal/server/persist/Flags.java b/src/main/java/io/ebeaninternal/server/persist/Flags.java index 0d9a9ccf9..6c200911e 100644 --- a/src/main/java/io/ebeaninternal/server/persist/Flags.java +++ b/src/main/java/io/ebeaninternal/server/persist/Flags.java @@ -78,6 +78,10 @@ public final class Flags { return (state & PUBLISH_MERGE_NORMAL) != 0; } + public static boolean isUpdateForce(int state) { + return !isSet(state, INSERT) && !isSet(state, NORMAL); + } + /** * Return true if the given flag is set. */ @@ -149,10 +153,10 @@ public final class Flags { } private static int set(int state, int flag) { - return (state |= flag); + return state | flag; } private static int unset(int state, int flag) { - return state &= ~flag; + return state & ~flag; } } diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index afbf394cd..883947439 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -38,7 +38,6 @@ public class SaveManyBeans extends SaveManyBase { private Collection collection; private final DefaultPersister persister; - private boolean deleteMissing; private int sortOrder; SaveManyBeans(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request, DefaultPersister persister) { @@ -56,18 +55,17 @@ public class SaveManyBeans extends SaveManyBase { void save() { if (many.hasJoinTable()) { - // check if we can save the m2m intersection in this direction // we only allow one direction based on first traversed basis boolean saveIntersectionFromThisDirection = isSaveIntersection(); if (cascade) { - saveAssocManyDetails(false); + saveAssocManyDetails(); } // for ManyToMany save the 'relationship' via inserts/deletes // into/from the intersection table if (saveIntersectionFromThisDirection) { // only allowed on one direction of a m2m based on beanName - saveAssocManyIntersection(request.isDeleteMissingChildren()); + saveAssocManyIntersection(); } else { resetModifyState(); } @@ -79,7 +77,7 @@ public class SaveManyBeans extends SaveManyBase { } if (cascade) { // potentially deletes 'missing children' for 'stateless update' - saveAssocManyDetails(request.isDeleteMissingChildren()); + saveAssocManyDetails(); } } } @@ -98,13 +96,9 @@ public class SaveManyBeans extends SaveManyBase { /** * Save the details from a OneToMany collection. */ - private void saveAssocManyDetails(boolean deleteMissingChildren) { - - this.deleteMissing = deleteMissingChildren; - + private void saveAssocManyDetails() { // check that the list is not null and if it is a BeanCollection // check that is has been populated (don't trigger lazy loading) - collection = BeanCollectionUtil.getActualEntries(value); if (collection != null) { processDetails(); @@ -127,7 +121,7 @@ public class SaveManyBeans extends SaveManyBase { targetDescriptor.preAllocateIds(collection.size()); } - if (deleteMissing) { + if (!insertedParent && many.isOrphanRemoval() && request.isForcedUpdate()) { // collect the Id's (to exclude from deleteManyDetails) List detailIds = collectIds(collection, targetDescriptor, isMap); // deleting missing children - children not in our collected detailIds @@ -142,7 +136,6 @@ public class SaveManyBeans extends SaveManyBase { transaction.depth(-1); } - private void saveAllBeans(BeanProperty orderColumn) { // if a map, then we get the key value and @@ -240,17 +233,16 @@ public class SaveManyBeans extends SaveManyBase { * This is done via MapBeans. *

*/ - private void saveAssocManyIntersection(boolean deleteMissingChildren) { + private void saveAssocManyIntersection() { if (value == null) { return; } if (request.isQueueManyIntersection()) { // queue/delay until bean persist request is flushed - this.deleteMissing = deleteMissingChildren; request.addManyIntersection(this); } else { - saveAssocManyIntersection(deleteMissingChildren, false); + saveAssocManyIntersection(false); } } @@ -258,13 +250,14 @@ public class SaveManyBeans extends SaveManyBase { * Push intersection table changes onto batch flush queue. */ public void saveIntersectionBatch() { - saveAssocManyIntersection(deleteMissing, true); + saveAssocManyIntersection(true); } - private void saveAssocManyIntersection(boolean deleteMissingChildren, boolean queue) { + private void saveAssocManyIntersection(boolean queue) { + boolean forcedUpdate = request.isForcedUpdate(); boolean vanillaCollection = !(value instanceof BeanCollection); - if (vanillaCollection || deleteMissingChildren) { + if (vanillaCollection || forcedUpdate) { // delete all intersection rows and then treat all // beans in the collection as additions persister.deleteManyIntersection(parentBean, many, transaction, publish, queue); @@ -273,7 +266,7 @@ public class SaveManyBeans extends SaveManyBase { Collection deletions = null; Collection additions; - if (insertedParent || vanillaCollection || deleteMissingChildren) { + if (insertedParent || vanillaCollection || forcedUpdate) { // treat everything in the list/set/map as an intersection addition if (value instanceof Map) { additions = ((Map) value).values(); diff --git a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java index 5da3fdabf..636e4db84 100644 --- a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java @@ -809,10 +809,6 @@ public class TDSpiEbeanServer implements SpiEbeanServer { public void update(Object bean, Transaction t) throws OptimisticLockException { } - @Override - public void update(Object bean, Transaction transaction, boolean deleteMissingChildren) throws OptimisticLockException { - } - @Override public void insert(Object bean) { } diff --git a/src/test/java/org/tests/cascade/COOne.java b/src/test/java/org/tests/cascade/COOne.java new file mode 100644 index 000000000..cc2430574 --- /dev/null +++ b/src/test/java/org/tests/cascade/COOne.java @@ -0,0 +1,37 @@ +package org.tests.cascade; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import java.util.List; + +import static javax.persistence.CascadeType.ALL; + +@Entity +public class COOne { + + @Id + private long id; + + private final String name; + + @OneToMany(cascade = ALL, orphanRemoval = true) + private List children; + + public COOne(String name) { + this.name = name; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public List getChildren() { + return children; + } + +} diff --git a/src/test/java/org/tests/cascade/COOneMany.java b/src/test/java/org/tests/cascade/COOneMany.java new file mode 100644 index 000000000..8894ca8ee --- /dev/null +++ b/src/test/java/org/tests/cascade/COOneMany.java @@ -0,0 +1,34 @@ +package org.tests.cascade; + +import io.ebean.annotation.SoftDelete; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class COOneMany { + + @Id + private long id; + + @SoftDelete + private boolean deleted; + + private String name; + + public COOneMany(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public long getId() { + return id; + } + + public boolean isDeleted() { + return deleted; + } +} diff --git a/src/test/java/org/tests/cascade/CORoot.java b/src/test/java/org/tests/cascade/CORoot.java new file mode 100644 index 000000000..68700b0d3 --- /dev/null +++ b/src/test/java/org/tests/cascade/CORoot.java @@ -0,0 +1,36 @@ +package org.tests.cascade; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; + +import static javax.persistence.CascadeType.ALL; + +@Entity +public class CORoot { + + @Id + private long id; + + private final String name; + + @OneToOne(cascade = ALL, orphanRemoval = true) + private COOne one; + + public CORoot(String name, COOne one) { + this.name = name; + this.one = one; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public COOne getOne() { + return one; + } +} diff --git a/src/test/java/org/tests/cascade/TestCascadeOrphanStatelessUpdate.java b/src/test/java/org/tests/cascade/TestCascadeOrphanStatelessUpdate.java new file mode 100644 index 000000000..178c5271c --- /dev/null +++ b/src/test/java/org/tests/cascade/TestCascadeOrphanStatelessUpdate.java @@ -0,0 +1,66 @@ +package org.tests.cascade; + +import io.ebean.BaseTestCase; +import io.ebean.DB; +import io.ebean.text.json.JsonContext; +import org.junit.Test; + +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; + +public class TestCascadeOrphanStatelessUpdate extends BaseTestCase { + + @Test + public void update() { + + final CORoot orig = setup(); + + CORoot root = DB.find(CORoot.class, orig.getId()); + assertNotNull(root); + assertThat(root.getOne().getChildren()).hasSize(3); + + final JsonContext jsonContext = DB.json(); + String asJson = jsonContext.toJson(root); + + final CORoot deserialized = jsonContext.toBean(CORoot.class, asJson); + + final COOne one = deserialized.getOne(); + COOneMany removed0 = one.getChildren().remove(0); + COOneMany removed1 = one.getChildren().remove(1); + + one.getChildren().add(new COOneMany("m3")); + one.getChildren().add(new COOneMany("m4")); + + DB.update(deserialized); + + CORoot saved = DB.find(CORoot.class, orig.getId()); + assertNotNull(saved); + assertThat(saved.getOne().getChildren()).hasSize(3); + + List softDeleted = DB.find(COOneMany.class) + .setIncludeSoftDeletes() + .where() + .idIn(asList(removed0.getId(), removed1.getId())) + .findList(); + + assertThat(softDeleted).hasSize(2); + assertThat(softDeleted.stream().map(COOneMany::getId).collect(toList())) + .contains(removed0.getId(), removed1.getId()); + } + + private CORoot setup() { + COOne one = new COOne("one"); + one.getChildren().add(new COOneMany("m0")); + one.getChildren().add(new COOneMany("m1")); + one.getChildren().add(new COOneMany("m2")); + + CORoot root = new CORoot("r0", one); + DB.save(root); + + return root; + } +} diff --git a/src/test/java/org/tests/model/softdelete/EsdMaster.java b/src/test/java/org/tests/model/softdelete/EsdMaster.java index 23bb3d33a..29632338b 100644 --- a/src/test/java/org/tests/model/softdelete/EsdMaster.java +++ b/src/test/java/org/tests/model/softdelete/EsdMaster.java @@ -1,16 +1,17 @@ package org.tests.model.softdelete; -import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.OneToMany; import java.util.List; +import static javax.persistence.CascadeType.ALL; + @Entity public class EsdMaster extends BaseSoftDelete { String name; - @OneToMany(mappedBy = "master", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "master", cascade = ALL, orphanRemoval = true) List details; public EsdMaster(String name) { diff --git a/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java b/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java index 720447f58..cf06b2c0f 100644 --- a/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java +++ b/src/test/java/org/tests/softdelete/TestSoftDeleteStatelessUpdate.java @@ -1,7 +1,7 @@ package org.tests.softdelete; import io.ebean.BaseTestCase; -import io.ebean.Ebean; +import io.ebean.DB; import io.ebeantest.LoggedSql; import org.junit.Test; import org.tests.model.softdelete.EsdDetail; @@ -21,8 +21,7 @@ public class TestSoftDeleteStatelessUpdate extends BaseTestCase { master.getDetails().add(new EsdDetail("d2")); master.getDetails().add(new EsdDetail("d3")); - - Ebean.save(master); + DB.save(master); EsdMaster upd = new EsdMaster("m1-modified"); upd.setId(master.getId()); @@ -38,7 +37,7 @@ public class TestSoftDeleteStatelessUpdate extends BaseTestCase { LoggedSql.start(); - Ebean.getDefaultServer().update(upd, null, true); + DB.update(upd); List sql = LoggedSql.collect(); assertThat(sql).hasSize(5); @@ -47,7 +46,7 @@ public class TestSoftDeleteStatelessUpdate extends BaseTestCase { assertThat(sql.get(1)).contains("update esd_detail set deleted=true where master_id = ? and not"); } - EsdMaster fetchedWithSoftDeletes = Ebean.find(EsdMaster.class) + EsdMaster fetchedWithSoftDeletes = DB.find(EsdMaster.class) .setId(master.getId()) .setIncludeSoftDeletes() .fetch("details") @@ -59,7 +58,7 @@ public class TestSoftDeleteStatelessUpdate extends BaseTestCase { assertThat(sql).hasSize(1); assertThat(sql.get(0)).contains("left join esd_detail t1 on t1.master_id = t0.id where t0.id = ?"); - EsdMaster fetchedWithOutSoftDeletes = Ebean.find(EsdMaster.class) + EsdMaster fetchedWithOutSoftDeletes = DB.find(EsdMaster.class) .setId(master.getId()) .fetch("details") .findOne(); diff --git a/src/test/java/org/tests/update/TestStatelessUpdate.java b/src/test/java/org/tests/update/TestStatelessUpdate.java index 7a3376adf..21b7af20c 100644 --- a/src/test/java/org/tests/update/TestStatelessUpdate.java +++ b/src/test/java/org/tests/update/TestStatelessUpdate.java @@ -48,7 +48,7 @@ public class TestStatelessUpdate extends TransactionalTestCase { updateAll.setId(e.getId()); updateAll.setName("updAllProps"); - server.update(updateAll, null, false); + server.update(updateAll); eBasic = server.find(EBasic.class, e.getId()); assertEquals(e.getStatus(), eBasic.getStatus()); @@ -383,9 +383,7 @@ public class TestStatelessUpdate extends TransactionalTestCase { updateCustomer.getContacts().add(updateContact3); // not adding contact2 but it won't be deleted in this case - boolean deleteMissingChildren = false; - server.update(updateCustomer, null, deleteMissingChildren); - + server.update(updateCustomer); // assert Customer assCustomer = server.find(Customer.class, customer.getId());