From 1b8b05790eb4fb36ffcff3e916892ebd5c7fdadd Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Thu, 14 Apr 2016 11:56:31 +1200 Subject: [PATCH] #650 - Refactor: Optimise L2 cache entry for serialisation --- .../server/cache/CachedBeanData.java | 6 + .../deploy/BeanDescriptorCacheHelp.java | 12 +- .../server/deploy/BeanProperty.java | 35 ++++- .../server/deploy/BeanPropertyAssocMany.java | 9 ++ .../server/deploy/BeanPropertyAssocOne.java | 18 ++- .../ebeaninternal/server/type/ScalarType.java | 6 + .../server/type/ScalarTypeBase.java | 6 + .../server/type/ScalarTypeByte.java | 5 + .../server/type/ScalarTypeBytesBase.java | 5 + .../server/type/ScalarTypeBytesEncrypted.java | 5 + .../type/ScalarTypeEncryptedWrapper.java | 5 + .../type/ScalarTypeEnumWithMapping.java | 5 + .../server/type/ScalarTypeFile.java | 5 + .../server/type/ScalarTypeWrapper.java | 5 + .../cache/CachedBeanDataFromBeanTest.java | 4 +- .../cache/CachedBeanDataSerializeTest.java | 137 ++++++++++++++++++ 16 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java index 8b35cd2b5..4088a9faa 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java @@ -131,4 +131,10 @@ public class CachedBeanData implements Externalizable { return data.get(propertyName); } + /** + * Return all the property data. + */ + public Map getData() { + return data; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index de3abf2f3..383e15761 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -634,11 +634,13 @@ final class BeanDescriptorCacheHelp { for (int i = 0; i < dirtyProperties.length; i++) { if (dirtyProperties[i]) { BeanProperty property = desc.propertiesIndex[i]; - Object val = property.getCacheDataValue(bean); - changes.put(property.getName(), val); - if (property.isNaturalKey()) { - updateNaturalKey = true; - changeSet.addNaturalKeyPut(desc, id, val); + if (property.isCacheDataInclude()) { + Object val = property.getCacheDataValue(bean); + changes.put(property.getName(), val); + if (property.isNaturalKey()) { + updateNaturalKey = true; + changeSet.addNaturalKeyPut(desc, id, val); + } } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index 777de4de3..d097ddc97 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -733,11 +733,42 @@ public class BeanProperty implements ElPropertyValue, Property { return prefix + name + " on [" + descriptor + "] arg[" + value + "] type[" + beanType + "] threw error"; } - public Object getCacheDataValue(EntityBean bean) { - return getValue(bean); + /** + * Return true if this property should be included in the cache bean data. + */ + public boolean isCacheDataInclude() { + return true; } + /** + * Return the value for this property which we hold in the L2 cache entry. + *

+ * This uses format() where possible to store the value as a string and this + * is done to make any resulting Java object serialisation content smaller as + * strings get special treatment. + *

