diff --git a/src/main/java/io/ebean/bean/BeanCollection.java b/src/main/java/io/ebean/bean/BeanCollection.java index 25d84de7d..28b17bd05 100644 --- a/src/main/java/io/ebean/bean/BeanCollection.java +++ b/src/main/java/io/ebean/bean/BeanCollection.java @@ -227,6 +227,11 @@ public interface BeanCollection extends Serializable { */ void modifyReset(); + /** + * Has been modified by an addition or removal. + */ + boolean wasTouched(); + /** * Return a shallow copy of this collection that is modifiable. */ diff --git a/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/src/main/java/io/ebean/bean/EntityBeanIntercept.java index cdeb21bba..17b1eac73 100644 --- a/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -90,6 +90,7 @@ public final class EntityBeanIntercept implements Serializable { private int lazyLoadProperty = -1; private Object ownerId; + private int sortOrder; /** * Create a intercept with a given entity. @@ -693,6 +694,9 @@ public final class EntityBeanIntercept implements Serializable { * Add and return a dirty property hash recursing into embedded beans. */ private void addDirtyPropertyKey(StringBuilder sb) { + if (sortOrder > 0) { + sb.append("s,"); + } int len = getPropertyLength(); for (int i = 0; i < len; i++) { if (changedProps != null && changedProps[i]) { @@ -1050,4 +1054,18 @@ public final class EntityBeanIntercept implements Serializable { public void setOldValue(int propertyIndex, Object oldValue) { setChangedPropertyValue(propertyIndex, true, oldValue); } + + /** + * Return the sort order value for an order column. + */ + public int getSortOrder() { + return sortOrder; + } + + /** + * Set the sort order value for an order column. + */ + public void setSortOrder(int sortOrder) { + this.sortOrder = sortOrder; + } } diff --git a/src/main/java/io/ebean/common/AbstractBeanCollection.java b/src/main/java/io/ebean/common/AbstractBeanCollection.java index e13a6dc1d..d02c0ffc7 100644 --- a/src/main/java/io/ebean/common/AbstractBeanCollection.java +++ b/src/main/java/io/ebean/common/AbstractBeanCollection.java @@ -213,7 +213,12 @@ abstract class AbstractBeanCollection implements BeanCollection { boolean holdsModifications() { return modifyHolder != null && modifyHolder.hasModifications(); } - + + @Override + public boolean wasTouched() { + return modifyHolder != null && modifyHolder.wasTouched(); + } + /** * Copies all relevant properties for a clone. See {@link #getShallowCopy()} * @param other diff --git a/src/main/java/io/ebean/common/ModifyHolder.java b/src/main/java/io/ebean/common/ModifyHolder.java index 1e4008c32..247fb5cf2 100644 --- a/src/main/java/io/ebean/common/ModifyHolder.java +++ b/src/main/java/io/ebean/common/ModifyHolder.java @@ -28,7 +28,10 @@ class ModifyHolder implements Serializable { */ private Set modifyAdditions = new LinkedHashSet<>(); + private boolean touched; + void reset() { + touched = false; modifyDeletions = new LinkedHashSet<>(); modifyAdditions = new LinkedHashSet<>(); } @@ -50,6 +53,7 @@ class ModifyHolder implements Serializable { void modifyAddition(E bean) { if (bean != null) { + touched = true; // If it is to delete then just remove the deletion if (!undoDeletion(bean)) { // Insert @@ -65,6 +69,7 @@ class ModifyHolder implements Serializable { @SuppressWarnings("unchecked") void modifyRemoval(Object bean) { if (bean != null) { + touched = true; // If it is to be added then just remove the addition if (!undoAddition(bean)) { modifyDeletions.add((E) bean); @@ -80,6 +85,14 @@ class ModifyHolder implements Serializable { return modifyDeletions; } + /** + * Return true if the collection was touched in some way. This is still true even if + * a bean was removed and then added (which is not held as a modification). + */ + boolean wasTouched() { + return touched; + } + /** * Return true if there additions or removals. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index c5b82b19d..1c34a0783 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -308,6 +308,7 @@ public class BeanDescriptor implements BeanType { private final BeanPropertyAssocOne unidirectional; + private final BeanProperty orderColumn; /** * list of properties that are Lists/Sets/Maps (Derived). @@ -480,6 +481,7 @@ public class BeanDescriptor implements BeanType { this.propertiesLocal = listHelper.getLocal(); this.propertiesMutable = listHelper.getMutable(); this.unidirectional = listHelper.getUnidirectional(); + this.orderColumn = listHelper.getOrderColumn(); this.propertiesOne = listHelper.getOnes(); this.propertiesOneExportedSave = listHelper.getOneExportedSave(); this.propertiesOneExportedDelete = listHelper.getOneExportedDelete(); @@ -1925,6 +1927,13 @@ public class BeanDescriptor implements BeanType { return owner.getBeanDescriptor(otherType); } + /** + * Return the order column property. + */ + public BeanProperty getOrderColumn() { + return orderColumn; + } + /** * Return the "shadow" property to support unidirectional relationships. *

diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index d9452bc8f..d523bb739 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -38,6 +38,7 @@ import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssoc; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne; import io.ebeaninternal.server.deploy.meta.DeployBeanTable; +import io.ebeaninternal.server.deploy.meta.DeployOrderColumn; import io.ebeaninternal.server.deploy.meta.DeployTableJoin; import io.ebeaninternal.server.deploy.parse.DeployBeanInfo; import io.ebeaninternal.server.deploy.parse.DeployCreateProperties; @@ -50,6 +51,7 @@ import io.ebeaninternal.server.properties.BeanPropertiesReader; import io.ebeaninternal.server.properties.BeanPropertyAccess; import io.ebeaninternal.server.properties.EnhanceBeanPropertyAccess; import io.ebeaninternal.server.query.CQueryPlan; +import io.ebeaninternal.server.type.ScalarTypeInteger; import io.ebeaninternal.xmlmapping.XmlMappingReader; import io.ebeaninternal.xmlmapping.model.XmAliasMapping; import io.ebeaninternal.xmlmapping.model.XmColumnMapping; @@ -979,6 +981,23 @@ public class BeanDescriptorManager implements BeanDescriptorMap { throw new PersistenceException(msg); } + private void makeOrderColumn(DeployBeanPropertyAssocMany oneToMany) { + + DeployBeanDescriptor targetDesc = getTargetDescriptor(oneToMany); + + DeployOrderColumn orderColumn = oneToMany.getOrderColumn(); + DeployBeanProperty orderProperty = new DeployBeanProperty(targetDesc, Integer.class, ScalarTypeInteger.INSTANCE, null); + + orderProperty.setName(DeployOrderColumn.LOGICAL_NAME); + orderProperty.setDbColumn(orderColumn.getName()); + orderProperty.setNullable(orderColumn.isNullable()); + orderProperty.setDbInsertable(orderColumn.isInsertable()); + orderProperty.setDbUpdateable(orderColumn.isUpdatable()); + orderProperty.setDbRead(true); + + targetDesc.setOrderColumn(orderProperty); + } + /** * A OneToMany with no matching mappedBy property in the target so must be * unidirectional. @@ -1112,6 +1131,10 @@ public class BeanDescriptorManager implements BeanDescriptorMap { prop.getCascadeInfo().setSaveDelete(true, true); } + if (prop.hasOrderColumn()) { + makeOrderColumn(prop); + } + if (prop.getMappedBy() == null) { // if we are doc store only we are done // this allowes the use of @OneToMany in @DocStore - Entities diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 1a04e8ef9..8f902c047 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -58,6 +58,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { */ private final boolean unidirectional; + /** + * Flag to indicate that the target has a order column to auto populate. + */ + private final boolean hasOrderColumn; + /** * Flag to indicate manyToMany relationship. */ @@ -109,6 +114,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { public BeanPropertyAssocMany(BeanDescriptor descriptor, DeployBeanPropertyAssocMany deploy) { super(descriptor, deploy); this.unidirectional = deploy.isUnidirectional(); + this.hasOrderColumn = deploy.hasOrderColumn(); this.manyToMany = deploy.isManyToMany(); this.manyType = deploy.getManyType(); this.mapKey = deploy.getMapKey(); @@ -534,6 +540,10 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { return true; } + public boolean hasOrderColumn() { + return hasOrderColumn; + } + @Override public boolean isAssocMany() { return true; diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyOrderColumn.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyOrderColumn.java new file mode 100644 index 000000000..319c8830b --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyOrderColumn.java @@ -0,0 +1,36 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.bean.EntityBean; +import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; + +/** + * Bean property for synthetic sort order value / order column. + * + * The value of which is held on the entity bean intercept. + */ +public class BeanPropertyOrderColumn extends BeanProperty { + + public BeanPropertyOrderColumn(BeanDescriptor descriptor, DeployBeanProperty deploy) { + super(descriptor, deploy); + } + + @Override + public Object getValue(EntityBean bean) { + return bean._ebean_getIntercept().getSortOrder(); + } + + @Override + public Object getValueIntercept(EntityBean bean) { + return bean._ebean_getIntercept().getSortOrder(); + } + + @Override + public void setValue(EntityBean bean, Object value) { + bean._ebean_getIntercept().setSortOrder((int)value); + } + + @Override + public void setValueIntercept(EntityBean bean, Object value) { + bean._ebean_getIntercept().setSortOrder((int)value); + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 2e8b4165c..36e14a2e8 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -81,6 +81,8 @@ public class DeployBeanDescriptor { private DeployBeanPropertyAssocOne unidirectional; + private DeployBeanProperty orderColumn; + /** * Type of Identity generation strategy used. */ @@ -446,6 +448,14 @@ public class DeployBeanDescriptor { this.unidirectional = unidirectional; } + public void setOrderColumn(DeployBeanProperty orderColumn) { + this.orderColumn = orderColumn; + } + + public DeployBeanProperty getOrderColumn() { + return orderColumn; + } + /** * Return the concurrency mode used for beans of this type. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java index 06a6c9915..121938fab 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java @@ -12,35 +12,37 @@ public class DeployBeanPropertyAssocMany extends DeployBeanPropertyAssoc { /** * The type of the many, set, list or map. */ - final ManyType manyType; + private final ManyType manyType; ModifyListenMode modifyListenMode = ModifyListenMode.NONE; /** * Flag to indicate manyToMany relationship. */ - boolean manyToMany; + private boolean manyToMany; /** * Flag to indicate this is a unidirectional relationship. */ - boolean unidirectional; + private boolean unidirectional; /** * Join for manyToMany intersection table. */ - DeployTableJoin intersectionJoin; + private DeployTableJoin intersectionJoin; /** * For ManyToMany this is the Inverse join used to build reference queries. */ - DeployTableJoin inverseJoin; + private DeployTableJoin inverseJoin; - String fetchOrderBy; + private String fetchOrderBy; - String mapKey; + private String mapKey; - String intersectionDraftTable; + private String intersectionDraftTable; + + private DeployOrderColumn orderColumn; /** * Create this property. @@ -206,4 +208,16 @@ public class DeployBeanPropertyAssocMany extends DeployBeanPropertyAssoc { public void setIntersectionDraftTable() { this.intersectionDraftTable = intersectionJoin.getTable() + "_draft"; } + + public void setOrderColumn(DeployOrderColumn orderColumn) { + this.orderColumn = orderColumn; + } + + public DeployOrderColumn getOrderColumn() { + return orderColumn; + } + + public boolean hasOrderColumn() { + return orderColumn != null; + } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java index 5fb1b7010..c8bcfc6ce 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java @@ -6,6 +6,7 @@ import io.ebeaninternal.server.deploy.BeanDescriptorMap; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; +import io.ebeaninternal.server.deploy.BeanPropertyOrderColumn; import io.ebeaninternal.server.deploy.BeanPropertySimpleCollection; import io.ebeaninternal.server.deploy.InheritInfo; import io.ebeaninternal.server.deploy.TableJoin; @@ -64,6 +65,7 @@ public class DeployBeanPropertyLists { private final TableJoin[] tableJoins; private final BeanPropertyAssocOne unidirectional; + private final BeanProperty orderColumn; @SuppressWarnings({"unchecked", "rawtypes"}) public DeployBeanPropertyLists(BeanDescriptorMap owner, BeanDescriptor desc, DeployBeanDescriptor deploy) { @@ -71,12 +73,11 @@ public class DeployBeanPropertyLists { setImportedPrimaryKeys(deploy); + DeployBeanProperty deployOrderColumn = deploy.getOrderColumn(); + this.orderColumn = deployOrderColumn != null ? new BeanPropertyOrderColumn(desc, deployOrderColumn) : null; + DeployBeanPropertyAssocOne deployUnidirectional = deploy.getUnidirectional(); - if (deployUnidirectional == null) { - unidirectional = null; - } else { - unidirectional = new BeanPropertyAssocOne(owner, desc, deployUnidirectional); - } + this.unidirectional = deployUnidirectional == null ? null : new BeanPropertyAssocOne(owner, desc, deployUnidirectional); this.propertyMap = new LinkedHashMap<>(); @@ -89,7 +90,7 @@ public class DeployBeanPropertyLists { // Create a BeanProperty for the discriminator column to support // using RawSql queries with inheritance discriminatorColumn = inheritInfo.getDiscriminatorColumn(); - DeployBeanProperty discDeployProp = new DeployBeanProperty(deploy, String.class, new ScalarTypeString(), null); + DeployBeanProperty discDeployProp = new DeployBeanProperty(deploy, String.class, ScalarTypeString.INSTANCE, null); discDeployProp.setDiscriminator(); discDeployProp.setName(discriminatorColumn); discDeployProp.setDbColumn(discriminatorColumn); @@ -115,6 +116,12 @@ public class DeployBeanPropertyLists { allocateToList(prop); } + if (orderColumn != null) { + orderColumn.setDeployOrder(order++); + allocateToList(orderColumn); + propertyMap.put(orderColumn.getName(), orderColumn); + } + if (discProperty != null) { // put the discriminator property into the property map only // (after the real properties have been organised into their lists) @@ -160,6 +167,13 @@ public class DeployBeanPropertyLists { return unidirectional; } + /** + * Return the order column property. + */ + public BeanProperty getOrderColumn() { + return orderColumn; + } + /** * Allocate the property to a list. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployOrderColumn.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployOrderColumn.java new file mode 100644 index 000000000..26a8402b6 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployOrderColumn.java @@ -0,0 +1,39 @@ +package io.ebeaninternal.server.deploy.meta; + +import javax.persistence.OrderColumn; + +public class DeployOrderColumn { + + /** + * Logical property name used for order by and available for expression language. + */ + public static final String LOGICAL_NAME = "orderColumn"; + + private final String name; + private final boolean insertable; + private final boolean updatable; + private final boolean nullable; + + public DeployOrderColumn(OrderColumn orderColumn) { + this.name = orderColumn.name(); + this.insertable = orderColumn.insertable(); + this.updatable = orderColumn.updatable(); + this.nullable = orderColumn.nullable(); + } + + public String getName() { + return name; + } + + public boolean isInsertable() { + return insertable; + } + + public boolean isUpdatable() { + return updatable; + } + + public boolean isNullable() { + return nullable; + } +} 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 2d4b72ced..a451886ba 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -13,16 +13,19 @@ import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanTable; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; +import io.ebeaninternal.server.deploy.meta.DeployOrderColumn; import io.ebeaninternal.server.deploy.meta.DeployTableJoin; import io.ebeaninternal.server.deploy.meta.DeployTableJoinColumn; import io.ebeaninternal.server.query.SqlJoinType; +import javax.persistence.CascadeType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.MapKey; import javax.persistence.OneToMany; import javax.persistence.OrderBy; +import javax.persistence.OrderColumn; import java.util.Set; /** @@ -66,6 +69,14 @@ class AnnotationAssocManys extends AnnotationParser { prop.setModifyListenMode(ModifyListenMode.REMOVALS); prop.getCascadeInfo().setDelete(privateOwned.cascadeRemove()); } + OrderColumn orderColumn = get(prop, OrderColumn.class); + if (orderColumn != null) { + // need to cascade as we set the order on cascade + prop.setOrderColumn(new DeployOrderColumn(orderColumn)); + prop.setFetchOrderBy(DeployOrderColumn.LOGICAL_NAME); + prop.getCascadeInfo().setType(CascadeType.ALL); + prop.setModifyListenMode(ModifyListenMode.ALL); + } } ManyToMany manyToMany = get(prop, ManyToMany.class); if (manyToMany != null) { diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 5de669ab3..3c9effc4d 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -8,7 +8,6 @@ import io.ebean.Update; import io.ebean.bean.BeanCollection; import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebean.bean.EntityBean; -import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.PersistenceContext; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiTransaction; @@ -21,7 +20,6 @@ import io.ebeaninternal.server.core.PersistRequestCallableSql; import io.ebeaninternal.server.core.PersistRequestOrmUpdate; import io.ebeaninternal.server.core.PersistRequestUpdateSql; import io.ebeaninternal.server.core.Persister; -import io.ebeaninternal.server.deploy.BeanCollectionUtil; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanDescriptorManager; import io.ebeaninternal.server.deploy.BeanManager; @@ -29,7 +27,6 @@ import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.deploy.IntersectionRow; -import io.ebeaninternal.server.deploy.ManyType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -413,7 +410,7 @@ public final class DefaultPersister implements Persister { } } - private void saveRecurse(EntityBean bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) { + void saveRecurse(EntityBean bean, Transaction t, Object parentBean, boolean insertMode, boolean publish) { // determine insert or update taking into account stateless updates PersistRequestBean request = createRequestRecurse(bean, t, parentBean, insertMode, publish); @@ -826,86 +823,6 @@ public final class DefaultPersister implements Persister { } } - /** - * Helper to wrap the details when saving a OneToMany or ManyToMany - * relationship. - */ - private static class SaveManyPropRequest { - private final boolean insertedParent; - private final BeanPropertyAssocMany many; - private final EntityBean parentBean; - private final SpiTransaction transaction; - private final boolean cascade; - private final boolean deleteMissingChildren; - private final boolean publish; - - private SaveManyPropRequest(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { - this.insertedParent = insertedParent; - this.many = many; - this.cascade = many.getCascadeInfo().isSave(); - this.parentBean = parentBean; - this.transaction = request.getTransaction(); - this.deleteMissingChildren = request.isDeleteMissingChildren(); - this.publish = request.isPublish(); - } - - public boolean isSaveIntersection() { - return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName()); - } - - private Object getValue() { - return many.getValue(parentBean); - } - - private boolean isModifyListenMode() { - return ModifyListenMode.REMOVALS == many.getModifyListenMode(); - } - - private boolean isDeleteMissingChildren() { - return deleteMissingChildren; - } - - private boolean isInsertedParent() { - return insertedParent; - } - - private BeanPropertyAssocMany getMany() { - return many; - } - - private EntityBean getParentBean() { - return parentBean; - } - - private SpiTransaction getTransaction() { - return transaction; - } - - private boolean isCascade() { - return cascade; - } - - public void modifyListenReset(BeanCollection c) { - if (insertedParent) { - // after insert set the modify listening mode - // for private owned etc - c.setModifyListening(many.getModifyListenMode()); - } - c.modifyReset(); - } - - public boolean isPublish() { - return publish; - } - - private void resetModifyState() { - Object details = getValue(); - if (details instanceof BeanCollection) { - modifyListenReset((BeanCollection) details); - } - } - } - private void saveMany(SaveManyPropRequest saveMany, boolean insertMode) { if (saveMany.getMany().isManyToMany()) { @@ -967,107 +884,7 @@ public final class DefaultPersister implements Persister { */ private void saveAssocManyDetails(SaveManyPropRequest saveMany, boolean deleteMissingChildren, boolean insertMode) { - BeanPropertyAssocMany prop = saveMany.getMany(); - - Object details = saveMany.getValue(); - - // check that the list is not null and if it is a BeanCollection - // check that is has been populated (don't trigger lazy loading) - // For a Map this is a collection of Map.Entry objects and not beans - Collection collection = BeanCollectionUtil.getActualEntries(details); - - if (collection == null) { - // nothing to do here - return; - } - - BeanDescriptor targetDescriptor = prop.getTargetDescriptor(); - if (saveMany.isInsertedParent()) { - // performance optimisation for large collections - targetDescriptor.preAllocateIds(collection.size()); - } - - SpiTransaction t = saveMany.getTransaction(); - boolean isMap = ManyType.MAP == prop.getManyType(); - EntityBean parentBean = saveMany.getParentBean(); - - if (deleteMissingChildren) { - // collect the Id's (to exclude from deleteManyDetails) - List detailIds = collectIds(collection, targetDescriptor, isMap); - // deleting missing children - children not in our collected detailIds - deleteManyDetails(t, prop.getBeanDescriptor(), parentBean, prop, detailIds, false); - } - - // increase depth for batching order - t.depth(+1); - - // if a map, then we get the key value and - // set it to the appropriate property on the - // detail bean before we save it - Object mapKeyValue = null; - - boolean saveSkippable = prop.isSaveRecurseSkippable(); - boolean skipSavingThisBean; - - for (Object detailBean : collection) { - if (isMap) { - // its a map so need the key and value - Map.Entry entry = (Map.Entry) detailBean; - mapKeyValue = entry.getKey(); - detailBean = entry.getValue(); - } - - if (detailBean instanceof EntityBean) { - EntityBean detail = (EntityBean) detailBean; - EntityBeanIntercept ebi = detail._ebean_getIntercept(); - if (prop.isManyToMany()) { - skipSavingThisBean = targetDescriptor.isReference(ebi); - } else { - if (targetDescriptor.isReference(ebi)) { - // we can skip this one - skipSavingThisBean = true; - - } else if (ebi.isNewOrDirty()) { - skipSavingThisBean = false; - // set the parent bean to detailBean - prop.setJoinValuesToChild(parentBean, detail, mapKeyValue); - - } else { - // unmodified so skip depending on prop.isSaveRecurseSkippable(); - skipSavingThisBean = saveSkippable; - } - } - - if (!skipSavingThisBean) { - saveRecurse(detail, t, parentBean, insertMode, saveMany.isPublish()); - } - } - } - - t.depth(-1); - } - - /** - * Collect the Id values of the details to remove 'missing children' for stateless updates. - */ - private List collectIds(Collection collection, BeanDescriptor targetDescriptor, boolean isMap) { - - List detailIds = new ArrayList<>(); - // stateless update with deleteMissingChildren so first - // collect the Id values to remove the 'missing children' - for (Object detailBean : collection) { - if (isMap) { - detailBean = ((Map.Entry) detailBean).getValue(); - } - if (detailBean instanceof EntityBean) { - Object id = targetDescriptor.getId((EntityBean) detailBean); - if (!DmlUtil.isNullOrZero(id)) { - // remember the Id (other details not in the collection) will be removed - detailIds.add(id); - } - } - } - return detailIds; + saveMany.saveDetails(this, deleteMissingChildren, insertMode); } /** @@ -1266,7 +1083,7 @@ public final class DefaultPersister implements Persister { * collection (and should not be deleted). *

*/ - private void deleteManyDetails(SpiTransaction t, BeanDescriptor desc, EntityBean parentBean, + void deleteManyDetails(SpiTransaction t, BeanDescriptor desc, EntityBean parentBean, BeanPropertyAssocMany many, List excludeDetailIds, boolean softDelete) { if (many.getCascadeInfo().isDelete()) { diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java new file mode 100644 index 000000000..f25fd9d48 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java @@ -0,0 +1,247 @@ +package io.ebeaninternal.server.persist; + +import io.ebean.bean.BeanCollection; +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; +import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.deploy.BeanCollectionUtil; +import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.BeanProperty; +import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; +import io.ebeaninternal.server.deploy.ManyType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Helper to wrap the details when saving a OneToMany or ManyToMany relationship. + */ +class SaveManyPropRequest { + + private final boolean insertedParent; + private final BeanPropertyAssocMany many; + private final EntityBean parentBean; + private final SpiTransaction transaction; + private final boolean cascade; + private final boolean deleteMissingChildren; + private final boolean publish; + + private final Object value; + private final BeanDescriptor targetDescriptor; + private final boolean isMap; + private final boolean saveRecurseSkippable; + + private Collection collection; + private DefaultPersister persister; + private boolean deleteMissing; + private boolean insertMode; + private int sortOrder; + + SaveManyPropRequest(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + this.insertedParent = insertedParent; + this.many = many; + this.cascade = many.getCascadeInfo().isSave(); + this.parentBean = parentBean; + this.transaction = request.getTransaction(); + this.deleteMissingChildren = request.isDeleteMissingChildren(); + this.publish = request.isPublish(); + this.value = many.getValue(parentBean); + this.targetDescriptor = many.getTargetDescriptor(); + this.isMap = ManyType.MAP == many.getManyType(); + this.saveRecurseSkippable = many.isSaveRecurseSkippable(); + } + + public boolean isSaveIntersection() { + return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName()); + } + + Object getValue() { + return value; + } + + boolean isModifyListenMode() { + return BeanCollection.ModifyListenMode.REMOVALS == many.getModifyListenMode(); + } + + boolean isDeleteMissingChildren() { + return deleteMissingChildren; + } + + boolean isInsertedParent() { + return insertedParent; + } + + BeanPropertyAssocMany getMany() { + return many; + } + + EntityBean getParentBean() { + return parentBean; + } + + SpiTransaction getTransaction() { + return transaction; + } + + boolean isCascade() { + return cascade; + } + + boolean isPublish() { + return publish; + } + + void modifyListenReset(BeanCollection c) { + if (insertedParent) { + // after insert set the modify listening mode for private owned etc + c.setModifyListening(many.getModifyListenMode()); + } + c.modifyReset(); + } + + void resetModifyState() { + if (value instanceof BeanCollection) { + modifyListenReset((BeanCollection) value); + } + } + + void saveDetails(DefaultPersister persister, boolean deleteMissing, boolean insertMode) { + + this.persister = persister; + this.deleteMissing = deleteMissing; + this.insertMode = insertMode; + + // check that the list is not null and if it is a BeanCollection + // check that is has been populated (don't trigger lazy loading) + // For a Map this is a collection of Map.Entry objects and not beans + collection = BeanCollectionUtil.getActualEntries(value); + if (collection != null) { + processDetails(); + } + } + + private void processDetails() { + + BeanProperty orderColumn = null; + boolean hasOrderColumn = many.hasOrderColumn(); + if (hasOrderColumn) { + if (!insertedParent && canSkipForOrderColumn()) { + return; + } + orderColumn = targetDescriptor.getOrderColumn(); + } + + if (insertedParent) { + // performance optimisation for large collections + targetDescriptor.preAllocateIds(collection.size()); + } + + if (deleteMissing) { + // collect the Id's (to exclude from deleteManyDetails) + List detailIds = collectIds(collection, targetDescriptor, isMap); + // deleting missing children - children not in our collected detailIds + persister.deleteManyDetails(transaction, many.getBeanDescriptor(), parentBean, many, detailIds, false); + } + + transaction.depth(+1); + saveAllBeans(orderColumn); + if (hasOrderColumn) { + resetModifyState(); + } + transaction.depth(-1); + } + + + private void saveAllBeans(BeanProperty orderColumn) { + + // if a map, then we get the key value and + // set it to the appropriate property on the + // detail bean before we save it + Object mapKeyValue = null; + boolean skipSavingThisBean; + + for (Object detailBean : collection) { + sortOrder++; + if (isMap) { + // its a map so need the key and value + Map.Entry entry = (Map.Entry) detailBean; + mapKeyValue = entry.getKey(); + detailBean = entry.getValue(); + } + + if (detailBean instanceof EntityBean) { + EntityBean detail = (EntityBean) detailBean; + EntityBeanIntercept ebi = detail._ebean_getIntercept(); + if (many.isManyToMany()) { + skipSavingThisBean = targetDescriptor.isReference(ebi); + } else { + if (orderColumn != null) { + orderColumn.setValue(detail, sortOrder); + ebi.setDirty(true); + } + if (targetDescriptor.isReference(ebi)) { + // we can skip this one + skipSavingThisBean = true; + + } else if (ebi.isNewOrDirty()) { + skipSavingThisBean = false; + // set the parent bean to detailBean + many.setJoinValuesToChild(parentBean, detail, mapKeyValue); + + } else { + // unmodified so skip depending on prop.isSaveRecurseSkippable(); + skipSavingThisBean = saveRecurseSkippable; + } + } + + if (!skipSavingThisBean) { + persister.saveRecurse(detail, transaction, parentBean, insertMode, publish); + } + } + } + } + + /** + * Return true if we can skip based on .. no modifications to the collection and no beans are dirty. + */ + private boolean canSkipForOrderColumn() { + return value instanceof BeanCollection + && !((BeanCollection) value).wasTouched() + && noDirtyBeans(); + } + + private boolean noDirtyBeans() { + for (Object bean : collection) { + if (bean instanceof EntityBean && ((EntityBean) bean)._ebean_getIntercept().isDirty()) { + return false; + } + } + return true; + } + + /** + * Collect the Id values of the details to remove 'missing children' for stateless updates. + */ + private List collectIds(Collection collection, BeanDescriptor targetDescriptor, boolean isMap) { + + List detailIds = new ArrayList<>(); + // stateless update with deleteMissingChildren so first + // collect the Id values to remove the 'missing children' + for (Object detailBean : collection) { + if (isMap) { + detailBean = ((Map.Entry) detailBean).getValue(); + } + if (detailBean instanceof EntityBean) { + Object id = targetDescriptor.getId((EntityBean) detailBean); + if (!DmlUtil.isNullOrZero(id)) { + // remember the Id (other details not in the collection) will be removed + detailIds.add(id); + } + } + } + return detailIds; + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/dml/MetaFactory.java b/src/main/java/io/ebeaninternal/server/persist/dml/MetaFactory.java index 9ab432141..7bc370757 100644 --- a/src/main/java/io/ebeaninternal/server/persist/dml/MetaFactory.java +++ b/src/main/java/io/ebeaninternal/server/persist/dml/MetaFactory.java @@ -3,10 +3,12 @@ package io.ebeaninternal.server.persist.dml; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DbEncrypt; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.persist.dmlbind.Bindable; import io.ebeaninternal.server.persist.dmlbind.BindableId; import io.ebeaninternal.server.persist.dmlbind.BindableList; +import io.ebeaninternal.server.persist.dmlbind.BindableOrderColumn; import io.ebeaninternal.server.persist.dmlbind.BindableUnidirectional; import io.ebeaninternal.server.persist.dmlbind.FactoryAssocOnes; import io.ebeaninternal.server.persist.dmlbind.FactoryBaseProperties; @@ -62,6 +64,11 @@ public class MetaFactory { embeddedFact.create(setList, desc, DmlMode.UPDATE, includeLobs); assocOneFact.create(setList, desc, DmlMode.UPDATE); + BeanProperty orderColumn = desc.getOrderColumn(); + if (orderColumn != null) { + setList.add(new BindableOrderColumn(orderColumn)); + } + BindableId id = idFact.createId(desc); Bindable version = versionFact.create(desc); Bindable tenantId = versionFact.createTenantId(desc); diff --git a/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableOrderColumn.java b/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableOrderColumn.java new file mode 100644 index 000000000..ef7046634 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableOrderColumn.java @@ -0,0 +1,37 @@ +package io.ebeaninternal.server.persist.dmlbind; + +import io.ebean.bean.EntityBean; +import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.deploy.BeanProperty; + +import java.sql.SQLException; +import java.util.List; + +/** + * Bindable for the synthetic order column. + */ +public class BindableOrderColumn extends BindableProperty { + + public BindableOrderColumn(BeanProperty prop) { + super(prop); + } + + @Override + public void addToUpdate(PersistRequestBean request, List list) { + int sortOrder = request.getEntityBeanIntercept().getSortOrder(); + if (sortOrder > 0) { + list.add(this); + } + } + + /** + * Normal binding of a property value from the bean. + */ + @Override + public void dmlBind(BindableRequest request, EntityBean bean) throws SQLException { + + int sortOrder = bean._ebean_getIntercept().getSortOrder(); + request.bind(sortOrder, prop); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 1e3812b3a..a6dd341d4 100644 --- a/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -109,7 +109,7 @@ public final class DefaultTypeManager implements TypeManager { private final ScalarType shortType = new ScalarTypeShort(); - private final ScalarType integerType = new ScalarTypeInteger(); + private final ScalarType integerType = ScalarTypeInteger.INSTANCE; private final ScalarType longType = new ScalarTypeLong(); @@ -130,7 +130,7 @@ public final class DefaultTypeManager implements TypeManager { private final ScalarType currencyType = new ScalarTypeCurrency(); private final ScalarType timeZoneType = new ScalarTypeTimeZone(); - private final ScalarType stringType = new ScalarTypeString(); + private final ScalarType stringType = ScalarTypeString.INSTANCE; private final ScalarType classType = new ScalarTypeClass(); diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java index 6a4caddcd..51f6cfe4d 100644 --- a/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeInteger.java @@ -1,10 +1,10 @@ package io.ebeaninternal.server.type; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import io.ebean.text.TextException; import io.ebeaninternal.server.core.BasicTypeConverter; import io.ebeanservice.docstore.api.mapping.DocPropertyType; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; import java.io.DataInput; import java.io.DataOutput; @@ -17,7 +17,9 @@ import java.sql.Types; */ public class ScalarTypeInteger extends ScalarTypeBase { - public ScalarTypeInteger() { + public static ScalarTypeInteger INSTANCE = new ScalarTypeInteger(); + + private ScalarTypeInteger() { super(Integer.class, true, Types.INTEGER); } diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeString.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeString.java index 164a55fcf..f9a31355c 100644 --- a/src/main/java/io/ebeaninternal/server/type/ScalarTypeString.java +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeString.java @@ -1,9 +1,9 @@ package io.ebeaninternal.server.type; -import io.ebeaninternal.server.core.BasicTypeConverter; -import io.ebeanservice.docstore.api.mapping.DocPropertyType; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import io.ebeaninternal.server.core.BasicTypeConverter; +import io.ebeanservice.docstore.api.mapping.DocPropertyType; import java.io.DataInput; import java.io.DataOutput; @@ -16,7 +16,9 @@ import java.sql.Types; */ public class ScalarTypeString extends ScalarTypeBase { - public ScalarTypeString() { + public static final ScalarTypeString INSTANCE = new ScalarTypeString(); + + private ScalarTypeString() { super(String.class, true, Types.VARCHAR); } diff --git a/src/test/java/org/tests/cascade/OmOrderedDetail.java b/src/test/java/org/tests/cascade/OmOrderedDetail.java new file mode 100644 index 000000000..b15a4ff8e --- /dev/null +++ b/src/test/java/org/tests/cascade/OmOrderedDetail.java @@ -0,0 +1,57 @@ +package org.tests.cascade; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Version; + +@Entity +public class OmOrderedDetail { + + @Id + Long id; + + String name; + + @ManyToOne + OmOrderedMaster master; + + @Version + Long version; + + public OmOrderedDetail(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OmOrderedMaster getMaster() { + return master; + } + + public void setMaster(OmOrderedMaster master) { + this.master = master; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/cascade/OmOrderedMaster.java b/src/test/java/org/tests/cascade/OmOrderedMaster.java new file mode 100644 index 000000000..8607084d3 --- /dev/null +++ b/src/test/java/org/tests/cascade/OmOrderedMaster.java @@ -0,0 +1,63 @@ +package org.tests.cascade; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.OrderColumn; +import javax.persistence.Version; +import java.util.List; + +@Entity +public class OmOrderedMaster { + + @Id + Long id; + + String name; + + /** + * Cascade ALL set automatically as we set order values when cascading. + */ + @OneToMany(mappedBy = "master") //, cascade = CascadeType.ALL) + @OrderColumn(name="sort_order") + List details; + + @Version + Long version; + + public OmOrderedMaster(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getDetails() { + return details; + } + + public void setDetails(List details) { + this.details = details; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/cascade/TestOrderedList.java b/src/test/java/org/tests/cascade/TestOrderedList.java new file mode 100644 index 000000000..9baf7a601 --- /dev/null +++ b/src/test/java/org/tests/cascade/TestOrderedList.java @@ -0,0 +1,98 @@ +package org.tests.cascade; + +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 TestOrderedList extends BaseTestCase { + + @Test + public void test() { + + OmOrderedMaster master = new OmOrderedMaster("m1"); + + List details = master.getDetails(); + for (int i = 0; i < 5; i++) { + details.add(new OmOrderedDetail("d"+i)); + } + + LoggedSqlCollector.start(); + Ebean.save(master); + + List sql = LoggedSqlCollector.current(); + assertThat(sql.size()).isGreaterThan(1); + assertThat(sql.get(0)).contains("insert into om_ordered_master"); + assertThat(sql.get(1)).contains("insert into om_ordered_detail (name, version, sort_order, master_id) values (?,?,?,?)"); + + // update without any changes + Ebean.save(master); + + sql = LoggedSqlCollector.current(); + assertThat(sql).isEmpty(); + + + // update just changing master + master.setName("m1-mod"); + Ebean.save(master); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("update om_ordered_master set name=?, version=?"); + + + fetchAndReorder(master.getId()); + + } + + private void fetchAndReorder(Long id) { + + OmOrderedMaster fresh = Ebean.find(OmOrderedMaster.class).setId(id).fetch("details").findOne(); + List details1 = fresh.getDetails(); + + List sql = LoggedSqlCollector.current(); + assertThat(sql.get(0)).contains("order by t0.id, t1.sort_order"); + + // fetched, not dirty + Ebean.save(fresh); + sql = LoggedSqlCollector.current(); + assertThat(sql).isEmpty(); + + + // reorder + OmOrderedDetail third = details1.remove(2); + OmOrderedDetail first = details1.remove(0); + details1.add(first); + details1.add(third); + + fresh.setName("m1-reorder"); + + Ebean.save(fresh); + + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(6); + assertThat(sql.get(0)).contains("update om_ordered_master set name=?, version=?"); + assertThat(sql.get(1)).contains("update om_ordered_detail set version=?, sort_order=? where id=? and version=?"); + + details1.get(1).setName("was 1"); + fresh.setName("m1-mod3"); + Ebean.save(fresh); + + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(6); + assertThat(sql.get(0)).contains("update om_ordered_master set name=?, version=?"); + assertThat(sql.get(1)).contains("update om_ordered_detail set version=?, sort_order=? where id=? and version=?"); + assertThat(sql.get(2)).contains("update om_ordered_detail set name=?, version=?, sort_order=? where id=? and version=?"); + + + Ebean.delete(fresh); + + sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from om_ordered_detail where master_id = ?"); + assertThat(sql.get(1)).contains("delete from om_ordered_master where id=? and version=?"); + } +}