diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionHelpFactory.java b/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionHelpFactory.java index 86dd30126..d57b622b1 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionHelpFactory.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionHelpFactory.java @@ -28,7 +28,7 @@ public class BeanCollectionHelpFactory { case SET: return elementCollection ? new BeanSetHelpElement<>(many) : new BeanSetHelp<>(many); case MAP: - return new BeanMapHelp<>(many); + return elementCollection ? new BeanMapHelpElement<>(many) :new BeanMapHelp<>(many); default: throw new RuntimeException("Invalid type " + manyType); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 6f31713a0..8a9d0fa4b 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -3330,6 +3330,10 @@ public class BeanDescriptor implements BeanType, STreeType { jsonHelp.jsonWriteDirtyProperties(writeJson, bean, dirtyProps); } + public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws IOException { + throw new IllegalStateException("Unexpected - expect Element override"); + } + public void jsonWriteElement(SpiJsonWriter ctx, Object element) { throw new IllegalStateException("Unexpected - expect Element override"); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java index a9c6fa057..a1c23603a 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java @@ -31,6 +31,7 @@ class BeanDescriptorElement extends BeanDescriptor { } } + @Override public boolean isElementType() { return true; } @@ -40,6 +41,7 @@ class BeanDescriptorElement extends BeanDescriptor { return new ElementEntityBean(properties); } + @Override public void jsonWriteElement(SpiJsonWriter ctx, Object element) { try { scalarType.jsonWrite(ctx.gen(), element); @@ -48,6 +50,7 @@ class BeanDescriptorElement extends BeanDescriptor { } } + @Override public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException { JsonParser parser = readJson.getParser(); diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementMap.java new file mode 100644 index 000000000..3319ba8fc --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementMap.java @@ -0,0 +1,97 @@ +package io.ebeaninternal.server.deploy; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.ebean.bean.EntityBean; +import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; +import io.ebeaninternal.server.text.json.ReadJson; +import io.ebeaninternal.server.text.json.SpiJsonWriter; +import io.ebeaninternal.server.type.ScalarType; + +import java.io.IOException; +import java.util.Map; + +/** + * Bean descriptor used with ElementCollection (where we don't have a mapped type/class). + */ +class BeanDescriptorElementMap extends BeanDescriptor { + + private final ScalarType[] scalarTypes; + private final ElementHelp elementHelp; + private final boolean stringKey; + + BeanDescriptorElementMap(BeanDescriptorMap owner, DeployBeanDescriptor deploy, ElementHelp elementHelp) { + super(owner, deploy); + this.elementHelp = elementHelp; + BeanProperty[] props = propertiesNonTransient(); + this.scalarTypes = new ScalarType[props.length]; + for (int i = 0; i < props.length; i++) { + scalarTypes[i] = props[i].getScalarType(); + } + this.stringKey = String.class.equals(scalarTypes[0].getType()); + } + + @Override + public boolean isElementType() { + return true; + } + + @Override + protected EntityBean createPrototypeEntityBean(Class beanType) { + return new ElementEntityBean(properties); + } + + @Override + @SuppressWarnings("unchecked") + public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws IOException { + ctx.writeStartObject(); + if (stringKey) { + Object key = entry.getKey(); + String keyName = (key == null) ? "null" : key.toString(); + ctx.writeFieldName(keyName); + scalarTypes[1].jsonWrite(ctx.gen(), entry.getValue()); + } else { + ctx.writeFieldName("key"); + scalarTypes[0].jsonWrite(ctx.gen(), entry.getKey()); + ctx.writeFieldName("value"); + scalarTypes[1].jsonWrite(ctx.gen(), entry.getValue()); + } + ctx.writeEndObject(); + } + + @Override + public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException { + + JsonParser parser = readJson.getParser(); + ElementCollector add = elementHelp.createCollector(); + do { + JsonToken token = parser.nextToken(); + if (token != JsonToken.START_OBJECT) { + break; + } + if (stringKey) { + String key = parser.nextFieldName(); + parser.nextToken(); + Object val = scalarTypes[0].jsonRead(parser); + add.addKeyValue(key, val); + + } else { + parser.nextFieldName(); + Object key= scalarTypes[0].jsonRead(parser); + + parser.nextFieldName(); + Object val = scalarTypes[0].jsonRead(parser); + add.addKeyValue(key, val); + } + + token = parser.nextToken(); + if (token != JsonToken.END_OBJECT) { + break; + } + + } while (true); + + return add.collection(); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 1fbb6a15c..190322d07 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -1660,7 +1660,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { */ public BeanDescriptor createElementDescriptor(DeployBeanDescriptor elementDescriptor, ManyType manyType) { - return new BeanDescriptorElement<>(this, elementDescriptor, elementHelper(manyType)); + ElementHelp elementHelp = elementHelper(manyType); + if (manyType.isMap()) { + return new BeanDescriptorElementMap<>(this, elementDescriptor, elementHelp); + } + return new BeanDescriptorElement<>(this, elementDescriptor, elementHelp); } private ElementHelp elementHelper(ManyType manyType) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java b/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java index 4c4e76190..b45bbc98b 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java @@ -17,7 +17,7 @@ import java.util.Map.Entry; /** * Helper specifically for dealing with Maps. */ -public final class BeanMapHelp extends BaseCollectionHelp { +public class BeanMapHelp extends BaseCollectionHelp { private final BeanPropertyAssocMany many; private final BeanDescriptor targetDescriptor; @@ -175,8 +175,7 @@ public final class BeanMapHelp extends BaseCollectionHelp { if (!map.isEmpty() || ctx.isIncludeEmpty()) { ctx.beginAssocMany(name); for (Entry entry : map.entrySet()) { - //FIXME: json write map key ... - targetDescriptor.jsonWrite(ctx, (EntityBean) entry.getValue()); + many.jsonWriteMapEntry(ctx, entry); } ctx.endAssocMany(); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelpElement.java b/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelpElement.java new file mode 100644 index 000000000..0531839e0 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelpElement.java @@ -0,0 +1,31 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.bean.BeanCollection; +import io.ebean.bean.EntityBean; +import io.ebean.common.BeanMap; +import io.ebeaninternal.server.text.json.SpiJsonWriter; + +public class BeanMapHelpElement extends BeanMapHelp { + + BeanMapHelpElement(BeanPropertyAssocMany many) { + super(many); + } + + @Override + public void add(BeanCollection collection, EntityBean bean, boolean withCheck) { + Object key = bean._ebean_getField(0); + Object val = bean._ebean_getField(1); + + BeanMap map = ((BeanMap) collection); + if (withCheck) { + map.internalPutWithCheck(key, val); + } else { + map.internalPut(key, val); + } + } + + @Override + void jsonWriteElement(SpiJsonWriter ctx, Object element) { + throw new IllegalStateException("not called"); + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 392e24e33..0072949a6 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -882,8 +882,13 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST return help; } + public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws IOException { + // Writing as json array rather than object ... + targetDescriptor.jsonWrite(ctx, (EntityBean) entry.getValue()); + } + public void jsonWriteElementValue(SpiJsonWriter ctx, Object element) { - throw new IllegalStateException("Never Expected"); + throw new IllegalStateException("Unexpected - expect Element override"); } /** diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java index 639f6782a..6b9d1602e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java @@ -1,12 +1,12 @@ package io.ebeaninternal.server.deploy; -import io.ebean.PersistenceIOException; import io.ebean.bean.EntityBean; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertySimpleCollection; import io.ebeaninternal.server.text.json.ReadJson; import io.ebeaninternal.server.text.json.SpiJsonWriter; import java.io.IOException; +import java.util.Map; public class BeanPropertySimpleCollection extends BeanPropertyAssocMany { @@ -34,18 +34,12 @@ public class BeanPropertySimpleCollection extends BeanPropertyAssocMany { } } - /** - * Json write scalar value. - */ @Override - public void jsonWriteValue(SpiJsonWriter writeJson, Object value) { - try { - scalarType.jsonWrite(writeJson.gen(), value); - } catch (IOException e) { - throw new PersistenceIOException(e); - } + public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws IOException { + elementDescriptor.jsonWriteMapEntry(ctx, entry); } + @Override public void jsonWriteElementValue(SpiJsonWriter ctx, Object element) { elementDescriptor.jsonWriteElement(ctx, element); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/ElementCollector.java b/src/main/java/io/ebeaninternal/server/deploy/ElementCollector.java index c75cfff80..d5b97bfdd 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/ElementCollector.java +++ b/src/main/java/io/ebeaninternal/server/deploy/ElementCollector.java @@ -10,6 +10,11 @@ public interface ElementCollector { */ void addElement(Object element); + /** + * Add an element. + */ + void addKeyValue(Object key, Object element); + /** * Return the populated collection/map. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/ElementHelpList.java b/src/main/java/io/ebeaninternal/server/deploy/ElementHelpList.java index 6e3f46c34..6c95550da 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/ElementHelpList.java +++ b/src/main/java/io/ebeaninternal/server/deploy/ElementHelpList.java @@ -19,6 +19,11 @@ class ElementHelpList implements ElementHelp { list.add(element); } + @Override + public void addKeyValue(Object key, Object element) { + throw new IllegalStateException("never called"); + } + @Override public Object collection() { return list; diff --git a/src/main/java/io/ebeaninternal/server/deploy/ElementHelpMap.java b/src/main/java/io/ebeaninternal/server/deploy/ElementHelpMap.java index 1d8869cfd..2d970a48f 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/ElementHelpMap.java +++ b/src/main/java/io/ebeaninternal/server/deploy/ElementHelpMap.java @@ -16,8 +16,12 @@ class ElementHelpMap implements ElementHelp { @Override public void addElement(Object element) { - throw new RuntimeException("asd"); - //map.put() + throw new IllegalStateException("never called"); + } + + @Override + public void addKeyValue(Object key, Object element) { + map.put(key, element); } @Override diff --git a/src/main/java/io/ebeaninternal/server/deploy/ElementHelpSet.java b/src/main/java/io/ebeaninternal/server/deploy/ElementHelpSet.java index d104b1d31..53a47de89 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/ElementHelpSet.java +++ b/src/main/java/io/ebeaninternal/server/deploy/ElementHelpSet.java @@ -19,6 +19,11 @@ class ElementHelpSet implements ElementHelp { set.add(element); } + @Override + public void addKeyValue(Object key, Object element) { + throw new IllegalStateException("never called"); + } + @Override public Object collection() { return set; 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 eba9a4fbb..7c98d20c4 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -29,6 +29,7 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.MapKey; +import javax.persistence.MapKeyColumn; import javax.persistence.OneToMany; import javax.persistence.OrderBy; import javax.persistence.OrderColumn; @@ -212,24 +213,50 @@ class AnnotationAssocManys extends AnnotationParser { elementDescriptor.setBaseTable(new TableName(fullTableName), readConfig.getAsOfViewSuffix(), readConfig.getVersionsBetweenSuffix()); ScalarType scalarType = util.getTypeManager().getScalarType(elementType); - DeployBeanProperty elementProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null); - elementProp.setName("value"); - elementProp.setDbColumn(prop.getDbColumn()); + int sortOrder = 0; + if (!prop.getManyType().isMap()) { + elementDescriptor.setProperties(new String[]{"value"}); + } else { + elementDescriptor.setProperties(new String[]{"key", "value"}); + String dbKeyColumn = "key"; + MapKeyColumn mapKeyColumn = get(prop, MapKeyColumn.class); + if (mapKeyColumn != null) { + dbKeyColumn = mapKeyColumn.name(); + } + + DeployBeanProperty keyProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null); + setElementProperty(keyProp, "key", dbKeyColumn, sortOrder++); + elementDescriptor.addBeanProperty(keyProp); + if (mapKeyColumn != null) { + keyProp.setDbLength(mapKeyColumn.length()); + keyProp.setDbScale(mapKeyColumn.scale()); + } + } + + DeployBeanProperty valueProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null); + setElementProperty(valueProp, "value", prop.getDbColumn(), sortOrder++); + if (column != null) { + valueProp.setDbLength(column.length()); + valueProp.setDbScale(column.scale()); + } + + elementDescriptor.addBeanProperty(valueProp); + elementDescriptor.setName(prop.getFullBeanName()); + + factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin()); + prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType())); + } + + private void setElementProperty(DeployBeanProperty elementProp, String name, String dbColumn, int sortOrder) { + elementProp.setName(name); + elementProp.setDbColumn(dbColumn); elementProp.setNullable(false); elementProp.setDbInsertable(true); elementProp.setDbUpdateable(true); elementProp.setDbRead(true); + elementProp.setSortOrder(sortOrder); elementProp.setElementProperty(); - - elementDescriptor.addBeanProperty(elementProp); - elementDescriptor.setProperties(new String[]{"value"}); - elementDescriptor.setName(prop.getFullBeanName()); - - Class owningType = prop.getOwningType(); - - factory.createUnidirectional(elementDescriptor, owningType, beanTable, prop.getTableJoin()); - prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType())); } /** diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index e1a96c042..29c64d4d1 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -213,7 +213,6 @@ public class DeployCreateProperties { } catch (NullPointerException e) { logger.debug("expected non-scalar type {}", e.getMessage()); } - // TODO: Handle Collection of CompoundType and Embedded Type return new DeployBeanPropertyAssocMany(desc, targetType, manyType); } diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 7a9748649..f02969461 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -29,7 +29,6 @@ import io.ebeaninternal.server.deploy.BeanManager; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; -import io.ebeaninternal.server.deploy.BeanPropertySimpleCollection; import io.ebeaninternal.server.deploy.IntersectionRow; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -554,7 +553,7 @@ public final class DefaultPersister implements Persister { /** * Execute the delete request returning true if a delete occurred. */ - private int deleteRequest(PersistRequestBean req) { + int deleteRequest(PersistRequestBean req) { return deleteRequest(req, null); } @@ -890,7 +889,7 @@ public final class DefaultPersister implements Persister { for (BeanPropertyAssocMany many : desc.propertiesManySave()) { // check that property is loaded and collection should be cascaded to if (request.isLoadedProperty(many) && !many.isSkipSaveBeanCollection(parentBean, insertedParent)) { - saveMany2(insertedParent, many, parentBean, request); + saveMany(insertedParent, many, parentBean, request); if (!insertedParent) { request.addUpdatedManyProperty(many); } @@ -898,201 +897,23 @@ public final class DefaultPersister implements Persister { } } - private void saveMany2(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { - saveMany(saveManyRequest(insertedParent, many, parentBean, request)); + private void saveMany(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + saveManyRequest(insertedParent, many, parentBean, request).save(); } - private SaveManyPropRequest saveManyRequest(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { - if (many instanceof BeanPropertySimpleCollection) { + private SaveManyBase saveManyRequest(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + if (!many.isElementCollection()) { + return new SaveManyBeans(insertedParent, many, parentBean, request, this); + + } else if (many.getManyType().isMap()) { + return new SaveManySimpleMap(insertedParent, many, parentBean, request); + + } else { return new SaveManySimpleCollection(insertedParent, many, parentBean, request); - } else { - return new SaveManyPropRequest(insertedParent, many, parentBean, request); } } - private void saveMany(SaveManyPropRequest saveMany) { - - if (saveMany.getMany().hasJoinTable()) { - - // check if we can save the m2m intersection in this direction - // we only allow one direction based on first traversed basis - boolean saveIntersectionFromThisDirection = saveMany.isSaveIntersection(); - if (saveMany.isCascade()) { - saveAssocManyDetails(saveMany, false); - } - // 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(saveMany, saveMany.isDeleteMissingChildren()); - } else { - saveMany.resetModifyState(); - } - } else { - if (saveMany.isModifyListenMode()) { - // delete any removed beans via private owned. Needs to occur before - // a 'deleteMissingChildren' statement occurs - removeAssocManyPrivateOwned(saveMany); - } - if (saveMany.isCascade()) { - // potentially deletes 'missing children' for 'stateless update' - saveAssocManyDetails(saveMany, saveMany.isDeleteMissingChildren()); - } - } - } - - private void removeAssocManyPrivateOwned(SaveManyPropRequest saveMany) { - - 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) - if (details instanceof BeanCollection) { - - BeanCollection c = (BeanCollection) details; - Set modifyRemovals = c.getModifyRemovals(); - saveMany.modifyListenReset(c); - if (modifyRemovals != null && !modifyRemovals.isEmpty()) { - for (Object removedBean : modifyRemovals) { - if (removedBean instanceof EntityBean) { - EntityBean eb = (EntityBean) removedBean; - if (eb._ebean_getIntercept().isLoaded()) { - // only delete if the bean was loaded meaning that it is known to exist in the DB - deleteRequest(createPublishRequest(removedBean, saveMany.getTransaction(), PersistRequest.Type.DELETE, saveMany.getFlags())); - } - } - } - } - } - } - - /** - * Save the details from a OneToMany collection. - */ - private void saveAssocManyDetails(SaveManyPropRequest saveMany, boolean deleteMissingChildren) { - - saveMany.saveDetails(this, deleteMissingChildren); - } - - /** - * Save the additions and removals from a ManyToMany collection as inserts - * and deletes from the intersection table. - *

- * This is done via MapBeans. - *

- */ - private void saveAssocManyIntersection(SaveManyPropRequest saveManyPropRequest, boolean deleteMissingChildren) { - - BeanPropertyAssocMany prop = saveManyPropRequest.getMany(); - Object value = prop.getValue(saveManyPropRequest.getParentBean()); - if (value == null) { - return; - } - - SpiTransaction t = saveManyPropRequest.getTransaction(); - boolean vanillaCollection = !(value instanceof BeanCollection); - - if (vanillaCollection || deleteMissingChildren) { - // delete all intersection rows and then treat all - // beans in the collection as additions - deleteAssocManyIntersection(saveManyPropRequest.getParentBean(), prop, t, saveManyPropRequest.isPublish()); - } - - Collection deletions = null; - Collection additions; - - if (saveManyPropRequest.isInsertedParent() || vanillaCollection || deleteMissingChildren) { - // treat everything in the list/set/map as an intersection addition - if (value instanceof Map) { - additions = ((Map) value).values(); - } else if (value instanceof Collection) { - additions = (Collection) value; - } else { - String msg = "Unhandled ManyToMany type " + value.getClass().getName() + " for " + prop.getFullBeanName(); - throw new PersistenceException(msg); - } - if (!vanillaCollection) { - BeanCollection manyValue = (BeanCollection) value; - setListenMode(manyValue, prop); - manyValue.modifyReset(); - } - } else { - // BeanCollection so get the additions/deletions - BeanCollection manyValue = (BeanCollection) value; - if (setListenMode(manyValue, prop)) { - additions = manyValue.getActualDetails(); - } else { - additions = manyValue.getModifyAdditions(); - deletions = manyValue.getModifyRemovals(); - } - // reset so the changes are only processed once - manyValue.modifyReset(); - } - - t.depth(+1); - - if (additions != null && !additions.isEmpty()) { - // ensure any cascade batch has been flushed prior - // to inserting into the intersection table - t.flushBatch(); - - for (Object other : additions) { - EntityBean otherBean = (EntityBean) other; - // the object from the 'other' side of the ManyToMany - if (deletions != null && deletions.remove(otherBean)) { - String m = "Inserting and Deleting same object? " + otherBean; - if (t.isLogSummary()) { - t.logSummary(m); - } - logger.warn(m); - - } else { - if (!prop.hasImportedId(otherBean)) { - String msg = "ManyToMany bean " + otherBean + " does not have an Id value."; - throw new PersistenceException(msg); - - } else { - // build a intersection row for 'insert' - IntersectionRow intRow = prop.buildManyToManyMapBean(saveManyPropRequest.getParentBean(), otherBean, saveManyPropRequest.isPublish()); - SqlUpdate sqlInsert = intRow.createInsert(server); - executeSqlUpdate(sqlInsert, t); - } - } - } - } - if (deletions != null && !deletions.isEmpty()) { - // ensure any cascade batch has been flushed prior - // to inserting into the intersection table - t.flushBatch(); - - for (Object other : deletions) { - EntityBean otherDelete = (EntityBean) other; - // the object from the 'other' side of the ManyToMany - // build a intersection row for 'delete' - IntersectionRow intRow = prop.buildManyToManyMapBean(saveManyPropRequest.getParentBean(), otherDelete, saveManyPropRequest.isPublish()); - SqlUpdate sqlDelete = intRow.createDelete(server, false); - executeSqlUpdate(sqlDelete, t); - } - } - - // decrease the depth back to what it was - t.depth(-1); - } - - /** - * Check if we need to set the listen mode (on new collections persisted for the first time). - */ - private boolean setListenMode(BeanCollection manyValue, BeanPropertyAssocMany prop) { - ModifyListenMode mode = manyValue.getModifyListening(); - if (mode == null) { - // new collection persisted for the first time - manyValue.setModifyListening(prop.getModifyListenMode()); - return true; - } - return false; - } - - private void deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany many, Transaction t, boolean publish) { + void deleteAssocManyIntersection(EntityBean bean, BeanPropertyAssocMany many, Transaction t, boolean publish) { // delete all intersection rows for this bean IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish); @@ -1241,9 +1062,7 @@ public final class DefaultPersister implements Persister { BeanDescriptor desc = request.getBeanDescriptor(); // imported ones with save cascade - BeanPropertyAssocOne[] ones = desc.propertiesOneImportedSave(); - - for (BeanPropertyAssocOne prop : ones) { + for (BeanPropertyAssocOne prop : desc.propertiesOneImportedSave()) { // check for partial objects if (request.isLoadedProperty(prop)) { EntityBean detailBean = prop.getValueAsEntityBean(request.getEntityBean()); @@ -1277,8 +1096,7 @@ public final class DefaultPersister implements Persister { DeleteUnloadedForeignKeys fkeys = null; - BeanPropertyAssocOne[] ones = request.getBeanDescriptor().propertiesOneImportedDelete(); - for (BeanPropertyAssocOne one : ones) { + for (BeanPropertyAssocOne one : request.getBeanDescriptor().propertiesOneImportedDelete()) { if (!request.isLoadedProperty(one)) { // we have cascade Delete on a partially populated bean and // this property was not loaded (so we are going to have to fetch it) @@ -1299,8 +1117,7 @@ public final class DefaultPersister implements Persister { boolean softDelete = request.isSoftDelete(); - BeanPropertyAssocOne[] ones = request.getBeanDescriptor().propertiesOneImportedDelete(); - for (BeanPropertyAssocOne prop : ones) { + for (BeanPropertyAssocOne prop : request.getBeanDescriptor().propertiesOneImportedDelete()) { if (!softDelete || prop.isTargetSoftDelete()) { if (request.isLoadedProperty(prop)) { Object detailBean = prop.getValue(request.getEntityBean()); @@ -1355,7 +1172,7 @@ public final class DefaultPersister implements Persister { /** * Create the Persist Request Object additionally specifying the publish status. */ - private PersistRequestBean createPublishRequest(T bean, Transaction t, PersistRequest.Type type, int flags) { + PersistRequestBean createPublishRequest(T bean, Transaction t, PersistRequest.Type type, int flags) { return createRequestInternal(bean, t, type, Flags.unsetRecuse(flags)); } diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java new file mode 100644 index 000000000..61a080188 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java @@ -0,0 +1,37 @@ +package io.ebeaninternal.server.persist; + +import io.ebean.bean.EntityBean; +import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; + +/** + * Base for saving entity bean collections and element collections. + */ +abstract class SaveManyBase { + + final PersistRequestBean request; + final SpiEbeanServer server; + final boolean insertedParent; + final BeanPropertyAssocMany many; + final SpiTransaction transaction; + final EntityBean parentBean; + final Object value; + + SaveManyBase(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + this.request = request; + this.server = request.getServer(); + this.insertedParent = insertedParent; + this.many = many; + this.parentBean = parentBean; + this.transaction = request.getTransaction(); + this.value = many.getValue(parentBean); + } + + /** + * Save the collection. + */ + abstract void save(); + +} diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java new file mode 100644 index 000000000..cdd84d158 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -0,0 +1,381 @@ +package io.ebeaninternal.server.persist; + +import io.ebean.SqlUpdate; +import io.ebean.bean.BeanCollection; +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; +import io.ebeaninternal.server.core.PersistRequest; +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.IntersectionRow; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.PersistenceException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Saves the details for a OneToMany or ManyToMany relationship (entity beans). + */ +class SaveManyBeans extends SaveManyBase { + + private static final Logger log = LoggerFactory.getLogger(SaveManyBeans.class); + + private final boolean cascade; + private final boolean publish; + private final BeanDescriptor targetDescriptor; + private final boolean isMap; + private final boolean saveRecurseSkippable; + + private Collection collection; + private DefaultPersister persister; + private boolean deleteMissing; + private int sortOrder; + + SaveManyBeans(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request, DefaultPersister persister) { + super(insertedParent, many, parentBean, request); + this.persister = persister; + this.cascade = many.getCascadeInfo().isSave(); + this.publish = request.isPublish(); + this.targetDescriptor = many.getTargetDescriptor(); + this.isMap = many.getManyType().isMap(); + this.saveRecurseSkippable = many.isSaveRecurseSkippable(); + } + + @Override + 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); + } + // 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()); + } else { + resetModifyState(); + } + } else { + if (isModifyListenMode()) { + // delete any removed beans via private owned. Needs to occur before + // a 'deleteMissingChildren' statement occurs + removeAssocManyPrivateOwned(); + } + if (cascade) { + // potentially deletes 'missing children' for 'stateless update' + saveAssocManyDetails(request.isDeleteMissingChildren()); + } + } + } + + private boolean isSaveIntersection() { + return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName()); + } + + private boolean isModifyListenMode() { + return BeanCollection.ModifyListenMode.REMOVALS == many.getModifyListenMode(); + } + + private void modifyListenReset(BeanCollection c) { + if (insertedParent) { + // after insert set the modify listening mode for private owned etc + c.setModifyListening(many.getModifyListenMode()); + } + c.modifyReset(); + } + + private void resetModifyState() { + if (value instanceof BeanCollection) { + modifyListenReset((BeanCollection) value); + } + } + + /** + * Save the details from a OneToMany collection. + */ + private void saveAssocManyDetails(boolean deleteMissingChildren) { + + this.deleteMissing = deleteMissingChildren; + + // 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(); + } + } + + 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.hasJoinTable()) { + 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, request.getFlags()); + } + } + } + } + + /** + * 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; + } + + /** + * Save the additions and removals from a ManyToMany collection as inserts + * and deletes from the intersection table. + *

+ * This is done via MapBeans. + *

+ */ + private void saveAssocManyIntersection(boolean deleteMissingChildren) { + + if (value == null) { + return; + } + + //SpiTransaction t = saveManyPropRequest.getTransaction(); + boolean vanillaCollection = !(value instanceof BeanCollection); + + if (vanillaCollection || deleteMissingChildren) { + // delete all intersection rows and then treat all + // beans in the collection as additions + persister.deleteAssocManyIntersection(parentBean, many, transaction, publish); + } + + Collection deletions = null; + Collection additions; + + if (insertedParent || vanillaCollection || deleteMissingChildren) { + // treat everything in the list/set/map as an intersection addition + if (value instanceof Map) { + additions = ((Map) value).values(); + } else if (value instanceof Collection) { + additions = (Collection) value; + } else { + throw new PersistenceException("Unhandled ManyToMany type " + value.getClass().getName() + " for " + many.getFullBeanName()); + } + if (!vanillaCollection) { + BeanCollection manyValue = (BeanCollection) value; + setListenMode(manyValue, many); + manyValue.modifyReset(); + } + } else { + // BeanCollection so get the additions/deletions + BeanCollection manyValue = (BeanCollection) value; + if (setListenMode(manyValue, many)) { + additions = manyValue.getActualDetails(); + } else { + additions = manyValue.getModifyAdditions(); + deletions = manyValue.getModifyRemovals(); + } + // reset so the changes are only processed once + manyValue.modifyReset(); + } + + transaction.depth(+1); + + if (additions != null && !additions.isEmpty()) { + // ensure any cascade batch has been flushed prior + // to inserting into the intersection table + transaction.flushBatch(); + + for (Object other : additions) { + EntityBean otherBean = (EntityBean) other; + // the object from the 'other' side of the ManyToMany + if (deletions != null && deletions.remove(otherBean)) { + String m = "Inserting and Deleting same object? " + otherBean; + if (transaction.isLogSummary()) { + transaction.logSummary(m); + } + log.warn(m); + + } else { + if (!many.hasImportedId(otherBean)) { + String msg = "ManyToMany bean " + otherBean + " does not have an Id value."; + throw new PersistenceException(msg); + + } else { + // build a intersection row for 'insert' + IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish); + SqlUpdate sqlInsert = intRow.createInsert(server); + persister.executeSqlUpdate(sqlInsert, transaction); + } + } + } + } + if (deletions != null && !deletions.isEmpty()) { + // ensure any cascade batch has been flushed prior + // to inserting into the intersection table + transaction.flushBatch(); + + for (Object other : deletions) { + EntityBean otherDelete = (EntityBean) other; + // the object from the 'other' side of the ManyToMany + // build a intersection row for 'delete' + IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish); + SqlUpdate sqlDelete = intRow.createDelete(server, false); + persister.executeSqlUpdate(sqlDelete, transaction); + } + } + + // decrease the depth back to what it was + transaction.depth(-1); + } + + private void removeAssocManyPrivateOwned() { + + // check that the list is not null and if it is a BeanCollection + // check that is has been populated (don't trigger lazy loading) + if (value instanceof BeanCollection) { + + BeanCollection c = (BeanCollection) value; + Set modifyRemovals = c.getModifyRemovals(); + modifyListenReset(c); + if (modifyRemovals != null && !modifyRemovals.isEmpty()) { + for (Object removedBean : modifyRemovals) { + if (removedBean instanceof EntityBean) { + EntityBean eb = (EntityBean) removedBean; + if (eb._ebean_getIntercept().isLoaded()) { + // only delete if the bean was loaded meaning that it is known to exist in the DB + persister.deleteRequest(persister.createPublishRequest(removedBean, transaction, PersistRequest.Type.DELETE, request.getFlags())); + } + } + } + } + } + } + + /** + * Check if we need to set the listen mode (on new collections persisted for the first time). + */ + private boolean setListenMode(BeanCollection manyValue, BeanPropertyAssocMany prop) { + BeanCollection.ModifyListenMode mode = manyValue.getModifyListening(); + if (mode == null) { + // new collection persisted for the first time + manyValue.setModifyListening(prop.getModifyListenMode()); + return true; + } + return false; + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java deleted file mode 100644 index dbd643d2f..000000000 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManyPropRequest.java +++ /dev/null @@ -1,251 +0,0 @@ -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 { - - final PersistRequestBean request; - private final boolean insertedParent; - final BeanPropertyAssocMany many; - final EntityBean parentBean; - 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; - - Collection collection; - DefaultPersister persister; - private boolean deleteMissing; - private int sortOrder; - - SaveManyPropRequest(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { - this.request = 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; - } - - public int getFlags() { - return request.getFlags(); - } - - 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) { - - this.persister = persister; - this.deleteMissing = deleteMissing; - - // 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(); - } - } - - 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.hasJoinTable()) { - 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, request.getFlags()); - } - } - } - } - - /** - * 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/SaveManySimpleCollection.java b/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleCollection.java index 62faf73e3..49a4f09e9 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleCollection.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleCollection.java @@ -4,16 +4,27 @@ import io.ebean.SqlUpdate; import io.ebean.bean.EntityBean; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.deploy.BeanCollectionUtil; import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; -class SaveManySimpleCollection extends SaveManyPropRequest { +import java.util.Collection; + +/** + * Save details for a simple scalar element collection. + */ +class SaveManySimpleCollection extends SaveManyBase { SaveManySimpleCollection(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { super(insertedParent, many, parentBean, request); } @Override - void processDetails() { + void save() { + + Collection collection = BeanCollectionUtil.getActualEntries(value); + if (collection == null) { + return; + } Object parentId = request.getBeanId(); diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleMap.java b/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleMap.java new file mode 100644 index 000000000..84d57c222 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleMap.java @@ -0,0 +1,52 @@ +package io.ebeaninternal.server.persist; + +import io.ebean.SqlUpdate; +import io.ebean.bean.EntityBean; +import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.server.core.PersistRequestBean; +import io.ebeaninternal.server.deploy.BeanCollectionUtil; +import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; + +import java.util.Map; +import java.util.Set; + +/** + * Save details for a simple scalar map element collection. + */ +class SaveManySimpleMap extends SaveManyBase { + + SaveManySimpleMap(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + super(insertedParent, many, parentBean, request); + } + + @SuppressWarnings("unchecked") + @Override + void save() { + + Set> entries = (Set>)BeanCollectionUtil.getActualEntries(value); + if (entries == null) { + return; + } + + Object parentId = request.getBeanId(); + + SqlUpdate sqlDelete = many.deleteByParentId(parentId, null); + + SpiEbeanServer server = request.getServer(); + server.execute(sqlDelete, transaction); + + transaction.depth(+1); + + String insert = many.insertElementCollection(); + SqlUpdate sqlInsert = server.createSqlUpdate(insert); + + for (Map.Entry entry : entries) { + sqlInsert.setParameter(1, parentId); + sqlInsert.setParameter(2, entry.getKey()); + sqlInsert.setParameter(3, entry.getValue()); + server.execute(sqlInsert, transaction); + } + + transaction.depth(-1); + } +} diff --git a/src/test/java/org/tests/model/elementcollection/EcmPerson.java b/src/test/java/org/tests/model/elementcollection/EcmPerson.java new file mode 100644 index 000000000..8be26fa50 --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/EcmPerson.java @@ -0,0 +1,68 @@ +package org.tests.model.elementcollection; + +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.MapKeyColumn; +import javax.persistence.Version; +import java.util.LinkedHashMap; +import java.util.Map; + +@Entity +public class EcmPerson { + + @Id + long id; + + String name; + + @ElementCollection + @MapKeyColumn(name = "type", length = 4) + @Column(name = "number", length = 10) + Map phoneNumbers = new LinkedHashMap<>(); + + @Version + long version; + + public EcmPerson(String name) { + this.name = name; + } + + @Override + public String toString() { + return "person id:" + id + " name:" + name + " phs:" + phoneNumbers; + } + + 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 Map getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(Map phoneNumbers) { + this.phoneNumbers = phoneNumbers; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java new file mode 100644 index 000000000..83bbb7561 --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java @@ -0,0 +1,63 @@ +package org.tests.model.elementcollection; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestElementCollectionBasicMap extends BaseTestCase { + + @Test + public void test() { + + EcmPerson person = new EcmPerson("Fiona021"); + person.getPhoneNumbers().put("home","021 1234"); + person.getPhoneNumbers().put("work","021 4321"); + Ebean.save(person); + + + EcmPerson person1 = new EcmPerson("Fiona09"); + person1.getPhoneNumbers().put("home","09 1234"); + person1.getPhoneNumbers().put("work","09 4321"); + person1.getPhoneNumbers().put("mob","09 9876"); + Ebean.save(person1); + + List found = + Ebean.find(EcmPerson.class).where() + .startsWith("name", "Fiona0") + .order().asc("id") + .findList(); + + Map phoneNumbers0 = found.get(0).getPhoneNumbers(); + Map phoneNumbers1 = found.get(1).getPhoneNumbers(); + phoneNumbers0.size(); + + assertThat(phoneNumbers0).containsValues("021 1234", "021 4321"); + assertThat(phoneNumbers0.get("work")).isEqualTo("021 4321"); + assertThat(phoneNumbers1).containsValues("09 1234", "09 4321", "09 9876"); + assertThat(phoneNumbers1.get("mob")).isEqualTo("09 9876"); + + + List found2 = + Ebean.find(EcmPerson.class) + .fetch("phoneNumbers") + .where() + .startsWith("name", "Fiona0") + .order().asc("id") + .findList(); + + assertThat(found2).hasSize(2); + EcmPerson foundFirst = found2.get(0); + String asJson = Ebean.json().toJson(foundFirst); + + EcmPerson fromJson = Ebean.json().toBean(EcmPerson.class, asJson); + + assertThat(fromJson.getPhoneNumbers()).containsValues("021 1234", "021 4321"); + assertThat(fromJson.getPhoneNumbers().get("home")).isEqualTo("021 1234"); + + } +}