From 57870aaa058966f73efb3078edaeccac75ad6841 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 28 Mar 2018 13:22:46 +1300 Subject: [PATCH] #115 - Mapping - Add support for @ElementCollection enhancement Add embedded bean support --- .../java/io/ebean/bean/BeanCollection.java | 5 + .../ebean/common/AbstractBeanCollection.java | 3 +- .../server/deploy/BeanCollectionUtil.java | 10 ++ .../server/deploy/BeanDescriptor.java | 31 +++- .../server/deploy/BeanDescriptorElement.java | 59 +++---- .../deploy/BeanDescriptorElementEmbedded.java | 79 ++++++++++ .../BeanDescriptorElementEmbeddedMap.java | 84 ++++++++++ .../deploy/BeanDescriptorElementMap.java | 97 ------------ .../deploy/BeanDescriptorElementScalar.java | 56 +++++++ .../BeanDescriptorElementScalarMap.java | 88 +++++++++++ .../server/deploy/BeanDescriptorJsonHelp.java | 2 +- .../server/deploy/BeanDescriptorManager.java | 16 +- .../server/deploy/BeanPropertyAssocMany.java | 38 ++++- .../deploy/BeanPropertySimpleCollection.java | 32 +--- .../meta/DeployBeanPropertyAssocMany.java | 11 ++ .../deploy/parse/AnnotationAssocManys.java | 42 +++-- .../server/persist/DefaultPersister.java | 4 +- .../server/persist/SaveManyBase.java | 14 ++ .../server/persist/SaveManyBeans.java | 14 -- ...on.java => SaveManyElementCollection.java} | 19 ++- ...java => SaveManyElementCollectionMap.java} | 23 +-- .../server/type/TypeReflectHelper.java | 7 + .../model/elementcollection/EcPhone.java | 52 ++++++ .../model/elementcollection/EcblPerson.java | 67 ++++++++ .../model/elementcollection/EcbmPerson.java | 68 ++++++++ .../TestElementCollectionBasic.java | 89 ++++++++++- .../TestElementCollectionBasicMap.java | 103 ++++++++++-- .../TestElementCollectionBasicSet.java | 88 ++++++++++- .../TestElementCollectionEmbeddedList.java | 148 ++++++++++++++++++ .../TestElementCollectionEmbeddedMap.java | 141 +++++++++++++++++ 30 files changed, 1247 insertions(+), 243 deletions(-) create mode 100644 src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java create mode 100644 src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java delete mode 100644 src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementMap.java create mode 100644 src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java create mode 100644 src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java rename src/main/java/io/ebeaninternal/server/persist/{SaveManySimpleCollection.java => SaveManyElementCollection.java} (64%) rename src/main/java/io/ebeaninternal/server/persist/{SaveManySimpleMap.java => SaveManyElementCollectionMap.java} (57%) create mode 100644 src/test/java/org/tests/model/elementcollection/EcPhone.java create mode 100644 src/test/java/org/tests/model/elementcollection/EcblPerson.java create mode 100644 src/test/java/org/tests/model/elementcollection/EcbmPerson.java create mode 100644 src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java create mode 100644 src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java diff --git a/src/main/java/io/ebean/bean/BeanCollection.java b/src/main/java/io/ebean/bean/BeanCollection.java index 8c3b95b08..3ec5163e3 100644 --- a/src/main/java/io/ebean/bean/BeanCollection.java +++ b/src/main/java/io/ebean/bean/BeanCollection.java @@ -72,6 +72,11 @@ public interface BeanCollection extends Serializable { */ boolean isSkipSave(); + /** + * Return true if the collection holds modifications. + */ + boolean holdsModifications(); + /** * Return the bean that owns this collection. */ diff --git a/src/main/java/io/ebean/common/AbstractBeanCollection.java b/src/main/java/io/ebean/common/AbstractBeanCollection.java index e4641814a..ca5493d2a 100644 --- a/src/main/java/io/ebean/common/AbstractBeanCollection.java +++ b/src/main/java/io/ebean/common/AbstractBeanCollection.java @@ -207,7 +207,8 @@ abstract class AbstractBeanCollection implements BeanCollection { /** * Return true if there are underlying additions or removals. */ - boolean holdsModifications() { + @Override + public boolean holdsModifications() { return modifyHolder != null && modifyHolder.hasModifications(); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java b/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java index 40171b0da..0b8d46dcf 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java @@ -11,6 +11,16 @@ import java.util.Map; */ public class BeanCollectionUtil { + /** + * Return true if this is a bean collection and not considered dirty. + */ + public static boolean isModified(Object collection) { + if ((collection instanceof BeanCollection)) { + return ((BeanCollection) collection).holdsModifications(); + } + return true; + } + /** * Return the details of the collection or map taking care to avoid * unnecessary fetching of the data. diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 5bbd28ee8..620b6e1fd 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -235,7 +235,7 @@ public class BeanDescriptor implements BeanType, STreeType { /** * The type of bean this describes. */ - private final Class beanType; + final Class beanType; protected final Class rootBeanType; @@ -428,8 +428,8 @@ public class BeanDescriptor implements BeanType, STreeType { this.name = InternString.intern(deploy.getName()); this.baseTableAlias = "t0"; this.fullName = InternString.intern(deploy.getFullName()); - this.locationById = ProfileLocation.createAt(fullName+".byId"); - this.locationAll = ProfileLocation.createAt(fullName+".all"); + this.locationById = ProfileLocation.createAt(fullName + ".byId"); + this.locationAll = ProfileLocation.createAt(fullName + ".all"); this.profileBeanId = deploy.getProfileId(); this.beanType = deploy.getBeanType(); this.rootBeanType = PersistenceContextUtil.root(beanType); @@ -776,7 +776,7 @@ public class BeanDescriptor implements BeanType, STreeType { for (BeanProperty prop : propertiesNonTransient) { if (prop.isUnique()) { - propertiesUnique.add(new BeanProperty[] { prop }); + propertiesUnique.add(new BeanProperty[]{prop}); } } // convert unique columns to properties @@ -860,6 +860,16 @@ public class BeanDescriptor implements BeanType, STreeType { } } + /** + * Bind all the property values to the SqlUpdate. + */ + public void bindElementValue(SqlUpdate insert, Object value) { + EntityBean bean = (EntityBean) value; + for (BeanProperty property : propertiesBaseScalar) { + insert.setNextParameter(property.getValue(bean)); + } + } + /** * Return the ReadAuditLogger for logging read audit events. */ @@ -1420,7 +1430,7 @@ public class BeanDescriptor implements BeanType, STreeType { @SuppressWarnings("unchecked") public void cacheBeanPutAll(Collection beans) { if (!beans.isEmpty()) { - cacheHelp.beanPutAll((Collection)beans); + cacheHelp.beanPutAll((Collection) beans); } } @@ -1879,12 +1889,19 @@ public class BeanDescriptor implements BeanType, STreeType { } /** - * Creates a new entitybean without invoking {@link BeanPostConstructListener#postCreate(Object)} + * Creates a new entity bean without invoking {@link BeanPostConstructListener#postCreate(Object)} */ public EntityBean createEntityBean() { return createEntityBean(false); } + /** + * Create an entity bean for JSON marshalling (which differs for the element collection case). + */ + public EntityBean createEntityBeanForJson() { + return createEntityBean(); + } + /** * Create a reference bean based on the id. */ @@ -2471,7 +2488,7 @@ public class BeanDescriptor implements BeanType, STreeType { /** * Return a property that is part of the SQL tree. - * + *