+ */ + public Object getCacheDataValue(EntityBean bean) { + Object value = getValue(bean); + if (value == null || scalarType.isBinaryType()) { + return value; + } else { + // convert to string as an optimisation for java object serialisation + return scalarType.format(value); + } + } + + /** + * Read the value for this property from L2 cache entry and set it to the bean. + *

+ * This uses parse() as per the comment in getCacheDataValue(). + *

+ */ public void setCacheDataValue(EntityBean bean, Object cacheData) { + if (cacheData instanceof String) { + // parse back from string to support optimisation of java object serialisation + cacheData = scalarType.parse((String)cacheData); + } setValue(bean, cacheData); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 01f42f5ea..fc364ac21 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -326,6 +326,15 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { return server.findIds(q, t); } + /** + * Exclude many properties from bean cache data. + */ + @Override + public boolean isCacheDataInclude() { + // this would change for DB Array type support + return false; + } + /** * Add the loaded current bean to its associated parent. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index a310f43a1..d299cb57e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -385,6 +385,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { return getPropertyType(); } + @Override public Object getCacheDataValue(EntityBean bean) { Object ap = getValue(bean); if (ap == null) { @@ -392,22 +393,23 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { } if (embedded) { return targetDescriptor.cacheEmbeddedBeanExtract((EntityBean) ap); - } else { - return targetDescriptor.getId((EntityBean) ap); + return targetDescriptor.getIdProperty().getCacheDataValue((EntityBean) ap); } } @Override public void setCacheDataValue(EntityBean bean, Object cacheData) { - if (cacheData != null) { + if (cacheData == null) { + setValue(bean, null); + } else { if (embedded) { - EntityBean embeddedBean = targetDescriptor.cacheEmbeddedBeanLoad((CachedBeanData) cacheData); - setValue(bean, embeddedBean); - + setValue(bean, targetDescriptor.cacheEmbeddedBeanLoad((CachedBeanData) cacheData)); } else { - T ref = targetDescriptor.createReference(Boolean.FALSE, cacheData); - setValue(bean, ref); + if (cacheData instanceof String) { + cacheData = targetDescriptor.getIdProperty().scalarType.parse((String)cacheData); + } + setValue(bean, targetDescriptor.createReference(Boolean.FALSE, cacheData)); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarType.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarType.java index da6d657b0..2c31e0490 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarType.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarType.java @@ -35,6 +35,12 @@ import java.sql.SQLException; */ public interface ScalarType extends StringParser, StringFormatter, ScalarDataReader { + /** + * Return true if this is a binary type and can not support parse() and format() from/to string. + * This allows Ebean to optimise marshalling types to string. + */ + boolean isBinaryType(); + /** * Return true if this is a mutable scalar type (like hstore). */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBase.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBase.java index b31731aef..31cba8a38 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBase.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBase.java @@ -22,6 +22,12 @@ public abstract class ScalarTypeBase implements ScalarType { throw new RuntimeException("not supported"); } + @Override + public boolean isBinaryType() { + // override for binary/byte based types + return false; + } + /** * Default implementation of mutable false. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeByte.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeByte.java index 3704778b0..80c575c21 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeByte.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeByte.java @@ -23,6 +23,11 @@ public class ScalarTypeByte extends ScalarTypeBase { super(Byte.class, true, Types.TINYINT); } + @Override + public boolean isBinaryType() { + return true; + } + public void bind(DataBind b, Byte value) throws SQLException { if (value == null) { b.setNull(Types.TINYINT); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesBase.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesBase.java index 787e81a1d..1381d8305 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesBase.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesBase.java @@ -20,6 +20,11 @@ public abstract class ScalarTypeBytesBase extends ScalarTypeBase { super(byte[].class, jdbcNative, jdbcType); } + @Override + public boolean isBinaryType() { + return true; + } + public byte[] convertToBytes(Object value) { return (byte[]) value; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java index 7cca1c9da..6ad818ebb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeBytesEncrypted.java @@ -29,6 +29,11 @@ public class ScalarTypeBytesEncrypted implements ScalarType { throw new RuntimeException("not supported"); } + @Override + public boolean isBinaryType() { + return true; + } + @Override public boolean isMutable() { return false; diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java index 997919141..22c0a667f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEncryptedWrapper.java @@ -28,6 +28,11 @@ public class ScalarTypeEncryptedWrapper implements ScalarType { throw new RuntimeException("not supported"); } + @Override + public boolean isBinaryType() { + return wrapped.isBinaryType(); + } + @Override public boolean isMutable() { return wrapped.isMutable(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEnumWithMapping.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEnumWithMapping.java index 41db357ca..9db8ca3fd 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEnumWithMapping.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeEnumWithMapping.java @@ -29,6 +29,11 @@ public class ScalarTypeEnumWithMapping extends ScalarTypeEnumStandard.EnumBase i throw new RuntimeException("not supported"); } + @Override + public boolean isBinaryType() { + return false; + } + /** * Return the IN values for DB constraint construction. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java index 15a3323be..64c69efb8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeFile.java @@ -54,6 +54,11 @@ public class ScalarTypeFile extends ScalarTypeBase { this.bufferSize = bufferSize; } + @Override + public boolean isBinaryType() { + return true; + } + private InputStream getInputStream(File value) throws IOException { FileInputStream fi = new FileInputStream(value); return new BufferedInputStream(fi, bufferSize); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeWrapper.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeWrapper.java index aaa1a957e..e0f11c2e5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeWrapper.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeWrapper.java @@ -47,6 +47,11 @@ public class ScalarTypeWrapper implements ScalarType { return scalarType.asVersion(unwrapValue); } + @Override + public boolean isBinaryType() { + return scalarType.isBinaryType(); + } + @Override public boolean isMutable() { return scalarType.isMutable(); diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBeanTest.java b/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBeanTest.java index 7ef133f39..fe4282482 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBeanTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataFromBeanTest.java @@ -32,9 +32,9 @@ public class CachedBeanDataFromBeanTest extends BaseTestCase { CachedBeanData cacheData = CachedBeanDataFromBean.extract(desc, (EntityBean) customer); - assertEquals(cacheData.getData("id"), Integer.valueOf(42)); + assertEquals(cacheData.getData("id"), "42"); assertEquals(cacheData.getData("name"), "Rob"); - assertEquals(cacheData.getData("billingAddress"), Short.valueOf("12")); + assertEquals(cacheData.getData("billingAddress"), "12"); } diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java b/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java new file mode 100644 index 000000000..151824317 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataSerializeTest.java @@ -0,0 +1,137 @@ +package com.avaje.ebeaninternal.server.cache; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.bean.EntityBean; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.ResetBasicData; +import com.avaje.tests.model.basic.TBytesOnly; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +public class CachedBeanDataSerializeTest extends BaseTestCase { + + @Test + public void write() throws IOException, ClassNotFoundException { + + Map map = new LinkedHashMap(); + map.put("name", "rob"); + map.put("some", "thing"); + map.put("whenCreated", ""+System.currentTimeMillis()); + + long version = System.currentTimeMillis(); + CachedBeanData write = new CachedBeanData(null, "C", map, version); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(os); + + write.writeExternal(oos); + oos.flush(); + oos.close(); + + byte[] bytes = os.toByteArray(); + + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(is); + + CachedBeanData read = new CachedBeanData(); + read.readExternal(ois); + + assertEquals(read.getVersion(), write.getVersion()); + assertEquals(read.getWhenCreated(), write.getWhenCreated()); + assertEquals(read.getDiscValue(), write.getDiscValue()); + assertEquals(read.getData(), write.getData()); + } + + + @Test + public void fullBean() throws IOException, ClassNotFoundException { + + ResetBasicData.reset(); + + List customers = Ebean.find(Customer.class) + .orderBy().asc("id") + .setMaxRows(1).findList(); + + Customer customer = customers.get(0); + + BeanDescriptor desc = getBeanDescriptor(Customer.class); + CachedBeanData extract = CachedBeanDataFromBean.extract(desc, (EntityBean)customer); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + writeToStream(extract, os); + + byte[] bytes = os.toByteArray(); + + CachedBeanData read = readFromStream(bytes); + + assertEquals(read.getData(), extract.getData()); + + Customer loadCustomer = new Customer(); + CachedBeanDataToBean.load(desc, (EntityBean)loadCustomer, read); + + assertEquals(loadCustomer.getVersion(), customer.getVersion()); + assertEquals(loadCustomer.getId(), customer.getId()); + assertEquals(loadCustomer.getName(), customer.getName()); + assertEquals(loadCustomer.getStatus(), customer.getStatus()); + } + + @Test + public void beanWithByteArray() throws IOException, ClassNotFoundException { + + String stringContent = "ThisIsSome"; + + TBytesOnly bean = new TBytesOnly(); + bean.setId(42); + bean.setContent(stringContent.getBytes("UTF8")); + + BeanDescriptor desc = getBeanDescriptor(TBytesOnly.class); + CachedBeanData extract = CachedBeanDataFromBean.extract(desc, (EntityBean)bean); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + writeToStream(extract, os); + byte[] bytes = os.toByteArray(); + + CachedBeanData read = readFromStream(bytes); + byte[] extraContent = (byte[])extract.getData("content"); + + assertEquals(stringContent, new String(extraContent)); + assertTrue(Arrays.equals(bean.getContent(), extraContent)); + + TBytesOnly loadBean= new TBytesOnly(); + CachedBeanDataToBean.load(desc, (EntityBean)loadBean, read); + + assertEquals(loadBean.getId(), bean.getId()); + assertTrue(Arrays.equals(loadBean.getContent(), bean.getContent())); + } + + private CachedBeanData readFromStream(byte[] bytes) throws IOException, ClassNotFoundException { + + ByteArrayInputStream is = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(is); + return (CachedBeanData)ois.readObject(); + } + + private void writeToStream(CachedBeanData extract, ByteArrayOutputStream os) throws IOException { + + ObjectOutputStream oos = new ObjectOutputStream(os); + oos.writeObject(extract); + oos.flush(); + oos.close(); + } + +} \ No newline at end of file