* The property can be a dynamic formula or a well known bean property. */ @Override diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java index a1c23603a..0790ba10e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElement.java @@ -1,34 +1,35 @@ package io.ebeaninternal.server.deploy; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import io.ebean.PersistenceIOException; 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.Arrays; /** * Bean descriptor used with ElementCollection (where we don't have a mapped type/class). + *

+ * This is somewhat a BeanDescriptor created 'on the fly' for a specific element collection property + * with a unidirectional property and mapping etc specific to the property (and not the type if embedded). */ -class BeanDescriptorElement extends BeanDescriptor { +abstract class BeanDescriptorElement extends BeanDescriptor { - private final ScalarType scalarType; - private final ElementHelp elementHelp; + final ElementHelp elementHelp; BeanDescriptorElement(BeanDescriptorMap owner, DeployBeanDescriptor deploy, ElementHelp elementHelp) { super(owner, deploy); this.elementHelp = elementHelp; + } - BeanProperty[] props = propertiesNonTransient(); - if (props.length == 1) { - this.scalarType = props[0].getScalarType(); - } else { - this.scalarType = null; + /** + * Find and return the first base scalar type (and we expect only 1). + */ + ScalarType firstBaseScalarType() { + BeanProperty[] props = propertiesBaseScalar(); + if (props.length != 1) { + throw new IllegalStateException("Expecting 1 property for element scalar but got " + Arrays.toString(props)); } + return props[0].getScalarType(); } @Override @@ -36,35 +37,13 @@ class BeanDescriptorElement extends BeanDescriptor { return true; } + /** + * Our entity beans used are somewhat fake ones (ElementEntityBean) such that we hold the unidirectional property + * value (foreign key) and the actual element collection value (scalar or embedded plus map key). + */ @Override protected EntityBean createPrototypeEntityBean(Class beanType) { return new ElementEntityBean(properties); } - @Override - public void jsonWriteElement(SpiJsonWriter ctx, Object element) { - try { - scalarType.jsonWrite(ctx.gen(), element); - } catch (IOException e) { - throw new PersistenceIOException(e); - } - } - - @Override - public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException { - - JsonParser parser = readJson.getParser(); - ElementCollector add = elementHelp.createCollector(); - do { - JsonToken token = parser.nextToken(); - if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { - break; - } - Object element = scalarType.jsonRead(parser); - add.addElement(element); - } while (true); - - return add.collection(); - } - } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java new file mode 100644 index 000000000..cb17a9492 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java @@ -0,0 +1,79 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.PersistenceIOException; +import io.ebean.SqlUpdate; +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 java.io.IOException; + +/** + * Bean descriptor used with element collection of list/set of embeddable. + */ +class BeanDescriptorElementEmbedded extends BeanDescriptorElement { + + private final BeanPropertyAssocOne embeddedProperty; + + private final EntityBean prototype; + + private BeanDescriptor targetDescriptor; + + BeanDescriptorElementEmbedded(BeanDescriptorMap owner, DeployBeanDescriptor deploy, ElementHelp elementHelp) { + super(owner, deploy, elementHelp); + try { + this.prototype = (EntityBean) beanType.newInstance(); + } catch (Exception e) { + throw new IllegalStateException("Unable to create entity bean prototype for "+beanType); + } + BeanPropertyAssocOne[] embedded = propertiesEmbedded(); + if (embedded.length == 1) { + embeddedProperty = embedded[0]; + } else { + embeddedProperty = null; + } + } + + @Override + public void initialiseOther(BeanDescriptorInitContext initContext) { + super.initialiseOther(initContext); + this.targetDescriptor = embeddedProperty.getTargetDescriptor(); + } + + @Override + public EntityBean createEntityBeanForJson() { + return (EntityBean)prototype._ebean_newInstance(); + } + + public void bindElementValue(SqlUpdate insert, Object value) { + targetDescriptor.bindElementValue(insert, value); + } + + @Override + public void jsonWriteElement(SpiJsonWriter ctx, Object element) { + writeJsonElement(ctx, element); + } + + @Override + public T jsonRead(ReadJson jsonRead, String path) throws IOException { + return readJsonElement(jsonRead, path); + } + + @SuppressWarnings("unchecked") + T readJsonElement(ReadJson jsonRead, String path) throws IOException { + return (T)targetDescriptor.jsonRead(jsonRead, path); + } + + void writeJsonElement(SpiJsonWriter ctx, Object element) { + try { + if (element == null) { + ctx.writeNull(); + } else { + targetDescriptor.jsonWrite(ctx, (EntityBean)element); + } + } catch (IOException e) { + throw new PersistenceIOException(e); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java new file mode 100644 index 000000000..49b85d699 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java @@ -0,0 +1,84 @@ +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; + +/** + * Descriptor for element collection using Map with the value holding an embedded bean. + *

+ * The expected limitation is that the key is a scalar type. + */ +class BeanDescriptorElementEmbeddedMap extends BeanDescriptorElementEmbedded { + + private final ScalarType scalarTypeKey; + + private final boolean stringKey; + + BeanDescriptorElementEmbeddedMap(BeanDescriptorMap owner, DeployBeanDescriptor deploy, ElementHelp elementHelp) { + super(owner, deploy, elementHelp); + this.scalarTypeKey = firstBaseScalarType(); + this.stringKey = String.class.equals(scalarTypeKey.getType()); + } + + @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); + writeJsonElement(ctx, entry.getValue()); + } else { + ctx.writeFieldName("key"); + scalarTypeKey.jsonWrite(ctx.gen(), entry.getKey()); + ctx.writeFieldName("value"); + writeJsonElement(ctx, 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 = readJsonElement(readJson, null); + add.addKeyValue(key, val); + + } else { + parser.nextFieldName(); + Object key = scalarTypeKey.jsonRead(parser); + + parser.nextFieldName(); + Object val = readJsonElement(readJson, null); + 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/BeanDescriptorElementMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementMap.java deleted file mode 100644 index 3319ba8fc..000000000 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementMap.java +++ /dev/null @@ -1,97 +0,0 @@ -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/BeanDescriptorElementScalar.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java new file mode 100644 index 000000000..1b7ac70ea --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalar.java @@ -0,0 +1,56 @@ +package io.ebeaninternal.server.deploy; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.ebean.PersistenceIOException; +import io.ebean.SqlUpdate; +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; + +/** + * Bean descriptor used with element collection mapped to a list or set of scalar values. + */ +class BeanDescriptorElementScalar extends BeanDescriptorElement { + + private final ScalarType scalarType; + + BeanDescriptorElementScalar(BeanDescriptorMap owner, DeployBeanDescriptor deploy, ElementHelp elementHelp) { + super(owner, deploy, elementHelp); + this.scalarType = firstBaseScalarType(); + } + + public void bindElementValue(SqlUpdate insert, Object value) { + insert.setNextParameter(value); + } + + @Override + public void jsonWriteElement(SpiJsonWriter ctx, Object element) { + try { + scalarType.jsonWrite(ctx.gen(), element); + } catch (IOException e) { + throw new PersistenceIOException(e); + } + } + + @Override + public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException { + + JsonParser parser = readJson.getParser(); + ElementCollector add = elementHelp.createCollector(); + do { + JsonToken token = parser.nextToken(); + if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { + break; + } + add.addElement(scalarType.jsonRead(parser)); + } while (true); + + return add.collection(); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java new file mode 100644 index 000000000..9eb87ba0b --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementScalarMap.java @@ -0,0 +1,88 @@ +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.Arrays; +import java.util.Map; + +/** + * Bean descriptor used with element collection mapped to Map where key and value are scalar types. + */ +class BeanDescriptorElementScalarMap extends BeanDescriptorElement { + + private final ScalarType scalarTypeKey; + private final ScalarType scalarTypeVal; + private final boolean stringKey; + + BeanDescriptorElementScalarMap(BeanDescriptorMap owner, DeployBeanDescriptor deploy, ElementHelp elementHelp) { + super(owner, deploy, elementHelp); + BeanProperty[] props = propertiesNonTransient(); + if (props.length != 2) { + throw new IllegalStateException("Expecting 2 properties for key and value but got " + Arrays.toString(props)); + } + this.scalarTypeKey = props[0].getScalarType(); + this.scalarTypeVal = props[1].getScalarType(); + this.stringKey = String.class.equals(scalarTypeKey.getType()); + } + + @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); + scalarTypeVal.jsonWrite(ctx.gen(), entry.getValue()); + } else { + ctx.writeFieldName("key"); + scalarTypeKey.jsonWrite(ctx.gen(), entry.getKey()); + ctx.writeFieldName("value"); + scalarTypeVal.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 = scalarTypeVal.jsonRead(parser); + add.addKeyValue(key, val); + + } else { + parser.nextFieldName(); + Object key = scalarTypeKey.jsonRead(parser); + + parser.nextFieldName(); + Object val = scalarTypeVal.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/BeanDescriptorJsonHelp.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java index ad9d640a3..8d90a3af5 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java @@ -118,7 +118,7 @@ public class BeanDescriptorJsonHelp { protected T jsonReadObject(ReadJson readJson, String path) throws IOException { - EntityBean bean = desc.createEntityBean(); + EntityBean bean = desc.createEntityBeanForJson(); return jsonReadProperties(readJson, bean, path); } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 0e6814365..3c95f9515 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -637,7 +637,7 @@ public class BeanDescriptorManager implements BeanDescriptorMap { /** * Return a BeanTable for an ElementCollection. */ - public BeanTable getCollectionBeanTable(String fullTableName, Class targetType) { + public BeanTable createCollectionBeanTable(String fullTableName, Class targetType) { return new BeanTable(this, fullTableName, targetType); } @@ -1637,13 +1637,21 @@ public class BeanDescriptorManager implements BeanDescriptorMap { /** * Create a BeanDescriptor for an ElementCollection target. */ - public BeanDescriptor createElementDescriptor(DeployBeanDescriptor elementDescriptor, ManyType manyType) { + public BeanDescriptor createElementDescriptor(DeployBeanDescriptor elementDescriptor, ManyType manyType, boolean scalar) { ElementHelp elementHelp = elementHelper(manyType); if (manyType.isMap()) { - return new BeanDescriptorElementMap<>(this, elementDescriptor, elementHelp); + if (scalar) { + return new BeanDescriptorElementScalarMap<>(this, elementDescriptor, elementHelp); + } else { + return new BeanDescriptorElementEmbeddedMap<>(this, elementDescriptor, elementHelp); + } + } + if (scalar) { + return new BeanDescriptorElementScalar<>(this, elementDescriptor, elementHelp); + } else { + return new BeanDescriptorElementEmbedded<>(this, elementDescriptor, elementHelp); } - return new BeanDescriptorElement<>(this, elementDescriptor, elementHelp); } private ElementHelp elementHelper(ManyType manyType) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 0072949a6..2ef769116 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -70,6 +70,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST private final boolean elementCollection; + /** + * Descriptor for the 'target' when the property maps to an element collection. + */ + BeanDescriptor elementDescriptor; + /** * Order by used when fetch joining the associated many. */ @@ -116,6 +121,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST this.hasOrderColumn = deploy.hasOrderColumn(); this.manyToMany = deploy.isManyToMany(); this.elementCollection = deploy.isElementCollection(); + this.elementDescriptor = deploy.getElementDescriptor(); this.manyType = deploy.getManyType(); this.mapKey = deploy.getMapKey(); this.fetchOrderBy = deploy.getFetchOrderBy(); @@ -136,6 +142,19 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST public void initialise(BeanDescriptorInitContext initContext) { super.initialise(initContext); initialiseAssocMany(); + if (elementCollection) { + // initialise all non-id properties (we don't have an Id property) + elementDescriptor.initialiseOther(initContext); + } + } + + @Override + void initialiseTargetDescriptor(BeanDescriptorInitContext initContext) { + if (elementCollection) { + targetDescriptor = elementDescriptor; + } else { + targetDescriptor = descriptor.getBeanDescriptor(targetType); + } } private void initialiseAssocMany() { @@ -170,7 +189,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST } String targetTable() { - return targetDescriptor.getBaseTable(); + return beanTable.getBaseTable(); } /** @@ -883,18 +902,22 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST } public void jsonWriteMapEntry(SpiJsonWriter ctx, Map.Entry entry) throws IOException { - // Writing as json array rather than object ... - targetDescriptor.jsonWrite(ctx, (EntityBean) entry.getValue()); + elementDescriptor.jsonWriteMapEntry(ctx, entry); } public void jsonWriteElementValue(SpiJsonWriter ctx, Object element) { - throw new IllegalStateException("Unexpected - expect Element override"); + elementDescriptor.jsonWriteElement(ctx, element); } /** * Read the collection (JSON Array) containing entity beans. */ public Object jsonReadCollection(ReadJson readJson, EntityBean parentBean) throws IOException { + + if (elementDescriptor != null && manyType.isMap()) { + return elementDescriptor.jsonReadCollection(readJson, parentBean); + } + BeanCollection collection = createEmpty(parentBean); BeanCollectionAdd add = getBeanCollectionAdd(collection, null); do { @@ -913,4 +936,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST return collection; } + + /** + * Bind all the property values to the SqlUpdate. + */ + public void bindElementValue(SqlUpdate insert, Object value) { + targetDescriptor.bindElementValue(insert, value); + } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java index 6b9d1602e..f00b401cd 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java @@ -1,47 +1,21 @@ package io.ebeaninternal.server.deploy; +import io.ebean.SqlUpdate; 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 { - private BeanDescriptor elementDescriptor; - public BeanPropertySimpleCollection(BeanDescriptor descriptor, DeployBeanPropertySimpleCollection deploy) { super(descriptor, deploy); - this.elementDescriptor = deploy.getElementDescriptor(); } @Override - public void initialise(BeanDescriptorInitContext initContext) { - super.initialise(initContext); - if (isElementCollection()) { - // initialise all non-id properties (we don't have an Id property) - elementDescriptor.initialiseOther(initContext); - } - } - - void initialiseTargetDescriptor(BeanDescriptorInitContext initContext) { - if (isElementCollection()) { - targetDescriptor = elementDescriptor; - } else { - targetDescriptor = descriptor.getBeanDescriptor(targetType); - } - } - - @Override - 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); + public void bindElementValue(SqlUpdate insert, Object value) { + insert.setNextParameter(value); } @Override 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 4e6d7d7d8..6e0394273 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java @@ -4,6 +4,9 @@ import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.ManyType; import io.ebeaninternal.server.deploy.TableJoin; +import io.ebeaninternal.server.type.TypeReflectHelper; + +import java.lang.reflect.Type; /** * Property mapped to a List Set or Map. @@ -179,6 +182,14 @@ public class DeployBeanPropertyAssocMany extends DeployBeanPropertyAssoc { return fetchOrderBy; } + /** + * Return the type of the map key (valid only when this property is a Map). + */ + public Class getMapKeyType() { + Type genericType = getField().getGenericType(); + return TypeReflectHelper.getMapKeyType(genericType); + } + /** * Return the default mapKey when returning a Map. */ 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 7c98d20c4..24f3c96aa 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocManys.java @@ -15,6 +15,7 @@ import io.ebeaninternal.server.deploy.BeanTable; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; +import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocOne; import io.ebeaninternal.server.deploy.meta.DeployOrderColumn; import io.ebeaninternal.server.deploy.meta.DeployTableJoin; import io.ebeaninternal.server.deploy.meta.DeployTableJoinColumn; @@ -204,7 +205,7 @@ class AnnotationAssocManys extends AnnotationParser { } } - BeanTable beanTable = factory.getCollectionBeanTable(fullTableName, prop.getTargetType()); + BeanTable beanTable = factory.createCollectionBeanTable(fullTableName, prop.getTargetType()); prop.setBeanTable(beanTable); Class elementType = prop.getTargetType(); @@ -212,8 +213,6 @@ class AnnotationAssocManys extends AnnotationParser { DeployBeanDescriptor elementDescriptor = factory.createDeployDescriptor(elementType); elementDescriptor.setBaseTable(new TableName(fullTableName), readConfig.getAsOfViewSuffix(), readConfig.getVersionsBetweenSuffix()); - ScalarType scalarType = util.getTypeManager().getScalarType(elementType); - int sortOrder = 0; if (!prop.getManyType().isMap()) { elementDescriptor.setProperties(new String[]{"value"}); @@ -225,27 +224,46 @@ class AnnotationAssocManys extends AnnotationParser { dbKeyColumn = mapKeyColumn.name(); } - DeployBeanProperty keyProp = new DeployBeanProperty(elementDescriptor, elementType, scalarType, null); + ScalarType keyScalarType = util.getTypeManager().getScalarType(prop.getMapKeyType()); + + DeployBeanProperty keyProp = new DeployBeanProperty(elementDescriptor, elementType, keyScalarType, null); setElementProperty(keyProp, "key", dbKeyColumn, sortOrder++); elementDescriptor.addBeanProperty(keyProp); if (mapKeyColumn != null) { keyProp.setDbLength(mapKeyColumn.length()); keyProp.setDbScale(mapKeyColumn.scale()); + keyProp.setUnique(mapKeyColumn.unique()); } } - 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()); + ScalarType valueScalarType = util.getTypeManager().getScalarType(elementType); + + boolean scalar = true; + if (valueScalarType == null) { + // embedded value type + scalar = false; + DeployBeanPropertyAssocOne valueProp = new DeployBeanPropertyAssocOne<>(elementDescriptor, elementType); + valueProp.setName("value"); + valueProp.setEmbedded(); + valueProp.setElementProperty(); + valueProp.setSortOrder(sortOrder++); + elementDescriptor.addBeanProperty(valueProp); + + } else { + // scalar value type + DeployBeanProperty valueProp = new DeployBeanProperty(elementDescriptor, elementType, valueScalarType, null); + setElementProperty(valueProp, "value", prop.getDbColumn(), sortOrder++); + if (column != null) { + valueProp.setDbLength(column.length()); + valueProp.setDbScale(column.scale()); + } + elementDescriptor.addBeanProperty(valueProp); } - elementDescriptor.addBeanProperty(valueProp); elementDescriptor.setName(prop.getFullBeanName()); factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin()); - prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType())); + prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType(), scalar)); } private void setElementProperty(DeployBeanProperty elementProp, String name, String dbColumn, int sortOrder) { @@ -307,7 +325,7 @@ class AnnotationAssocManys extends AnnotationParser { * Return the full table name */ private String getFullTableName(CollectionTable collectionTable) { - if (collectionTable == null) { + if (collectionTable == null || collectionTable.name().isEmpty()) { return null; } StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index f02969461..b16a0ced8 100644 --- a/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -906,10 +906,10 @@ public final class DefaultPersister implements Persister { return new SaveManyBeans(insertedParent, many, parentBean, request, this); } else if (many.getManyType().isMap()) { - return new SaveManySimpleMap(insertedParent, many, parentBean, request); + return new SaveManyElementCollectionMap(insertedParent, many, parentBean, request); } else { - return new SaveManySimpleCollection(insertedParent, many, parentBean, request); + return new SaveManyElementCollection(insertedParent, many, parentBean, request); } } diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java index 61a080188..9f892c5e8 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBase.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.persist; +import io.ebean.bean.BeanCollection; import io.ebean.bean.EntityBean; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiTransaction; @@ -34,4 +35,17 @@ abstract class SaveManyBase { */ abstract void save(); + void resetModifyState() { + if (value instanceof BeanCollection) { + modifyListenReset((BeanCollection) value); + } + } + + void modifyListenReset(BeanCollection c) { + if (insertedParent) { + // after insert set the modify listening mode for private owned etc + c.setModifyListening(many.getModifyListenMode()); + } + c.modifyReset(); + } } diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index cdd84d158..063862967 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -89,20 +89,6 @@ class SaveManyBeans extends SaveManyBase { 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. */ diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleCollection.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyElementCollection.java similarity index 64% rename from src/main/java/io/ebeaninternal/server/persist/SaveManySimpleCollection.java rename to src/main/java/io/ebeaninternal/server/persist/SaveManyElementCollection.java index 49a4f09e9..1867421fa 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleCollection.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyElementCollection.java @@ -12,9 +12,9 @@ import java.util.Collection; /** * Save details for a simple scalar element collection. */ -class SaveManySimpleCollection extends SaveManyBase { +class SaveManyElementCollection extends SaveManyBase { - SaveManySimpleCollection(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + SaveManyElementCollection(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { super(insertedParent, many, parentBean, request); } @@ -22,16 +22,18 @@ class SaveManySimpleCollection extends SaveManyBase { void save() { Collection collection = BeanCollectionUtil.getActualEntries(value); - if (collection == null) { + if (collection == null || !BeanCollectionUtil.isModified(value)) { return; } Object parentId = request.getBeanId(); - SqlUpdate sqlDelete = many.deleteByParentId(parentId, null); - SpiEbeanServer server = request.getServer(); - server.execute(sqlDelete, transaction); + + if (!insertedParent) { + SqlUpdate sqlDelete = many.deleteByParentId(parentId, null); + server.execute(sqlDelete, transaction); + } transaction.depth(+1); @@ -40,11 +42,12 @@ class SaveManySimpleCollection extends SaveManyBase { for (Object value : collection) { - sqlInsert.setParameter(1, parentId); - sqlInsert.setParameter(2, value); + sqlInsert.setNextParameter(parentId); + many.bindElementValue(sqlInsert, value); server.execute(sqlInsert, transaction); } transaction.depth(-1); + resetModifyState(); } } diff --git a/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleMap.java b/src/main/java/io/ebeaninternal/server/persist/SaveManyElementCollectionMap.java similarity index 57% rename from src/main/java/io/ebeaninternal/server/persist/SaveManySimpleMap.java rename to src/main/java/io/ebeaninternal/server/persist/SaveManyElementCollectionMap.java index 84d57c222..3ba94d781 100644 --- a/src/main/java/io/ebeaninternal/server/persist/SaveManySimpleMap.java +++ b/src/main/java/io/ebeaninternal/server/persist/SaveManyElementCollectionMap.java @@ -13,9 +13,9 @@ import java.util.Set; /** * Save details for a simple scalar map element collection. */ -class SaveManySimpleMap extends SaveManyBase { +class SaveManyElementCollectionMap extends SaveManyBase { - SaveManySimpleMap(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { + SaveManyElementCollectionMap(boolean insertedParent, BeanPropertyAssocMany many, EntityBean parentBean, PersistRequestBean request) { super(insertedParent, many, parentBean, request); } @@ -23,17 +23,19 @@ class SaveManySimpleMap extends SaveManyBase { @Override void save() { - Set> entries = (Set>)BeanCollectionUtil.getActualEntries(value); - if (entries == null) { + Set> entries = (Set>) BeanCollectionUtil.getActualEntries(value); + if (entries == null || !BeanCollectionUtil.isModified(value)) { return; } Object parentId = request.getBeanId(); - SqlUpdate sqlDelete = many.deleteByParentId(parentId, null); - SpiEbeanServer server = request.getServer(); - server.execute(sqlDelete, transaction); + + if (!insertedParent) { + SqlUpdate sqlDelete = many.deleteByParentId(parentId, null); + server.execute(sqlDelete, transaction); + } transaction.depth(+1); @@ -41,12 +43,13 @@ class SaveManySimpleMap extends SaveManyBase { SqlUpdate sqlInsert = server.createSqlUpdate(insert); for (Map.Entry entry : entries) { - sqlInsert.setParameter(1, parentId); - sqlInsert.setParameter(2, entry.getKey()); - sqlInsert.setParameter(3, entry.getValue()); + sqlInsert.setNextParameter(parentId); + sqlInsert.setNextParameter(entry.getKey()); + many.bindElementValue(sqlInsert, entry.getValue()); server.execute(sqlInsert, transaction); } transaction.depth(-1); + resetModifyState(); } } diff --git a/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java b/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java index 392c0829b..663d18f00 100644 --- a/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java +++ b/src/main/java/io/ebeaninternal/server/type/TypeReflectHelper.java @@ -30,6 +30,13 @@ public class TypeReflectHelper { } } + /** + * Return the type of the map key. + */ + public static Class getMapKeyType(Type genericType) { + return getClass(getValueType(genericType)); + } + /** * Return the value type of a collection type (list, set, map values). */ diff --git a/src/test/java/org/tests/model/elementcollection/EcPhone.java b/src/test/java/org/tests/model/elementcollection/EcPhone.java new file mode 100644 index 000000000..24f71559f --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/EcPhone.java @@ -0,0 +1,52 @@ +package org.tests.model.elementcollection; + +import javax.persistence.Embeddable; +import javax.validation.constraints.Size; + +@Embeddable +public class EcPhone { + + @Size(max = 2) + String countryCode; + + @Size(max = 6) + String area; + + @Size(max = 20) + String number; + + public EcPhone(String countryCode, String area, String number) { + this.countryCode = countryCode; + this.area = area; + this.number = number; + } + + @Override + public String toString() { + return countryCode + "-" + area + "-" + number; + } + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + + public String getArea() { + return area; + } + + public void setArea(String area) { + this.area = area; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } +} diff --git a/src/test/java/org/tests/model/elementcollection/EcblPerson.java b/src/test/java/org/tests/model/elementcollection/EcblPerson.java new file mode 100644 index 000000000..982071e13 --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/EcblPerson.java @@ -0,0 +1,67 @@ +package org.tests.model.elementcollection; + +import javax.persistence.CollectionTable; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Version; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class EcblPerson { + + @Id + long id; + + String name; + + @ElementCollection + @CollectionTable(joinColumns = @JoinColumn(name = "person_id")) + List phoneNumbers = new ArrayList<>(); + + @Version + long version; + + public EcblPerson(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 List getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(List 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/EcbmPerson.java b/src/test/java/org/tests/model/elementcollection/EcbmPerson.java new file mode 100644 index 000000000..7a236bb85 --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/EcbmPerson.java @@ -0,0 +1,68 @@ +package org.tests.model.elementcollection; + +import javax.persistence.CollectionTable; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Version; +import java.util.LinkedHashMap; +import java.util.Map; + + +@Entity +public class EcbmPerson { + + @Id + long id; + + String name; + + @ElementCollection + @CollectionTable(joinColumns = @JoinColumn(name = "person_id")) + Map phoneNumbers = new LinkedHashMap<>(); + + @Version + long version; + + public EcbmPerson(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/TestElementCollectionBasic.java b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasic.java index 54ef094cb..b55b3dba2 100644 --- a/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasic.java +++ b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasic.java @@ -2,6 +2,7 @@ package org.tests.model.elementcollection; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import org.ebeantest.LoggedSqlCollector; import org.junit.Test; import java.util.List; @@ -13,17 +14,25 @@ public class TestElementCollectionBasic extends BaseTestCase { @Test public void test() { + LoggedSqlCollector.start(); + EcPerson person = new EcPerson("Fiona021"); person.getPhoneNumbers().add("021 1234"); person.getPhoneNumbers().add("021 4321"); Ebean.save(person); + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into ec_person"); + assertThat(sql.get(1)).contains("insert into ec_person_phone"); EcPerson person1 = new EcPerson("Fiona09"); person1.getPhoneNumbers().add("09 1234"); person1.getPhoneNumbers().add("09 4321"); Ebean.save(person1); + LoggedSqlCollector.current(); + List found = Ebean.find(EcPerson.class).where() .startsWith("name", "Fiona0") @@ -37,6 +46,10 @@ public class TestElementCollectionBasic extends BaseTestCase { assertThat(phoneNumbers0).containsExactly("021 1234", "021 4321"); assertThat(phoneNumbers1).containsExactly("09 1234", "09 4321"); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version from ec_person t0 where"); + assertThat(trimSql(sql.get(1))).contains("select t0.owner_id, t0.phone from ec_person_phone t0 where"); List found2 = Ebean.find(EcPerson.class) @@ -47,11 +60,83 @@ public class TestElementCollectionBasic extends BaseTestCase { .findList(); assertThat(found2).hasSize(2); + + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version, t1.phone from ec_person t0 left join ec_person_phone t1"); + EcPerson foundFirst = found2.get(0); + + jsonToFrom(foundFirst); + updateBasic(foundFirst); + + LoggedSqlCollector.stop(); + } + + private void updateBasic(EcPerson bean) { + + bean.setName("Fiona021-mod-0"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("update ec_person"); + + updateBoth(bean); + } + + private void updateBoth(EcPerson bean) { + + bean.setName("Fiona021-mod-both"); + bean.getPhoneNumbers().add("01-22123"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update ec_person set name=?, version=? where id=? and version=?"); + assertThat(sql.get(1)).contains("delete from ec_person_phone where owner_id=?"); + assertThat(sql.get(2)).contains("insert into ec_person_phone (owner_id,phone) values (?,?)"); + + updateNothing(bean); + } + + private void updateNothing(EcPerson bean) { + + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(0); + + updateOnlyCollection(bean); + } + + private void updateOnlyCollection(EcPerson bean) { + + bean.getPhoneNumbers().add("01-4321"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ec_person_phone where owner_id=?"); + assertThat(sql.get(1)).contains("insert into ec_person_phone (owner_id,phone) values (?,?)"); + + delete(bean); + } + + private void delete(EcPerson bean) { + + Ebean.delete(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ec_person_phone where owner_id = ?"); + assertThat(sql.get(1)).contains("delete from ec_person where id=? and version=?"); + } + + private void jsonToFrom(EcPerson foundFirst) { + String asJson = Ebean.json().toJson(foundFirst); - EcPerson fromJson = Ebean.json().toBean(EcPerson.class, asJson); - assertThat(fromJson.getPhoneNumbers()).containsAll(foundFirst.getPhoneNumbers()); } } diff --git a/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java index 83bbb7561..f03bf1933 100644 --- a/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java +++ b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicMap.java @@ -2,6 +2,7 @@ package org.tests.model.elementcollection; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import org.ebeantest.LoggedSqlCollector; import org.junit.Test; import java.util.List; @@ -14,26 +15,34 @@ public class TestElementCollectionBasicMap extends BaseTestCase { @Test public void test() { + LoggedSqlCollector.start(); + EcmPerson person = new EcmPerson("Fiona021"); - person.getPhoneNumbers().put("home","021 1234"); - person.getPhoneNumbers().put("work","021 4321"); + person.getPhoneNumbers().put("home", "021 1234"); + person.getPhoneNumbers().put("work", "021 4321"); Ebean.save(person); + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into ecm_person"); + assertThat(sql.get(1)).contains("insert into ecm_person_phone"); EcmPerson person1 = new EcmPerson("Fiona09"); - person1.getPhoneNumbers().put("home","09 1234"); - person1.getPhoneNumbers().put("work","09 4321"); - person1.getPhoneNumbers().put("mob","09 9876"); + person1.getPhoneNumbers().put("home", "09 1234"); + person1.getPhoneNumbers().put("work", "09 4321"); + person1.getPhoneNumbers().put("mob", "09 9876"); Ebean.save(person1); + LoggedSqlCollector.current(); + 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(); + Map phoneNumbers0 = found.get(0).getPhoneNumbers(); + Map phoneNumbers1 = found.get(1).getPhoneNumbers(); phoneNumbers0.size(); assertThat(phoneNumbers0).containsValues("021 1234", "021 4321"); @@ -41,6 +50,10 @@ public class TestElementCollectionBasicMap extends BaseTestCase { assertThat(phoneNumbers1).containsValues("09 1234", "09 4321", "09 9876"); assertThat(phoneNumbers1.get("mob")).isEqualTo("09 9876"); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version from ecm_person t0 where"); + assertThat(trimSql(sql.get(1))).contains("select t0.ecm_person_id, t0.type, t0.number from ecm_person_phone_numbers t0 where"); List found2 = Ebean.find(EcmPerson.class) @@ -51,13 +64,83 @@ public class TestElementCollectionBasicMap extends BaseTestCase { .findList(); assertThat(found2).hasSize(2); - EcmPerson foundFirst = found2.get(0); - String asJson = Ebean.json().toJson(foundFirst); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version, t1.type, t1.number from ecm_person t0 left join ecm_person_phone_numbers t1"); + + EcmPerson foundFirst = found2.get(0); + jsonToFrom(foundFirst); + updateBasic(foundFirst); + + LoggedSqlCollector.stop(); + } + + private void updateBasic(EcmPerson bean) { + + bean.setName("Fiona021-mod-0"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("update ecm_person"); + + updateBoth(bean); + } + + private void updateBoth(EcmPerson bean) { + + bean.setName("Fiona021-mod-both"); + bean.getPhoneNumbers().put("one", "01-22123"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update ecm_person set name=?, version=? where id=? and version=?"); + assertThat(sql.get(1)).contains("delete from ecm_person_phone_numbers where ecm_person_id=?"); + assertThat(sql.get(2)).contains("insert into ecm_person_phone_numbers (ecm_person_id,type,number) values (?,?,?)"); + + updateNothing(bean); + } + + private void updateNothing(EcmPerson bean) { + + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(0); + + updateOnlyCollection(bean); + } + + private void updateOnlyCollection(EcmPerson bean) { + + bean.getPhoneNumbers().put("two", "01-4321"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecm_person_phone_numbers where ecm_person_id=?"); + assertThat(sql.get(1)).contains("insert into ecm_person_phone_numbers (ecm_person_id,type,number) values (?,?,?)"); + + delete(bean); + } + + private void delete(EcmPerson bean) { + + Ebean.delete(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecm_person_phone_numbers where ecm_person_id = ?"); + assertThat(sql.get(1)).contains("delete from ecm_person where id=? and version=?"); + } + + private void jsonToFrom(EcmPerson foundFirst) { + 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"); - } } diff --git a/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java index 946e273f7..2d3124351 100644 --- a/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java +++ b/src/test/java/org/tests/model/elementcollection/TestElementCollectionBasicSet.java @@ -2,6 +2,7 @@ package org.tests.model.elementcollection; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import org.ebeantest.LoggedSqlCollector; import org.junit.Test; import java.util.List; @@ -14,11 +15,17 @@ public class TestElementCollectionBasicSet extends BaseTestCase { @Test public void test() { + LoggedSqlCollector.start(); + EcsPerson person = new EcsPerson("Fiona021"); person.getPhoneNumbers().add("021 1234"); person.getPhoneNumbers().add("021 4321"); Ebean.save(person); + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into ecs_person"); + assertThat(sql.get(1)).contains("insert into ecs_person_phone"); EcsPerson person1 = new EcsPerson("Fiona09"); person1.getPhoneNumbers().add("09 1234"); @@ -26,6 +33,8 @@ public class TestElementCollectionBasicSet extends BaseTestCase { person1.getPhoneNumbers().add("09 9876"); Ebean.save(person1); + LoggedSqlCollector.current(); + List found = Ebean.find(EcsPerson.class).where() .startsWith("name", "Fiona0") @@ -39,6 +48,10 @@ public class TestElementCollectionBasicSet extends BaseTestCase { assertThat(phoneNumbers0).containsExactly("021 1234", "021 4321"); assertThat(phoneNumbers1).containsExactly("09 1234", "09 4321", "09 9876"); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version from ecs_person t0 where"); + assertThat(trimSql(sql.get(1))).contains("select t0.ecs_person_id, t0.phone from ecs_person_phone t0 where"); List found2 = Ebean.find(EcsPerson.class) @@ -49,11 +62,82 @@ public class TestElementCollectionBasicSet extends BaseTestCase { .findList(); assertThat(found2).hasSize(2); + + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version, t1.phone from ecs_person t0 left join ecs_person_phone t1"); + EcsPerson foundFirst = found2.get(0); + jsonToFrom(foundFirst); + + updateBasic(foundFirst); + + LoggedSqlCollector.stop(); + } + + private void updateBasic(EcsPerson bean) { + + bean.setName("Fiona021-mod-0"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("update ecs_person"); + + updateBoth(bean); + } + + private void updateBoth(EcsPerson bean) { + + bean.setName("Fiona021-mod-both"); + bean.getPhoneNumbers().add("01-22123"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update ecs_person set name=?, version=? where id=? and version=?"); + assertThat(sql.get(1)).contains("delete from ecs_person_phone where ecs_person_id=?"); + assertThat(sql.get(2)).contains("insert into ecs_person_phone (ecs_person_id,phone) values (?,?)"); + + updateNothing(bean); + } + + private void updateNothing(EcsPerson bean) { + + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(0); + + updateOnlyCollection(bean); + } + + private void updateOnlyCollection(EcsPerson bean) { + + bean.getPhoneNumbers().add("01-4321"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecs_person_phone where ecs_person_id=?"); + assertThat(sql.get(1)).contains("insert into ecs_person_phone (ecs_person_id,phone) values (?,?)"); + + delete(bean); + } + + private void delete(EcsPerson bean) { + + Ebean.delete(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecs_person_phone where ecs_person_id = ?"); + assertThat(sql.get(1)).contains("delete from ecs_person where id=? and version=?"); + } + + private void jsonToFrom(EcsPerson foundFirst) { String asJson = Ebean.json().toJson(foundFirst); - EcsPerson fromJson = Ebean.json().toBean(EcsPerson.class, asJson); - assertThat(fromJson.getPhoneNumbers()).containsAll(foundFirst.getPhoneNumbers()); } } diff --git a/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java b/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java new file mode 100644 index 000000000..8b86e6477 --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedList.java @@ -0,0 +1,148 @@ +package org.tests.model.elementcollection; + +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 TestElementCollectionEmbeddedList extends BaseTestCase { + + @Test + public void test() { + + LoggedSqlCollector.start(); + + EcblPerson person = new EcblPerson("Fiona64021"); + person.getPhoneNumbers().add(new EcPhone("64", "021","1234")); + person.getPhoneNumbers().add(new EcPhone("64","021","4321")); + Ebean.save(person); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into ecbl_person"); + assertThat(sql.get(1)).contains("insert into ecbl_person_phone_numbers"); + + EcblPerson person1 = new EcblPerson("Fiona6409"); + person1.getPhoneNumbers().add(new EcPhone("61","09","1234")); + person1.getPhoneNumbers().add(new EcPhone("64","09","4321")); + Ebean.save(person1); + + LoggedSqlCollector.current(); + + List found = + Ebean.find(EcblPerson.class).where() + .startsWith("name", "Fiona640") + .order().asc("id") + .findList(); + + List phoneNumbers0 = found.get(0).getPhoneNumbers(); + List phoneNumbers1 = found.get(1).getPhoneNumbers(); + phoneNumbers0.size(); + + assertThat(phoneNumbers0.toString()).contains("64-021-1234", "64-021-4321"); + assertThat(phoneNumbers1.toString()).contains("61-09-1234", "64-09-4321"); + + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(trimSql(sql.get(0))).contains("select t0.id, t0.name, t0.version from ecbl_person"); + assertThat(trimSql(sql.get(1))).contains("select t0.person_id, t0.country_code, t0.area, t0.number from ecbl_person_phone_numbers"); + + List found2 = + Ebean.find(EcblPerson.class) + .fetch("phoneNumbers") + .where() + .startsWith("name", "Fiona640") + .order().asc("id") + .findList(); + + assertThat(found2).hasSize(2); + + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + String trimmedSql = trimSql(sql.get(0)); + assertThat(trimmedSql).contains("select t0.id, t0.name, t0.version, t1.country_code, t1.area, t1.number from ecbl_person t0 left join ecbl_person_phone_numbers t1"); + + + EcblPerson foundFirst = found2.get(0); + jsonToFrom(foundFirst); + + updateBasic(foundFirst); + + LoggedSqlCollector.stop(); + } + + private void updateBasic(EcblPerson bean) { + + bean.setName("Fiona64-mod-0"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("update ecbl_person"); + + updateBoth(bean); + } + + private void updateBoth(EcblPerson bean) { + + bean.setName("Fiona64-mod-both"); + bean.getPhoneNumbers().add(new EcPhone("01", "234", "123")); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update ecbl_person set name=?, version=? where id=? and version=?"); + assertThat(sql.get(1)).contains("delete from ecbl_person_phone_numbers where person_id=?"); + assertThat(sql.get(2)).contains("insert into ecbl_person_phone_numbers (person_id,country_code,area,number) values (?,?,?,?)"); + + updateNothing(bean); + } + + private void updateNothing(EcblPerson bean) { + + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(0); + + updateOnlyCollection(bean); + } + + private void updateOnlyCollection(EcblPerson bean) { + + bean.getPhoneNumbers().add(new EcPhone("01", "12", "4321")); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecbl_person_phone_numbers where person_id=?"); + assertThat(sql.get(1)).contains("insert into ecbl_person_phone_numbers (person_id,country_code,area,number) values (?,?,?,?)"); + + delete(bean); + } + + private void delete(EcblPerson bean) { + + Ebean.delete(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecbl_person_phone_numbers where person_id = ?"); + assertThat(sql.get(1)).contains("delete from ecbl_person where id=? and version=?"); + } + + private void jsonToFrom(EcblPerson foundFirst) { + + String asJson = Ebean.json().toJson(foundFirst); + + EcblPerson fromJson = Ebean.json().toBean(EcblPerson.class, asJson); + + String phoneString = fromJson.getPhoneNumbers().toString(); + assertThat(phoneString).contains("64-021-1234"); + assertThat(phoneString).contains("64-021-4321"); + } +} diff --git a/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java b/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java new file mode 100644 index 000000000..ca342342c --- /dev/null +++ b/src/test/java/org/tests/model/elementcollection/TestElementCollectionEmbeddedMap.java @@ -0,0 +1,141 @@ +package org.tests.model.elementcollection; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestElementCollectionEmbeddedMap extends BaseTestCase { + + @Test + public void test() { + + LoggedSqlCollector.start(); + + EcbmPerson person = new EcbmPerson("Fiona64021"); + person.getPhoneNumbers().put("home", new EcPhone("64", "021","1234")); + person.getPhoneNumbers().put("work", new EcPhone("64","021","4321")); + Ebean.save(person); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("insert into ecbm_person"); + assertThat(sql.get(1)).contains("insert into ecbm_person_phone_numbers"); + + EcbmPerson person1 = new EcbmPerson("Fiona6409"); + person1.getPhoneNumbers().put("home",new EcPhone("64","09","1234")); + person1.getPhoneNumbers().put("mob",new EcPhone("61","09","4321")); + + Ebean.save(person1); + + LoggedSqlCollector.current(); + + List found = + Ebean.find(EcbmPerson.class).where() + .startsWith("name", "Fiona640") + .order().asc("id") + .findList(); + + Map phoneNumbers0 = found.get(0).getPhoneNumbers(); + Map phoneNumbers1 = found.get(1).getPhoneNumbers(); + phoneNumbers0.size(); + + assertThat(phoneNumbers0.toString()).contains("64-021-1234", "64-021-4321"); + assertThat(phoneNumbers1.toString()).contains("64-09-1234", "61-09-4321"); + + List found2 = + Ebean.find(EcbmPerson.class) + .fetch("phoneNumbers") + .where() + .startsWith("name", "Fiona640") + .order().asc("id") + .findList(); + + assertThat(found2).hasSize(2); + EcbmPerson foundFirst = found2.get(0); + + LoggedSqlCollector.current(); + + jsonToFrom(foundFirst); + + updateBasic(foundFirst); + + LoggedSqlCollector.stop(); + } + + private void updateBasic(EcbmPerson bean) { + + bean.setName("Fiona64-mod-0"); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("update ecbm_person"); + + updateBoth(bean); + } + + private void updateBoth(EcbmPerson bean) { + + bean.setName("Fiona64-mod-both"); + bean.getPhoneNumbers().put("more", new EcPhone("01", "234", "123")); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("update ecbm_person set name=?, version=? where id=? and version=?"); + assertThat(sql.get(1)).contains("delete from ecbm_person_phone_numbers where person_id=?"); + assertThat(sql.get(2)).contains("insert into ecbm_person_phone_numbers (person_id,key,country_code,area,number)"); + + updateNothing(bean); + } + + private void updateNothing(EcbmPerson bean) { + + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(0); + + updateOnlyCollection(bean); + } + + private void updateOnlyCollection(EcbmPerson bean) { + + bean.getPhoneNumbers().put("other", new EcPhone("01", "12", "4321")); + Ebean.save(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecbm_person_phone_numbers where person_id=?"); + assertThat(sql.get(1)).contains("insert into ecbm_person_phone_numbers (person_id,key,country_code,area,number) values (?,?,?,?,?)"); + + delete(bean); + } + + private void delete(EcbmPerson bean) { + + Ebean.delete(bean); + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("delete from ecbm_person_phone_numbers where person_id = ?"); + assertThat(sql.get(1)).contains("delete from ecbm_person where id=? and version=?"); + } + + private void jsonToFrom(EcbmPerson foundFirst) { + String asJson = Ebean.json().toJson(foundFirst); + + EcbmPerson fromJson = Ebean.json().toBean(EcbmPerson.class, asJson); + + String phoneString = fromJson.getPhoneNumbers().toString(); + assertThat(phoneString).contains("64-021-1234"); + assertThat(phoneString).contains("64-021-4321"); + } + +}