From 6c262f1df549d07298431941bc08c71b9b2da6e8 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 23 Jul 2021 16:50:24 +1200 Subject: [PATCH 01/26] JSON bean dirty detection via MD5 of JSON string content - MD5 of json content stored on EntityBeanIntercept for dirty detection - Only convert to JSON once (at dirty detection time). Store this json content on EntityBeanIntercept to later push to ScalarTypeJsonObjectMapper for bind Should consider alternative to extend BeanProperty rather than have these if blocks. --- .../io/ebean/bean/EntityBeanIntercept.java | 33 +++++++++ .../java/io/ebean/core/type/DataBinder.java | 11 +++ .../java/io/ebean/core/type/DataReader.java | 10 +++ .../java/io/ebean/core/type/ScalarType.java | 8 ++ .../server/deploy/BeanDescriptor.java | 4 +- .../server/deploy/BeanProperty.java | 47 ++++++++---- .../server/persist/dml/DmlHandler.java | 5 ++ .../dmlbind/BindablePropertyJsonInsert.java | 42 +++++++++++ .../dmlbind/BindablePropertyJsonUpdate.java | 35 +++++++++ .../persist/dmlbind/BindableRequest.java | 5 ++ .../persist/dmlbind/FactoryProperty.java | 7 ++ .../server/query/SqlBeanLoad.java | 11 ++- .../ebeaninternal/server/type/DataBind.java | 11 +++ .../server/type/RsetDataReader.java | 15 +++- .../type/ScalarTypeJsonObjectMapper.java | 52 ++++++++++++- .../org/tests/json/TestDbJson_Jackson3.java | 3 +- .../java/org/tests/json/TestDbJson_List.java | 12 ++- .../org/tests/model/json/EBasicPlain.java | 54 ++++++++++++++ .../model/json/TestJacksonPlainBean.java | 73 +++++++++++++++++++ 19 files changed, 404 insertions(+), 34 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java create mode 100644 ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java create mode 100644 ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 28a088ce6..59a2450bd 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -91,6 +91,17 @@ public final class EntityBeanIntercept implements Serializable { private Object ownerId; private int sortOrder; + /** + * Holds MD5 hash of json loaded jackson beans. + */ + private String[] mutableHash; + + /** + * Holds json content determined at point of dirty check. + * Stored here on dirty check such that we only convert to json once. + */ + private String[] mutableContent; + /** * Create a intercept with a given entity. */ @@ -1138,4 +1149,26 @@ public final class EntityBeanIntercept implements Serializable { } return ret; } + + public String mutableHash(int propertyIndex) { + return mutableHash == null ? null : mutableHash[propertyIndex]; + } + + public void mutableHash(int propertyIndex, String content) { + if (mutableHash == null) { + mutableHash = new String[flags.length]; + } + mutableHash[propertyIndex] = content; + } + + public String mutableContent(int propertyIndex) { + return mutableContent == null ? null : mutableContent[propertyIndex]; + } + + public void mutableContent(int propertyIndex, String content) { + if (mutableContent == null) { + mutableContent = new String[flags.length]; + } + mutableContent[propertyIndex] = content; + } } diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/DataBinder.java b/ebean-core-type/src/main/java/io/ebean/core/type/DataBinder.java index be6a3b559..b08bb529a 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/DataBinder.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/DataBinder.java @@ -163,4 +163,15 @@ public interface DataBinder { * Bind an array value. */ void setArray(String arrayType, Object[] elements) throws SQLException; + + /** + * Push json from dirty detection to be available for binding. + */ + void pushJson(String json); + + /** + * Pop json made during dirty detection for scalarType binding. + */ + String popJson(); + } diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java b/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java index a8ace12be..ef61b9122 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java @@ -48,4 +48,14 @@ public interface DataReader { Object getObject() throws SQLException; InputStream getBinaryStream() throws SQLException; + + /** + * Push json from dirty detection to be available for binding. + */ + void pushJson(String json); + + /** + * Pop json made during dirty detection for scalarType binding. + */ + String popJson(); } diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index a971a0115..831664991 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -34,6 +34,14 @@ import java.sql.SQLException; */ public interface ScalarType extends StringParser, StringFormatter, ScalarDataReader { + default boolean isJsonMapper() { + return false; + } + + default String jsonMapper(Object value) { + throw new UnsupportedOperationException(); + } + /** * 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. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 63599b7ad..d165b1489 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -2022,7 +2022,7 @@ public class BeanDescriptor implements BeanType, STreeType, SpiBeanType { public boolean isTableManaged(String tableName) { return owner.isTableManaged(tableName); } - + /** * Return the order column property. */ @@ -3200,7 +3200,7 @@ public class BeanDescriptor implements BeanType, STreeType, SpiBeanType { int propertyIndex = beanProperty.getPropertyIndex(); if (!ebi.isDirtyProperty(propertyIndex) && ebi.isLoadedProperty(propertyIndex)) { Object value = beanProperty.getValue(ebi.getOwner()); - if (value != null && beanProperty.isDirtyValue(value)) { + if (value != null && beanProperty.isDirtyValue(value, ebi)) { // mutable scalar value which is considered dirty so mark // it as such so that it is included in an update ebi.markPropertyAsChanged(propertyIndex); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 8793942b6..d3c832c85 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -13,6 +13,7 @@ import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; import io.ebean.plugin.Property; import io.ebean.text.StringParser; +import io.ebean.text.TextException; import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiQuery; @@ -31,11 +32,8 @@ import io.ebeaninternal.server.properties.BeanPropertySetter; import io.ebeaninternal.server.query.STreeProperty; import io.ebeaninternal.server.query.SqlBeanLoad; import io.ebeaninternal.server.query.SqlJoinType; -import io.ebeaninternal.server.type.DataBind; -import io.ebeaninternal.server.type.LocalEncryptedType; -import io.ebeaninternal.server.type.ScalarTypeBoolean; -import io.ebeaninternal.server.type.ScalarTypeEnum; -import io.ebeaninternal.server.type.ScalarTypeLogicalType; +import io.ebeaninternal.server.type.*; +import io.ebeaninternal.server.util.Md5; import io.ebeaninternal.util.ValueUtil; import io.ebeanservice.docstore.api.mapping.DocMappingBuilder; import io.ebeanservice.docstore.api.mapping.DocPropertyMapping; @@ -54,6 +52,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -220,6 +219,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { */ @SuppressWarnings("rawtypes") final ScalarType scalarType; + final boolean jsonMapperType; private final DocPropertyOptions docOptions; @@ -333,6 +333,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { this.formula = sqlFormulaSelect != null; this.dbType = deploy.getDbType(); this.scalarType = deploy.getScalarType(); + this.jsonMapperType = (scalarType == null) ? false : scalarType.isJsonMapper(); this.lob = isLobType(dbType); this.propertyType = deploy.getPropertyType(); this.field = deploy.getField(); @@ -427,6 +428,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { this.setter = source.setter; this.dbType = source.getDbType(true); this.scalarType = source.scalarType; + this.jsonMapperType = source.jsonMapperType; this.lob = isLobType(dbType); this.propertyType = source.getPropertyType(); this.field = source.getField(); @@ -630,8 +632,17 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { Object value = scalarType.read(reader); if (bean != null) { setValue(bean, value); + if (jsonMapperType) { + String json = reader.popJson(); + if (json != null) { + final String hash = Md5.hash(json); + bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + } + } } return value; + } catch (TextException e) { + throw e; } catch (Exception e) { throw new PersistenceException("Error readSet on " + descriptor + "." + name, e); } @@ -643,13 +654,11 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { public Object readSet(DbReadContext ctx, EntityBean bean) throws SQLException { try { - Object value = scalarType.read(ctx.getDataReader()); - if (bean != null) { - setValue(bean, value); - } - return value; - } catch (Exception e) { - throw new PersistenceException("Error readSet on " + descriptor + "." + name, e); + return readSet(ctx.getDataReader(), bean); + } catch (TextException e) { + bean._ebean_getIntercept().setLoadError(propertyIndex, e); + ctx.handleLoadError(getFullBeanName(), e); + return getValue(bean); } } @@ -1018,7 +1027,19 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { * Return true if the mutable value is considered dirty. * This is only used for 'mutable' scalar types like hstore etc. */ - boolean isDirtyValue(Object value) { + boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { + if (jsonMapperType) { + // dirty detection based on md5 hash of json content + final String json = scalarType.jsonMapper(value); + final String newHash = Md5.hash(json); + final String oldHash = ebi.mutableHash(propertyIndex); + if (!Objects.equals(newHash, oldHash)) { + ebi.mutableContent(propertyIndex, json); // so we only convert to json once + ebi.mutableHash(propertyIndex, newHash); // for dirty detection next time + return true; + } + return false; + } return scalarType.isDirty(value); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java index 189613808..0a4bb6b8a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dml/DmlHandler.java @@ -63,6 +63,11 @@ public abstract class DmlHandler implements PersistHandler, BindableRequest { } } + @Override + public void pushJson(String json) { + dataBind.pushJson(json); + } + @Override public long now() { return now; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java new file mode 100644 index 000000000..0a93452f1 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java @@ -0,0 +1,42 @@ +package io.ebeaninternal.server.persist.dmlbind; + +import io.ebean.bean.EntityBean; +import io.ebeaninternal.server.deploy.BeanProperty; +import io.ebeaninternal.server.util.Md5; + +import java.sql.SQLException; + +/** + * For JSON Jackson properties - dirty detection via MD5 of json content. + */ +class BindablePropertyJsonInsert extends BindableProperty { + + private final int propertyIndex; + + BindablePropertyJsonInsert(BeanProperty prop) { + super(prop); + this.propertyIndex = prop.getPropertyIndex(); + } + + /** + * Normal binding of a property value from the bean. + */ + @Override + public void dmlBind(BindableRequest request, EntityBean bean) throws SQLException { + if (bean == null) { + request.bind(null, prop); + } else { + Object value = prop.getValue(bean); + if (value == null) { + request.bind(null, prop); + } else { + // on insert store MD5 hash and push json + final String json = prop.format(value); + final String hash = Md5.hash(json); + bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + request.pushJson(json); + request.bind(value, prop); + } + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java new file mode 100644 index 000000000..3418369f5 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java @@ -0,0 +1,35 @@ +package io.ebeaninternal.server.persist.dmlbind; + +import io.ebean.bean.EntityBean; +import io.ebeaninternal.server.deploy.BeanProperty; + +import java.sql.SQLException; + +/** + * For JSON Jackson properties - dirty detection via MD5 of json content. + */ +class BindablePropertyJsonUpdate extends BindableProperty { + + private final int propertyIndex; + + BindablePropertyJsonUpdate(BeanProperty prop) { + super(prop); + this.propertyIndex = prop.getPropertyIndex(); + } + + /** + * Normal binding of a property value from the bean. + */ + @Override + public void dmlBind(BindableRequest request, EntityBean bean) throws SQLException { + if (bean == null) { + request.bind(null, prop); + } else { + // on update push json + final String json = bean._ebean_getIntercept().mutableContent(propertyIndex); + request.pushJson(json); + final Object value = prop.getValue(bean); + request.bind(value, prop); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableRequest.java index 4a1fc6754..e06862e86 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindableRequest.java @@ -57,4 +57,9 @@ public interface BindableRequest { * Return true if this is an update request. */ boolean isUpdate(); + + /** + * Push json content for scalarType bind(). + */ + void pushJson(String json); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java index b47b4ecb6..78ceb098b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java @@ -43,6 +43,13 @@ class FactoryProperty { return new BindableAssocOne((BeanPropertyAssocOne)prop); } + if (prop.getScalarType().isJsonMapper()) { + if (DmlMode.INSERT == mode) { + return new BindablePropertyJsonInsert(prop); + } else if (DmlMode.UPDATE == mode) { + return new BindablePropertyJsonUpdate(prop); + } + } return new BindableProperty(prop); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java index ac1386e11..20d1dfddb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java @@ -25,7 +25,6 @@ public class SqlBeanLoad { private final boolean rawSql; SqlBeanLoad(DbReadContext ctx, Class type, EntityBean bean, Mode queryMode) { - this.ctx = ctx; this.rawSql = ctx.isRawSql(); this.type = type; @@ -69,16 +68,16 @@ public class SqlBeanLoad { } try { - Object dbVal = prop.read(ctx); if (!refreshLoading) { - prop.setValue(bean, dbVal); - } else { - prop.setValueIntercept(bean, dbVal); + return prop.readSet(ctx, bean); } - + // TODO: maybe create prop.readSetIntercept() and move this + Object dbVal = prop.read(ctx); + prop.setValueIntercept(bean, dbVal); return dbVal; } catch (Exception e) { + // TODO: maybe move this into prop.readSetIntercept() bean._ebean_getIntercept().setLoadError(prop.getPropertyIndex(), e); ctx.handleLoadError(prop.getFullBeanName(), e); return prop.getValue(bean); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java index 2959c872c..489046405 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java @@ -36,6 +36,7 @@ public class DataBind implements DataBinder { private List inputStreams; protected int pos; + private String json; public DataBind(DataTimeZone dataTimeZone, PreparedStatement pstmt, Connection connection) { this.dataTimeZone = dataTimeZone; @@ -43,6 +44,16 @@ public class DataBind implements DataBinder { this.connection = connection; } + @Override + public void pushJson(String json) { + this.json = json; + } + + @Override + public String popJson() { + return json; + } + @Override public StringBuilder append(Object entry) { return bindLog.append(entry); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java index 9763a5667..b85c8e688 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java @@ -20,22 +20,29 @@ import java.util.Calendar; public class RsetDataReader implements DataReader { private static final int bufferSize = 512; - static final int clobBufferSize = 512; - static final int stringInitialSize = 512; private final DataTimeZone dataTimeZone; - private final ResultSet rset; - protected int pos; + private String json; public RsetDataReader(DataTimeZone dataTimeZone, ResultSet rset) { this.dataTimeZone = dataTimeZone; this.rset = rset; } + @Override + public void pushJson(String json) { + this.json = json; + } + + @Override + public String popJson() { + return json; + } + @Override public void close() throws SQLException { rset.close(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 5e24412b5..d71cf18d6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -56,6 +56,50 @@ class ScalarTypeJsonObjectMapper { GenericObject(TypeJsonManager jsonManager, AnnotatedField field, int dbType, Class rawType) { super(Object.class, jsonManager, field, dbType, DocPropertyType.OBJECT, rawType); } + + @Override + public boolean isJsonMapper() { + return true; + } + + @Override + public String jsonMapper(Object value) { + return formatValue(value); + } + + @Override + public Object read(DataReader reader) throws SQLException { + String json = reader.getString(); + if (json == null || json.isEmpty()) { + return null; + } + // pushJson such that we MD5 and store on EntityBeanIntercept later + reader.pushJson(json); + try { + return objectReader.readValue(json, deserType); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as " + deserType, json, e); + } + } + + @Override + public void bind(DataBinder binder, Object value) throws SQLException { + // popJson as dirty detection already converted to json string + String rawJson = binder.popJson(); + if (rawJson == null && value != null) { + rawJson = formatValue(value); // not expected, need to check? + } + if (pgType != null) { + binder.setObject(PostgresHelper.asObject(pgType, rawJson)); + } else { + if (value == null) { + // use varchar, otherwise SqlServer/db2 will fail with 'Invalid JDBC data type 5.001.' + binder.setNull(Types.VARCHAR); + } else { + binder.setString(rawJson); + } + } + } } /** @@ -118,10 +162,10 @@ class ScalarTypeJsonObjectMapper { */ private static abstract class Base extends ScalarTypeBase { - private final ObjectWriter objectWriter; - private final ObjectMapper objectReader; - private final JavaType deserType; - private final String pgType; + protected final ObjectWriter objectWriter; + protected final ObjectMapper objectReader; + protected final JavaType deserType; + protected final String pgType; private final DocPropertyType docType; private final TypeJsonManager.DirtyHandler dirtyHandler; diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 106beb9f2..4bc0194d6 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -74,6 +74,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { final List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, plain_bean=?, version=? where id=?"); + // plain_bean=?, no longer included with MD5 dirty detection + assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, version=? where id=?"); } } diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_List.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_List.java index 28fd78221..f43d6f23c 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_List.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_List.java @@ -113,7 +113,8 @@ public class TestDbJson_List extends BaseTestCase { List sql = LoggedSqlCollector.stop(); // we don't update the phone numbers (as they are not dirty) - assertSql(sql.get(0)).contains("update ebasic_json_list set name=?, plain_bean=?, version=? where"); + // plain_bean=?, no longer included with MD5 dirty detection + assertSql(sql.get(0)).contains("update ebasic_json_list set name=?, version=? where"); } public void update_when_dirty() { @@ -126,7 +127,8 @@ public class TestDbJson_List extends BaseTestCase { List sql = LoggedSqlCollector.stop(); // we don't update the phone numbers (as they are not dirty) - assertSql(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, tags=?, version=? where id=? and version=?"); + // plain_bean=? not included using MD5 dirty detection + assertSql(sql.get(0)).contains("update ebasic_json_list set tags=?, version=? where id=? and version=?"); } public void update_when_dirty_flags() { @@ -139,7 +141,8 @@ public class TestDbJson_List extends BaseTestCase { List sql = LoggedSqlCollector.stop(); // we don't update the phone numbers (as they are not dirty) - assertSql(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, flags=?, version=? where id=? and version=?;"); + // plain_bean=? not included with MD5 dirty detection + assertSql(sql.get(0)).contains("update ebasic_json_list set flags=?, version=? where id=? and version=?;"); } public void update_when_dirty_SetListMap() { @@ -154,7 +157,8 @@ public class TestDbJson_List extends BaseTestCase { List sql = LoggedSqlCollector.stop(); // we don't update the phone numbers (as they are not dirty) - assertSql(sql.get(0)).contains("update ebasic_json_list set beans=?, bean_list=?, bean_map=?, plain_bean=?, version=? where id=? and version=?"); + // plain_bean=? not included with MD5 dirty detection + assertSql(sql.get(0)).contains("update ebasic_json_list set beans=?, bean_list=?, bean_map=?, version=? where id=? and version=?"); } @Test diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java b/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java new file mode 100644 index 000000000..f3705aa16 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java @@ -0,0 +1,54 @@ +package org.tests.model.json; + +import io.ebean.annotation.DbJson; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; + +@Entity +public class EBasicPlain { + + @Id + long id; + + String attr; + + @DbJson(length = 500) + PlainBean plainBean; + + @Version + long version; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getAttr() { + return attr; + } + + public void setAttr(String attr) { + this.attr = attr; + } + + public PlainBean getPlainBean() { + return plainBean; + } + + public void setPlainBean(PlainBean plainBean) { + this.plainBean = plainBean; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } +} diff --git a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java new file mode 100644 index 000000000..0e3d22aa7 --- /dev/null +++ b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java @@ -0,0 +1,73 @@ +package org.tests.model.json; + +import io.ebean.DB; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestJacksonPlainBean { + + @Test + public void insertUpdate() { + + DB.getDefault(); + LoggedSqlCollector.start(); + + PlainBean content = new PlainBean(); + content.setAlong(42); + content.setName("foo"); + + EBasicPlain bean = new EBasicPlain(); + bean.setAttr("attr0"); + bean.setPlainBean(content); + + + DB.save(bean); + expectedSql(0, "insert into ebasic_plain (attr, plain_bean, version) values (?,?,?)"); + + + // inserted plainBean has not been mutated + bean.setAttr("attr1"); + DB.save(bean); + expectedSql(0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); + + + // inserted plainBean has now been mutated + content.setName("notFoo"); + bean.setAttr("attr2"); + DB.save(bean); + expectedSql(0, "update ebasic_plain set attr=?, plain_bean=?, version=? where id=? and version=?"); + + + final EBasicPlain found = DB.find(EBasicPlain.class, bean.getId()); + + // update mutating PlainBean only + final PlainBean plainBean = found.getPlainBean(); + plainBean.setName("mod1"); + DB.save(found); + expectedSql(1, "update ebasic_plain set plain_bean=?, version=? where id=? and version=?"); + + + // update bean, mutate PlainBean only + plainBean.setName("mod2"); + DB.save(found); + expectedSql(0, "update ebasic_plain set plain_bean=?, version=? where id=? and version=?"); + + + // update bean, not mutating PlainBean + found.setAttr("attr3"); + DB.save(found); + expectedSql(LoggedSqlCollector.stop(), 0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); + } + + private void expectedSql(int i, String s) { + assertThat(LoggedSqlCollector.current().get(i)).contains(s); + } + + private void expectedSql(List sql, int i, String s) { + assertThat(sql.get(i)).contains(s); + } +} From 11eed4cfe9cb9118624bd0200691acf6b71d587e Mon Sep 17 00:00:00 2001 From: rbygrave Date: Wed, 28 Jul 2021 21:43:57 +1200 Subject: [PATCH 02/26] Add BeanPropertyJsonMapper for JSON dirty detection --- .../server/deploy/BeanProperty.java | 22 ------- .../server/deploy/BeanPropertyJsonMapper.java | 57 +++++++++++++++++++ .../deploy/meta/DeployBeanProperty.java | 3 + .../deploy/meta/DeployBeanPropertyLists.java | 13 ++--- 4 files changed, 64 insertions(+), 31 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index d3c832c85..d9af317d7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -219,7 +219,6 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { */ @SuppressWarnings("rawtypes") final ScalarType scalarType; - final boolean jsonMapperType; private final DocPropertyOptions docOptions; @@ -333,7 +332,6 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { this.formula = sqlFormulaSelect != null; this.dbType = deploy.getDbType(); this.scalarType = deploy.getScalarType(); - this.jsonMapperType = (scalarType == null) ? false : scalarType.isJsonMapper(); this.lob = isLobType(dbType); this.propertyType = deploy.getPropertyType(); this.field = deploy.getField(); @@ -428,7 +426,6 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { this.setter = source.setter; this.dbType = source.getDbType(true); this.scalarType = source.scalarType; - this.jsonMapperType = source.jsonMapperType; this.lob = isLobType(dbType); this.propertyType = source.getPropertyType(); this.field = source.getField(); @@ -632,13 +629,6 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { Object value = scalarType.read(reader); if (bean != null) { setValue(bean, value); - if (jsonMapperType) { - String json = reader.popJson(); - if (json != null) { - final String hash = Md5.hash(json); - bean._ebean_getIntercept().mutableHash(propertyIndex, hash); - } - } } return value; } catch (TextException e) { @@ -1028,18 +1018,6 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { * This is only used for 'mutable' scalar types like hstore etc. */ boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { - if (jsonMapperType) { - // dirty detection based on md5 hash of json content - final String json = scalarType.jsonMapper(value); - final String newHash = Md5.hash(json); - final String oldHash = ebi.mutableHash(propertyIndex); - if (!Objects.equals(newHash, oldHash)) { - ebi.mutableContent(propertyIndex, json); // so we only convert to json once - ebi.mutableHash(propertyIndex, newHash); // for dirty detection next time - return true; - } - return false; - } return scalarType.isDirty(value); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java new file mode 100644 index 000000000..2d24958b6 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -0,0 +1,57 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.bean.EntityBean; +import io.ebean.bean.EntityBeanIntercept; +import io.ebean.core.type.DataReader; +import io.ebean.text.TextException; +import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.util.Md5; + +import javax.persistence.PersistenceException; +import java.sql.SQLException; +import java.util.Objects; + +public class BeanPropertyJsonMapper extends BeanProperty { + + public BeanPropertyJsonMapper(BeanDescriptor desc, DeployBeanProperty deployProp) { + super(desc, deployProp); + } + + /** + * Return true if the mutable value is considered dirty. + * This is only used for 'mutable' scalar types like hstore etc. + */ + @Override + boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { + // dirty detection based on md5 hash of json content + final String json = scalarType.jsonMapper(value); + final String newHash = Md5.hash(json); + final String oldHash = ebi.mutableHash(propertyIndex); + if (!Objects.equals(newHash, oldHash)) { + ebi.mutableContent(propertyIndex, json); // so we only convert to json once + ebi.mutableHash(propertyIndex, newHash); // for dirty detection next time + return true; + } + return false; + } + + @Override + public Object readSet(DataReader reader, EntityBean bean) throws SQLException { + try { + Object value = scalarType.read(reader); + if (bean != null) { + setValue(bean, value); + String json = reader.popJson(); + if (json != null) { + final String hash = Md5.hash(json); + bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + } + } + return value; + } catch (TextException e) { + throw e; + } catch (Exception e) { + throw new PersistenceException("Error readSet on " + descriptor + "." + name, e); + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index e20c898ce..92ad7f1e9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -1201,4 +1201,7 @@ public class DeployBeanProperty { return false; } + boolean isJsonMapper() { + return scalarType != null && scalarType.isJsonMapper(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java index f6280b260..9843f8e5c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java @@ -1,15 +1,7 @@ package io.ebeaninternal.server.deploy.meta; import io.ebean.bean.EntityBean; -import io.ebeaninternal.server.deploy.BeanDescriptor; -import io.ebeaninternal.server.deploy.BeanDescriptorMap; -import io.ebeaninternal.server.deploy.BeanProperty; -import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; -import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; -import io.ebeaninternal.server.deploy.BeanPropertyIdClass; -import io.ebeaninternal.server.deploy.BeanPropertyOrderColumn; -import io.ebeaninternal.server.deploy.BeanPropertySimpleCollection; -import io.ebeaninternal.server.deploy.InheritInfo; +import io.ebeaninternal.server.deploy.*; import io.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty; import io.ebeaninternal.server.properties.BeanPropertySetter; import io.ebeaninternal.server.type.ScalarTypeString; @@ -490,6 +482,9 @@ public class DeployBeanPropertyLists { return new BeanPropertyAssocMany(desc, (DeployBeanPropertyAssocMany) deployProp); } + if (deployProp.isJsonMapper()) { + return new BeanPropertyJsonMapper(desc, deployProp); + } return new BeanProperty(desc, deployProp); } From 49b86d07d3bb5bdd13b97a180ab92038011655fc Mon Sep 17 00:00:00 2001 From: rbygrave Date: Wed, 28 Jul 2021 22:48:29 +1200 Subject: [PATCH 03/26] Modify SqlBeanLoad to use readSet() Note that we don't need to use readSetIntercept() now as there is no property change listener support --- .../server/deploy/BeanProperty.java | 18 +++++--------- .../server/query/SqlBeanLoad.java | 24 ++----------------- .../java/io/ebean/EbeanServer_refresh.java | 5 +++- 3 files changed, 12 insertions(+), 35 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index d9af317d7..4306ed97d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -33,7 +33,6 @@ import io.ebeaninternal.server.query.STreeProperty; import io.ebeaninternal.server.query.SqlBeanLoad; import io.ebeaninternal.server.query.SqlJoinType; import io.ebeaninternal.server.type.*; -import io.ebeaninternal.server.util.Md5; import io.ebeaninternal.util.ValueUtil; import io.ebeanservice.docstore.api.mapping.DocMappingBuilder; import io.ebeanservice.docstore.api.mapping.DocPropertyMapping; @@ -52,7 +51,6 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; /** @@ -643,13 +641,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { } public Object readSet(DbReadContext ctx, EntityBean bean) throws SQLException { - try { - return readSet(ctx.getDataReader(), bean); - } catch (TextException e) { - bean._ebean_getIntercept().setLoadError(propertyIndex, e); - ctx.handleLoadError(getFullBeanName(), e); - return getValue(bean); - } + return readSet(ctx.getDataReader(), bean); } @SuppressWarnings("unchecked") @@ -908,7 +900,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { /** * Return the name of the property. */ - @Override @Nonnull + @Override + @Nonnull public String getName() { return name; } @@ -1280,7 +1273,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { @Override public Object localEncrypt(Object value) { - return ((LocalEncryptedType)scalarType).localEncrypt(value); + return ((LocalEncryptedType) scalarType).localEncrypt(value); } /** @@ -1393,7 +1386,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { /** * Return the property type. */ - @Override @Nonnull + @Override + @Nonnull public Class getPropertyType() { return propertyType; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java index 20d1dfddb..83c7b2936 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlBeanLoad.java @@ -18,10 +18,8 @@ public class SqlBeanLoad { private final DbReadContext ctx; private final EntityBean bean; private final EntityBeanIntercept ebi; - private final Class type; private final boolean lazyLoading; - private final boolean refreshLoading; private final boolean rawSql; SqlBeanLoad(DbReadContext ctx, Class type, EntityBean bean, Mode queryMode) { @@ -29,7 +27,6 @@ public class SqlBeanLoad { this.rawSql = ctx.isRawSql(); this.type = type; this.lazyLoading = queryMode == Mode.LAZYLOAD_BEAN; - this.refreshLoading = queryMode == Mode.REFRESH_BEAN; this.bean = bean; this.ebi = bean == null ? null : bean._ebean_getIntercept(); } @@ -49,35 +46,22 @@ public class SqlBeanLoad { } public Object load(BeanProperty prop) { - if (!rawSql && !prop.isLoadProperty(ctx.isDraftQuery())) { return null; } - if ((bean == null) || (lazyLoading && ebi.isLoadedProperty(prop.getPropertyIndex())) || (type != null && !prop.isAssignableFrom(type))) { - // ignore this property // ... null: bean already in persistence context // ... lazyLoading: partial bean that is lazy loading // ... type: inheritance and not assignable to this instance - prop.loadIgnore(ctx); return null; } - try { - if (!refreshLoading) { - return prop.readSet(ctx, bean); - } - // TODO: maybe create prop.readSetIntercept() and move this - Object dbVal = prop.read(ctx); - prop.setValueIntercept(bean, dbVal); - return dbVal; - + return prop.readSet(ctx, bean); } catch (Exception e) { - // TODO: maybe move this into prop.readSetIntercept() bean._ebean_getIntercept().setLoadError(prop.getPropertyIndex(), e); ctx.handleLoadError(prop.getFullBeanName(), e); return prop.getValue(bean); @@ -88,10 +72,6 @@ public class SqlBeanLoad { * Load the given value into the property. */ public void load(BeanProperty target, Object dbVal) { - if (!refreshLoading) { - target.setValue(bean, dbVal); - } else { - target.setValueIntercept(bean, dbVal); - } + target.setValue(bean, dbVal); } } diff --git a/ebean-core/src/test/java/io/ebean/EbeanServer_refresh.java b/ebean-core/src/test/java/io/ebean/EbeanServer_refresh.java index 1e1900a13..66af5d164 100644 --- a/ebean-core/src/test/java/io/ebean/EbeanServer_refresh.java +++ b/ebean-core/src/test/java/io/ebean/EbeanServer_refresh.java @@ -11,7 +11,7 @@ import java.util.HashMap; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; public class EbeanServer_refresh { @@ -39,8 +39,11 @@ public class EbeanServer_refresh { assertEquals(rows, 1); + basic.setName("modify"); + assertTrue(DB.getBeanState(basic).isDirty()); server.refresh(basic); assertEquals(basic.getStatus(), EBasic.Status.ACTIVE); + assertFalse(DB.getBeanState(basic).isDirty()); } @Test From 5e595ec5f875690d7c6e198871fc8f726f234ef7 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 28 Jul 2021 14:54:56 +0200 Subject: [PATCH 04/26] Proof of concept to support also bean change detection / changelog on mutable properties --- .../io/ebean/bean/EntityBeanIntercept.java | 29 ++++++-- .../main/java/io/ebean/bean/MutableJson.java | 12 ++++ .../java/io/ebean/core/type/ScalarType.java | 5 ++ .../server/deploy/BeanPropertyJsonMapper.java | 13 ++-- .../dmlbind/BindablePropertyJsonInsert.java | 3 +- .../type/ScalarTypeJsonObjectMapper.java | 64 +++++++++++++++++ .../org/tests/json/TestDbJson_Jackson3.java | 70 ++++++++++++++++++- 7 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/bean/MutableJson.java diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 59a2450bd..2ea4afbb5 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -94,7 +94,7 @@ public final class EntityBeanIntercept implements Serializable { /** * Holds MD5 hash of json loaded jackson beans. */ - private String[] mutableHash; + private MutableJson[] mutableHash; /** * Holds json content determined at point of dirty check. @@ -381,7 +381,11 @@ public final class EntityBeanIntercept implements Serializable { this.origValues = null; for (int i = 0; i < flags.length; i++) { flags[i] &= ~(FLAG_CHANGED_PROP + FLAG_ORIG_VALUE_SET); + if (mutableHash != null && mutableHash[i] != null) { + mutableHash[i].update(owner._ebean_getField(i)); + } } + this.dirty = false; } @@ -649,7 +653,7 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyNames(Set props, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0 || isChangedByHash(i)) { // the property has been changed on this bean props.add((prefix == null ? getProperty(i) : prefix + getProperty(i))); } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { @@ -667,7 +671,7 @@ public final class EntityBeanIntercept implements Serializable { String[] names = owner._ebean_getPropertyNames(); int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0 || isChangedByHash(i)) { if (propertyNames.contains(names[i])) { return true; } @@ -695,7 +699,12 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyValues(Map dirtyValues, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + if (isChangedByHash(i)) { + String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); + Object newVal = owner._ebean_getField(i); + Object oldVal = mutableHash[i].get(); + dirtyValues.put(propName, new ValuePair(newVal, oldVal)); + } else if ((flags[i] & FLAG_CHANGED_PROP) != 0) { // the property has been changed on this bean String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); Object newVal = owner._ebean_getField(i); @@ -1150,13 +1159,19 @@ public final class EntityBeanIntercept implements Serializable { return ret; } - public String mutableHash(int propertyIndex) { + private boolean isChangedByHash(int propertyIndex) { + return mutableHash != null + && mutableHash[propertyIndex] != null + && !mutableHash[propertyIndex].isEqualToObject(owner._ebean_getField(propertyIndex)); + } + + public MutableJson mutableHash(int propertyIndex) { return mutableHash == null ? null : mutableHash[propertyIndex]; } - public void mutableHash(int propertyIndex, String content) { + public void mutableHash(int propertyIndex, MutableJson content) { if (mutableHash == null) { - mutableHash = new String[flags.length]; + mutableHash = new MutableJson[flags.length]; } mutableHash[propertyIndex] = content; } diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableJson.java b/ebean-api/src/main/java/io/ebean/bean/MutableJson.java new file mode 100644 index 000000000..5989465e2 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/MutableJson.java @@ -0,0 +1,12 @@ +package io.ebean.bean; + +public interface MutableJson { + + boolean isEqualToObject(Object obj); + + boolean isEqualToJson(String json); + + Object get(); + + void update(Object obj); +} diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index 831664991..b9f7f168c 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -2,6 +2,8 @@ package io.ebean.core.type; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; + +import io.ebean.bean.MutableJson; import io.ebean.text.StringFormatter; import io.ebean.text.StringParser; @@ -42,6 +44,9 @@ public interface ScalarType extends StringParser, StringFormatter, ScalarData throw new UnsupportedOperationException(); } + default MutableJson jsonMutable(String json) { + throw new UnsupportedOperationException(); + } /** * 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. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index 2d24958b6..de5d148b5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; +import io.ebean.bean.MutableJson; import io.ebean.core.type.DataReader; import io.ebean.text.TextException; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; @@ -25,11 +26,11 @@ public class BeanPropertyJsonMapper extends BeanProperty { boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { // dirty detection based on md5 hash of json content final String json = scalarType.jsonMapper(value); - final String newHash = Md5.hash(json); - final String oldHash = ebi.mutableHash(propertyIndex); - if (!Objects.equals(newHash, oldHash)) { + final MutableJson oldHash = ebi.mutableHash(propertyIndex); + if (oldHash == null || !oldHash.isEqualToJson(json)) { ebi.mutableContent(propertyIndex, json); // so we only convert to json once - ebi.mutableHash(propertyIndex, newHash); // for dirty detection next time + //ebi.mutableHash(propertyIndex, scalarType.jsonMutable(json)); // for dirty detection next time + //must be done AFTER persistControllers are called. return true; } return false; @@ -43,8 +44,8 @@ public class BeanPropertyJsonMapper extends BeanProperty { setValue(bean, value); String json = reader.popJson(); if (json != null) { - final String hash = Md5.hash(json); - bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + final String hash = scalarType.format(value); + bean._ebean_getIntercept().mutableHash(propertyIndex, scalarType.jsonMutable(hash)); } } return value; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java index 0a93452f1..24cf050e3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java @@ -32,8 +32,7 @@ class BindablePropertyJsonInsert extends BindableProperty { } else { // on insert store MD5 hash and push json final String json = prop.format(value); - final String hash = Md5.hash(json); - bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + bean._ebean_getIntercept().mutableHash(propertyIndex, prop.getScalarType().jsonMutable(json)); request.pushJson(json); request.bind(value, prop); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index d71cf18d6..5b44b63e4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.introspect.AnnotatedField; + +import io.ebean.bean.MutableJson; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -15,6 +17,7 @@ import io.ebean.text.TextException; import io.ebeaninternal.json.ModifyAwareList; import io.ebeaninternal.json.ModifyAwareMap; import io.ebeaninternal.json.ModifyAwareSet; +import io.ebeaninternal.server.util.Md5; import javax.persistence.PersistenceException; import java.io.DataInput; @@ -24,6 +27,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -66,6 +70,66 @@ class ScalarTypeJsonObjectMapper { public String jsonMapper(Object value) { return formatValue(value); } + + private class Md5MutableJson implements MutableJson { + + private String md5; + Md5MutableJson(String json) { + md5 = Md5.hash(json); + } + @Override + public boolean isEqualToObject(Object obj) { + return true; // we cannot determine differences... + } + + @Override + public boolean isEqualToJson(String json) { + return Md5.hash(json).equals(md5); + } + + @Override + public Object get() { + return null; // cannot create object from json + } + @Override + public void update(Object obj) { + md5 = Md5.hash(format(obj)); + } + } + + private class PlainMutableJson implements MutableJson { + + private String originalJson; + PlainMutableJson(String json) { + originalJson = json; + } + @Override + public boolean isEqualToObject(Object obj) { + return isEqualToJson(format(obj)); + } + + @Override + public boolean isEqualToJson(String json) { + return Objects.equals(originalJson, json); + } + + @Override + public Object get() { + return parse(originalJson); + } + @Override + public void update(Object obj) { + originalJson = format(obj); + } + } + @Override + public MutableJson jsonMutable(String originalJson) { + if (false) { + return new Md5MutableJson(originalJson); + } else { + return new PlainMutableJson(originalJson); + } + } @Override public Object read(DataReader reader) throws SQLException { diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 4bc0194d6..4d31f8a56 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -1,7 +1,11 @@ package org.tests.json; import io.ebean.BaseTestCase; +import io.ebean.BeanState; import io.ebean.DB; +import io.ebean.ValuePair; +import io.ebean.event.BeanPersistAdapter; +import io.ebean.event.BeanPersistRequest; import io.ebeantest.LoggedSql; import org.junit.Test; import org.tests.model.json.EBasicJsonJackson3; @@ -11,11 +15,33 @@ import org.tests.model.json.PlainBeanDirtyAware; import java.util.Arrays; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; public class TestDbJson_Jackson3 extends BaseTestCase { + public static class EBasicJsonListPersistController extends BeanPersistAdapter { + + private static Map updatedValues; + + @Override + public boolean isRegisterFor(Class cls) { + return EBasicJsonList.class.isAssignableFrom(cls); + } + + @Override + public boolean preInsert(BeanPersistRequest request) { + updatedValues = request.getUpdatedValues(); + return true; + } + + @Override + public boolean preUpdate(BeanPersistRequest request) { + updatedValues = request.getUpdatedValues(); + return true; + } + } @Test public void updateIncludesJsonColumn_when_explicit_isMarkedDirty() { @@ -69,12 +95,54 @@ public class TestDbJson_Jackson3 extends BaseTestCase { found.setName("p1-mod"); found.setBeanList(null); + BeanState state = DB.getBeanState(found); + assertThat(state.getChangedProps()).containsExactlyInAnyOrder("name", "beanList"); + + ValuePair pair = state.getDirtyValues().get("name"); + assertThat(pair.getNewValue()).isEqualTo("p1-mod"); + assertThat(pair.getOldValue()).isEqualTo("p1"); + + pair = state.getDirtyValues().get("beanList"); + assertThat(pair.getNewValue()).isEqualTo(null); + assertThat((List)pair.getOldValue()).hasSize(1) + .extracting(PlainBean::getName).containsExactly("a"); + + LoggedSql.start(); DB.save(found); - final List sql = LoggedSql.stop(); + List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); // plain_bean=?, no longer included with MD5 dirty detection assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, version=? where id=?"); + + assertThat(EBasicJsonListPersistController.updatedValues.entrySet()) + .extracting(Map.Entry::toString) + .containsExactlyInAnyOrder("beanList=null,[name:a]","name=p1-mod,p1","version=2,1"); + + + found.getPlainBean().setName("b"); + + // CHECKME: How do we get these checks to work? + // assertThat(DB.getBeanState(found).isDirty()).isTrue(); + + state = DB.getBeanState(found); + assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainBean"); + pair = state.getDirtyValues().get("plainBean"); + assertThat(pair.getNewValue()).hasToString("name:b"); + assertThat(pair.getOldValue()).hasToString("name:a"); + + + LoggedSql.start(); + DB.save(found); + + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + // plain_bean=?, no longer included with MD5 dirty detection + assertThat(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, version=? where id=?"); + + assertThat(EBasicJsonListPersistController.updatedValues.entrySet()) + .extracting(Map.Entry::toString) + .containsExactlyInAnyOrder("plainBean=name:b,name:a", "version=3,2"); } } From b497b8635b8c3fcbc0ee08028aec2b67be2148e0 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 28 Jul 2021 17:22:29 +0200 Subject: [PATCH 05/26] some refactor and tidying --- .../io/ebean/bean/EntityBeanIntercept.java | 71 +++++++---- .../main/java/io/ebean/bean/MutableHash.java | 16 +++ .../main/java/io/ebean/bean/MutableJson.java | 12 -- .../java/io/ebean/core/type/ScalarType.java | 5 +- .../server/deploy/BeanProperty.java | 8 ++ .../server/deploy/BeanPropertyJsonMapper.java | 12 +- .../dmlbind/BindablePropertyJsonInsert.java | 7 +- .../dmlbind/BindablePropertyJsonUpdate.java | 5 +- .../ebeaninternal/server/type/DataBind.java | 5 +- .../type/ScalarTypeJsonObjectMapper.java | 111 +++++++++--------- .../org/tests/json/TestDbJson_Jackson3.java | 28 ++++- .../tests/model/json/EBasicJsonJackson3.java | 11 ++ 12 files changed, 184 insertions(+), 107 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/bean/MutableHash.java delete mode 100644 ebean-api/src/main/java/io/ebean/bean/MutableJson.java diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 2ea4afbb5..37446a1b3 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -51,6 +51,11 @@ public final class EntityBeanIntercept implements Serializable { */ private static final byte FLAG_ORIG_VALUE_SET = 8; + /** + * Flags indicating if the mutable hash is set. + */ + private static final byte FLAG_MUTABLE_HASH_SET = 16; + private transient final ReentrantLock lock = new ReentrantLock(); private transient NodeUsageCollector nodeUsageCollector; private transient PersistenceContext persistenceContext; @@ -94,7 +99,7 @@ public final class EntityBeanIntercept implements Serializable { /** * Holds MD5 hash of json loaded jackson beans. */ - private MutableJson[] mutableHash; + private MutableHash[] mutableHash; /** * Holds json content determined at point of dirty check. @@ -239,6 +244,17 @@ public final class EntityBeanIntercept implements Serializable { * if any embedded beans are either new or dirty (and hence need saving). */ public boolean isDirty() { + if (dirty) { + return true; + } + if (mutableHash != null) { + for (int i = 0; i < mutableHash.length; i++) { + if (mutableHash[i] != null && !mutableHash[i].isEqualToObject(owner._ebean_getField(i))) { + dirty = true; + break; + } + } + } return dirty; } @@ -379,13 +395,10 @@ public final class EntityBeanIntercept implements Serializable { this.owner._ebean_setEmbeddedLoaded(); this.lazyLoadProperty = -1; this.origValues = null; + this.mutableContent = null; for (int i = 0; i < flags.length; i++) { - flags[i] &= ~(FLAG_CHANGED_PROP + FLAG_ORIG_VALUE_SET); - if (mutableHash != null && mutableHash[i] != null) { - mutableHash[i].update(owner._ebean_getField(i)); - } + flags[i] &= ~(FLAG_CHANGED_PROP | FLAG_ORIG_VALUE_SET); } - this.dirty = false; } @@ -653,14 +666,14 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyNames(Set props, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0 || isChangedByHash(i)) { + if (isChangedProp(i)) { // the property has been changed on this bean props.add((prefix == null ? getProperty(i) : prefix + getProperty(i))); } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, getProperty(i) + "."); - } + } } } @@ -671,7 +684,7 @@ public final class EntityBeanIntercept implements Serializable { String[] names = owner._ebean_getPropertyNames(); int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0 || isChangedByHash(i)) { + if (isChangedProp(i)) { if (propertyNames.contains(names[i])) { return true; } @@ -699,16 +712,17 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyValues(Map dirtyValues, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (isChangedByHash(i)) { - String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); - Object newVal = owner._ebean_getField(i); - Object oldVal = mutableHash[i].get(); - dirtyValues.put(propName, new ValuePair(newVal, oldVal)); - } else if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + + if (isChangedProp(i)) { // the property has been changed on this bean String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); Object newVal = owner._ebean_getField(i); Object oldVal = getOrigValue(i); + if ((flags[i] & (FLAG_ORIG_VALUE_SET | FLAG_MUTABLE_HASH_SET)) == FLAG_MUTABLE_HASH_SET) { + // mutable hash set, but not ORIG_VALUE + oldVal = mutableHash[i].get(); + setOriginalValue(i, oldVal); + } if (notEqual(oldVal, newVal)) { dirtyValues.put(propName, new ValuePair(newVal, oldVal)); } @@ -726,7 +740,7 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyValues(BeanDiffVisitor visitor) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + if (isChangedProp(i)) { // the property has been changed on this bean Object newVal = owner._ebean_getField(i); Object oldVal = getOrigValue(i); @@ -761,7 +775,7 @@ public final class EntityBeanIntercept implements Serializable { } int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { // we do not check against mutablecontent here. sb.append(i).append(','); } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse @@ -1159,20 +1173,29 @@ public final class EntityBeanIntercept implements Serializable { return ret; } - private boolean isChangedByHash(int propertyIndex) { - return mutableHash != null - && mutableHash[propertyIndex] != null - && !mutableHash[propertyIndex].isEqualToObject(owner._ebean_getField(propertyIndex)); + private boolean isChangedProp(int i) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { + return true; + } else if (mutableHash == null || mutableHash[i] == null + || mutableHash[i].isEqualToObject(owner._ebean_getField(i))) { + return false; + } else { + // mark for change + flags[i] |= FLAG_CHANGED_PROP; + dirty = true; // this makes the bean automatically dirty! + return true; + } } - public MutableJson mutableHash(int propertyIndex) { + public MutableHash mutableHash(int propertyIndex) { return mutableHash == null ? null : mutableHash[propertyIndex]; } - public void mutableHash(int propertyIndex, MutableJson content) { + public void mutableHash(int propertyIndex, MutableHash content) { if (mutableHash == null) { - mutableHash = new MutableJson[flags.length]; + mutableHash = new MutableHash[flags.length]; } + flags[propertyIndex] |= FLAG_MUTABLE_HASH_SET; mutableHash[propertyIndex] = content; } diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java new file mode 100644 index 000000000..09c8761af --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java @@ -0,0 +1,16 @@ +package io.ebean.bean; + + +public interface MutableHash { + + boolean isEqualToJson(String json); + + default boolean isEqualToObject(Object obj) { + return true; + } + + default Object get() { + return null; + } +} + \ No newline at end of file diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableJson.java b/ebean-api/src/main/java/io/ebean/bean/MutableJson.java deleted file mode 100644 index 5989465e2..000000000 --- a/ebean-api/src/main/java/io/ebean/bean/MutableJson.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.ebean.bean; - -public interface MutableJson { - - boolean isEqualToObject(Object obj); - - boolean isEqualToJson(String json); - - Object get(); - - void update(Object obj); -} diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index b9f7f168c..55c8ad9bd 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -3,7 +3,7 @@ package io.ebean.core.type; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import io.ebean.bean.MutableJson; +import io.ebean.bean.MutableHash; import io.ebean.text.StringFormatter; import io.ebean.text.StringParser; @@ -44,9 +44,10 @@ public interface ScalarType extends StringParser, StringFormatter, ScalarData throw new UnsupportedOperationException(); } - default MutableJson jsonMutable(String json) { + default MutableHash createMutableHash(String json) { throw new UnsupportedOperationException(); } + /** * 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. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 4306ed97d..99ef307a7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonToken; import io.ebean.ValuePair; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; +import io.ebean.bean.MutableHash; import io.ebean.bean.PersistenceContext; import io.ebean.config.EncryptKey; import io.ebean.config.dbplatform.DbEncryptFunction; @@ -818,6 +819,13 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { public Object parse(String value) { return scalarType.parse(value); } + + /** + * creates a mutableHash for the given JSON value. + */ + public MutableHash createMutableHash(String json) { + return scalarType.createMutableHash(json); + } /** * Read the value for this property from L2 cache entry and set it to the bean. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index de5d148b5..204b9aac5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -2,15 +2,13 @@ package io.ebeaninternal.server.deploy; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; -import io.ebean.bean.MutableJson; +import io.ebean.bean.MutableHash; import io.ebean.core.type.DataReader; import io.ebean.text.TextException; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; -import io.ebeaninternal.server.util.Md5; import javax.persistence.PersistenceException; import java.sql.SQLException; -import java.util.Objects; public class BeanPropertyJsonMapper extends BeanProperty { @@ -26,11 +24,9 @@ public class BeanPropertyJsonMapper extends BeanProperty { boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { // dirty detection based on md5 hash of json content final String json = scalarType.jsonMapper(value); - final MutableJson oldHash = ebi.mutableHash(propertyIndex); + final MutableHash oldHash = ebi.mutableHash(propertyIndex); if (oldHash == null || !oldHash.isEqualToJson(json)) { ebi.mutableContent(propertyIndex, json); // so we only convert to json once - //ebi.mutableHash(propertyIndex, scalarType.jsonMutable(json)); // for dirty detection next time - //must be done AFTER persistControllers are called. return true; } return false; @@ -44,8 +40,8 @@ public class BeanPropertyJsonMapper extends BeanProperty { setValue(bean, value); String json = reader.popJson(); if (json != null) { - final String hash = scalarType.format(value); - bean._ebean_getIntercept().mutableHash(propertyIndex, scalarType.jsonMutable(hash)); + final MutableHash hash = scalarType.createMutableHash(json); + bean._ebean_getIntercept().mutableHash(propertyIndex, hash); } } return value; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java index 24cf050e3..e9de67a52 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java @@ -1,8 +1,8 @@ package io.ebeaninternal.server.persist.dmlbind; import io.ebean.bean.EntityBean; +import io.ebean.bean.MutableHash; import io.ebeaninternal.server.deploy.BeanProperty; -import io.ebeaninternal.server.util.Md5; import java.sql.SQLException; @@ -30,9 +30,10 @@ class BindablePropertyJsonInsert extends BindableProperty { if (value == null) { request.bind(null, prop); } else { - // on insert store MD5 hash and push json + // on insert store hash and push json final String json = prop.format(value); - bean._ebean_getIntercept().mutableHash(propertyIndex, prop.getScalarType().jsonMutable(json)); + final MutableHash hash = prop.createMutableHash(json); + bean._ebean_getIntercept().mutableHash(propertyIndex, hash); request.pushJson(json); request.bind(value, prop); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java index 3418369f5..ab281cf01 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.persist.dmlbind; import io.ebean.bean.EntityBean; +import io.ebean.bean.MutableHash; import io.ebeaninternal.server.deploy.BeanProperty; import java.sql.SQLException; @@ -25,8 +26,10 @@ class BindablePropertyJsonUpdate extends BindableProperty { if (bean == null) { request.bind(null, prop); } else { - // on update push json + // on update store hash and push json final String json = bean._ebean_getIntercept().mutableContent(propertyIndex); + final MutableHash hash = prop.createMutableHash(json); + bean._ebean_getIntercept().mutableHash(propertyIndex, hash); request.pushJson(json); final Object value = prop.getValue(bean); request.bind(value, prop); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java index 489046405..a1a9ec8cf 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DataBind.java @@ -46,12 +46,15 @@ public class DataBind implements DataBinder { @Override public void pushJson(String json) { + assert this.json == null; // we can only push one value this.json = json; } @Override public String popJson() { - return json; + String ret = json; + json = null; + return ret; } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 5b44b63e4..0012c4cd6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.introspect.AnnotatedField; -import io.ebean.bean.MutableJson; +import io.ebean.bean.MutableHash; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -51,7 +51,59 @@ class ScalarTypeJsonObjectMapper { } return new GenericObject(jsonManager, field, dbType, type); } + + private static class Md5MutableHash implements MutableHash { + private final String md5; + + Md5MutableHash(String json) { + md5 = Md5.hash(json); + } + + @Override + public boolean isEqualToObject(Object obj) { + return true; // we cannot determine differences... + } + + @Override + public boolean isEqualToJson(String json) { + return Md5.hash(json).equals(md5); + } + + @Override + public Object get() { + return null; // cannot create object from json + } + + } + + private static class JsonMutableHash implements MutableHash { + + private final String originalJson; + private ScalarType parent; + + JsonMutableHash(ScalarType parent, String json) { + this.parent = parent; + originalJson = json; + } + + @Override + public boolean isEqualToObject(Object obj) { + return isEqualToJson(parent.format(obj)); + } + + @Override + public boolean isEqualToJson(String json) { + return Objects.equals(originalJson, json); + } + + @Override + public Object get() { + return parent.parse(originalJson); + } + + } + /** * Maps any type (Object) using Jackson ObjectMapper. */ @@ -71,63 +123,16 @@ class ScalarTypeJsonObjectMapper { return formatValue(value); } - private class Md5MutableJson implements MutableJson { - private String md5; - Md5MutableJson(String json) { - md5 = Md5.hash(json); - } - @Override - public boolean isEqualToObject(Object obj) { - return true; // we cannot determine differences... - } - @Override - public boolean isEqualToJson(String json) { - return Md5.hash(json).equals(md5); - } - @Override - public Object get() { - return null; // cannot create object from json - } - @Override - public void update(Object obj) { - md5 = Md5.hash(format(obj)); - } - } - - private class PlainMutableJson implements MutableJson { - - private String originalJson; - PlainMutableJson(String json) { - originalJson = json; - } - @Override - public boolean isEqualToObject(Object obj) { - return isEqualToJson(format(obj)); - } - - @Override - public boolean isEqualToJson(String json) { - return Objects.equals(originalJson, json); - } - - @Override - public Object get() { - return parse(originalJson); - } - @Override - public void update(Object obj) { - originalJson = format(obj); - } - } + @Override - public MutableJson jsonMutable(String originalJson) { - if (false) { - return new Md5MutableJson(originalJson); + public MutableHash createMutableHash(String json) { + if (false) { // TODO should we make that configurable? + return new Md5MutableHash(json); } else { - return new PlainMutableJson(originalJson); + return new JsonMutableHash(this, json); } } diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 4d31f8a56..8e399d3df 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -50,6 +50,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { EBasicJsonJackson3 bean = new EBasicJsonJackson3(); bean.setName("b1"); bean.setPlainValue(contentBean); + bean.setPlainValue2(contentBean); bean.save(); @@ -121,10 +122,11 @@ public class TestDbJson_Jackson3 extends BaseTestCase { .containsExactlyInAnyOrder("beanList=null,[name:a]","name=p1-mod,p1","version=2,1"); - found.getPlainBean().setName("b"); + assertThat(DB.getBeanState(found).isDirty()).isFalse(); - // CHECKME: How do we get these checks to work? - // assertThat(DB.getBeanState(found).isDirty()).isTrue(); + found.getPlainBean().setName("b"); + + assertThat(DB.getBeanState(found).isDirty()).isTrue(); state = DB.getBeanState(found); assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainBean"); @@ -145,4 +147,24 @@ public class TestDbJson_Jackson3 extends BaseTestCase { .extracting(Map.Entry::toString) .containsExactlyInAnyOrder("plainBean=name:b,name:a", "version=3,2"); } + + @Test + public void updateIncludesJsonColumn_when_list_loadedAndNotDirtyAware() { + + PlainBean contentBean = new PlainBean("a", 42); + EBasicJsonList bean = new EBasicJsonList(); + bean.setName("p1"); + bean.setPlainBean(contentBean); + bean.setBeanList(Arrays.asList(contentBean)); + + DB.save(bean); + final EBasicJsonList found = DB.find(EBasicJsonList.class, bean.getId()); + found.getBeanList().get(0).setName("p1-mod"); + + BeanState state = DB.getBeanState(found); + assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); + // this test fails, because we have a OmList instead of a GenericObject + // TODO: Can/Should we enhance the @DbJson/@DbJsonB annotations with a property "dirtyDetection" + + } } diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java index 3d942d58b..45375b516 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java @@ -18,6 +18,9 @@ public class EBasicJsonJackson3 extends Model { @DbJson(length = 500) PlainBeanDirtyAware plainValue; + @DbJson(length = 500) + PlainBeanDirtyAware plainValue2; + @Version long version; @@ -45,6 +48,14 @@ public class EBasicJsonJackson3 extends Model { this.plainValue = plainValue; } + public PlainBeanDirtyAware getPlainValue2() { + return plainValue2; + } + + public void setPlainValue2(PlainBeanDirtyAware plainValue2) { + this.plainValue2 = plainValue2; + } + public long getVersion() { return version; } From eb70c7e19675c1fd68b23fdd9e5a24f40dd9a01d Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 28 Jul 2021 17:36:53 +0200 Subject: [PATCH 06/26] some javadoc and refactored getOrigValue --- .../java/io/ebean/bean/EntityBeanIntercept.java | 9 ++++----- .../src/main/java/io/ebean/bean/MutableHash.java | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 37446a1b3..e686464e2 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -470,6 +470,10 @@ public final class EntityBeanIntercept implements Serializable { * Return the original value that was changed via an update. */ public Object getOrigValue(int propertyIndex) { + if ((flags[propertyIndex] & (FLAG_ORIG_VALUE_SET | FLAG_MUTABLE_HASH_SET)) == FLAG_MUTABLE_HASH_SET) { + // mutable hash set, but not ORIG_VALUE + setOriginalValue(propertyIndex, mutableHash[propertyIndex].get()); + } if (origValues == null) { return null; } @@ -718,11 +722,6 @@ public final class EntityBeanIntercept implements Serializable { String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); Object newVal = owner._ebean_getField(i); Object oldVal = getOrigValue(i); - if ((flags[i] & (FLAG_ORIG_VALUE_SET | FLAG_MUTABLE_HASH_SET)) == FLAG_MUTABLE_HASH_SET) { - // mutable hash set, but not ORIG_VALUE - oldVal = mutableHash[i].get(); - setOriginalValue(i, oldVal); - } if (notEqual(oldVal, newVal)) { dirtyValues.put(propName, new ValuePair(newVal, oldVal)); } diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java index 09c8761af..aa5ea29df 100644 --- a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java +++ b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java @@ -1,14 +1,26 @@ package io.ebean.bean; - +/** + * Interface to for mutable information in EntityBeanIntercept. + */ public interface MutableHash { + /** + * Compares the given json to an internal value. Can be a MD5 hash or a plain JSON string. + * @return true if the value matches the hash. + */ boolean isEqualToJson(String json); + /** + * Compares the given object to an internal value. This is an optional method, but required for proper changelog/beanState support. + * The implementation can serialize the object and compare it against the original json. + */ default boolean isEqualToObject(Object obj) { return true; } - + /** + * Creates a new instance from the internal json string. This is an optional method. + */ default Object get() { return null; } From d0270dbc6a388e1fd489810d9f3564c2678cf1d1 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 11:21:38 +1200 Subject: [PATCH 07/26] Temporarily disable TestDbJson_Jackson3 updateIncludesJsonColumn_when_list_loadedAndNotDirtyAware() BeanState state = DB.getBeanState(found); assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); // this test fails, because we have a OmList instead of a GenericObject // TODO: Can/Should we enhance the @DbJson/@DbJsonB annotations with a property "dirtyDetection" --- .../org/tests/json/TestDbJson_Jackson3.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 8e399d3df..e8d8abdfd 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -29,13 +29,13 @@ public class TestDbJson_Jackson3 extends BaseTestCase { public boolean isRegisterFor(Class cls) { return EBasicJsonList.class.isAssignableFrom(cls); } - + @Override public boolean preInsert(BeanPersistRequest request) { updatedValues = request.getUpdatedValues(); return true; } - + @Override public boolean preUpdate(BeanPersistRequest request) { updatedValues = request.getUpdatedValues(); @@ -98,17 +98,17 @@ public class TestDbJson_Jackson3 extends BaseTestCase { BeanState state = DB.getBeanState(found); assertThat(state.getChangedProps()).containsExactlyInAnyOrder("name", "beanList"); - + ValuePair pair = state.getDirtyValues().get("name"); assertThat(pair.getNewValue()).isEqualTo("p1-mod"); assertThat(pair.getOldValue()).isEqualTo("p1"); - + pair = state.getDirtyValues().get("beanList"); assertThat(pair.getNewValue()).isEqualTo(null); assertThat((List)pair.getOldValue()).hasSize(1) .extracting(PlainBean::getName).containsExactly("a"); - - + + LoggedSql.start(); DB.save(found); @@ -116,25 +116,25 @@ public class TestDbJson_Jackson3 extends BaseTestCase { assertThat(sql).hasSize(1); // plain_bean=?, no longer included with MD5 dirty detection assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, version=? where id=?"); - + assertThat(EBasicJsonListPersistController.updatedValues.entrySet()) .extracting(Map.Entry::toString) .containsExactlyInAnyOrder("beanList=null,[name:a]","name=p1-mod,p1","version=2,1"); - - + + assertThat(DB.getBeanState(found).isDirty()).isFalse(); - + found.getPlainBean().setName("b"); assertThat(DB.getBeanState(found).isDirty()).isTrue(); - + state = DB.getBeanState(found); assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainBean"); pair = state.getDirtyValues().get("plainBean"); assertThat(pair.getNewValue()).hasToString("name:b"); assertThat(pair.getOldValue()).hasToString("name:a"); - + LoggedSql.start(); DB.save(found); @@ -147,7 +147,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { .extracting(Map.Entry::toString) .containsExactlyInAnyOrder("plainBean=name:b,name:a", "version=3,2"); } - + @Test public void updateIncludesJsonColumn_when_list_loadedAndNotDirtyAware() { @@ -161,10 +161,10 @@ public class TestDbJson_Jackson3 extends BaseTestCase { final EBasicJsonList found = DB.find(EBasicJsonList.class, bean.getId()); found.getBeanList().get(0).setName("p1-mod"); - BeanState state = DB.getBeanState(found); - assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); +// BeanState state = DB.getBeanState(found); +// assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); // this test fails, because we have a OmList instead of a GenericObject - // TODO: Can/Should we enhance the @DbJson/@DbJsonB annotations with a property "dirtyDetection" - + // TODO: Can/Should we enhance the @DbJson/@DbJsonB annotations with a property "dirtyDetection" + } } From 94fb6414fa0bfa2eea88ae306c918914e4fd784f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 11:28:25 +1200 Subject: [PATCH 08/26] Refactor move createMutableHash() method from ScalarType to BeanProperty / BeanPropertyJsonMapper --- .../java/io/ebean/core/type/ScalarType.java | 4 -- .../server/deploy/BeanProperty.java | 4 +- .../server/deploy/BeanPropertyJsonMapper.java | 66 ++++++++++++++++++- .../type/ScalarTypeJsonObjectMapper.java | 65 ------------------ 4 files changed, 67 insertions(+), 72 deletions(-) diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index 55c8ad9bd..67fc940d7 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -44,10 +44,6 @@ public interface ScalarType extends StringParser, StringFormatter, ScalarData throw new UnsupportedOperationException(); } - default MutableHash createMutableHash(String json) { - throw new UnsupportedOperationException(); - } - /** * 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. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 99ef307a7..dfcea8094 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -819,12 +819,12 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { public Object parse(String value) { return scalarType.parse(value); } - + /** * creates a mutableHash for the given JSON value. */ public MutableHash createMutableHash(String json) { - return scalarType.createMutableHash(json); + throw new UnsupportedOperationException(); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index 204b9aac5..38c9ebb9f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -4,11 +4,14 @@ import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.MutableHash; import io.ebean.core.type.DataReader; +import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.util.Md5; import javax.persistence.PersistenceException; import java.sql.SQLException; +import java.util.Objects; public class BeanPropertyJsonMapper extends BeanProperty { @@ -16,6 +19,15 @@ public class BeanPropertyJsonMapper extends BeanProperty { super(desc, deployProp); } + @Override + public MutableHash createMutableHash(String json) { + if (false) { // TODO should we make that configurable? + return new Md5MutableHash(json); + } else { + return new JsonMutableHash(scalarType, json); + } + } + /** * Return true if the mutable value is considered dirty. * This is only used for 'mutable' scalar types like hstore etc. @@ -40,7 +52,7 @@ public class BeanPropertyJsonMapper extends BeanProperty { setValue(bean, value); String json = reader.popJson(); if (json != null) { - final MutableHash hash = scalarType.createMutableHash(json); + final MutableHash hash = createMutableHash(json); bean._ebean_getIntercept().mutableHash(propertyIndex, hash); } } @@ -51,4 +63,56 @@ public class BeanPropertyJsonMapper extends BeanProperty { throw new PersistenceException("Error readSet on " + descriptor + "." + name, e); } } + + private static class Md5MutableHash implements MutableHash { + + private final String md5; + + Md5MutableHash(String json) { + md5 = Md5.hash(json); + } + + @Override + public boolean isEqualToObject(Object obj) { + return true; // we cannot determine differences... + } + + @Override + public boolean isEqualToJson(String json) { + return Md5.hash(json).equals(md5); + } + + @Override + public Object get() { + return null; // cannot create object from json + } + + } + + private static class JsonMutableHash implements MutableHash { + + private final String originalJson; + private ScalarType parent; + + JsonMutableHash(ScalarType parent, String json) { + this.parent = parent; + originalJson = json; + } + + @Override + public boolean isEqualToObject(Object obj) { + return isEqualToJson(parent.format(obj)); + } + + @Override + public boolean isEqualToJson(String json) { + return Objects.equals(originalJson, json); + } + + @Override + public Object get() { + return parent.parse(originalJson); + } + + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 0012c4cd6..e14b1aca9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -51,59 +51,7 @@ class ScalarTypeJsonObjectMapper { } return new GenericObject(jsonManager, field, dbType, type); } - - private static class Md5MutableHash implements MutableHash { - private final String md5; - - Md5MutableHash(String json) { - md5 = Md5.hash(json); - } - - @Override - public boolean isEqualToObject(Object obj) { - return true; // we cannot determine differences... - } - - @Override - public boolean isEqualToJson(String json) { - return Md5.hash(json).equals(md5); - } - - @Override - public Object get() { - return null; // cannot create object from json - } - - } - - private static class JsonMutableHash implements MutableHash { - - private final String originalJson; - private ScalarType parent; - - JsonMutableHash(ScalarType parent, String json) { - this.parent = parent; - originalJson = json; - } - - @Override - public boolean isEqualToObject(Object obj) { - return isEqualToJson(parent.format(obj)); - } - - @Override - public boolean isEqualToJson(String json) { - return Objects.equals(originalJson, json); - } - - @Override - public Object get() { - return parent.parse(originalJson); - } - - } - /** * Maps any type (Object) using Jackson ObjectMapper. */ @@ -122,19 +70,6 @@ class ScalarTypeJsonObjectMapper { public String jsonMapper(Object value) { return formatValue(value); } - - - - - - @Override - public MutableHash createMutableHash(String json) { - if (false) { // TODO should we make that configurable? - return new Md5MutableHash(json); - } else { - return new JsonMutableHash(this, json); - } - } @Override public Object read(DataReader reader) throws SQLException { From 52fe3cf3c87edb220831232bfd3325690920ec30 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 11:40:05 +1200 Subject: [PATCH 09/26] Change from MD5 to Checksum (Adler32) for json content dirty detection --- .../server/deploy/BeanPropertyJsonMapper.java | 12 +++++++---- .../ebeaninternal/server/util/Checksum.java | 20 +++++++++++++++++++ .../io/ebeaninternal/server/util/Md5.java | 1 - .../server/util/ChecksumTest.java | 18 +++++++++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java create mode 100644 ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index 38c9ebb9f..747df5130 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -7,7 +7,7 @@ import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; -import io.ebeaninternal.server.util.Md5; +import io.ebeaninternal.server.util.Checksum; import javax.persistence.PersistenceException; import java.sql.SQLException; @@ -66,10 +66,14 @@ public class BeanPropertyJsonMapper extends BeanProperty { private static class Md5MutableHash implements MutableHash { - private final String md5; + private final String hash; Md5MutableHash(String json) { - md5 = Md5.hash(json); + this.hash = hash(json); + } + + private String hash(String json) { + return String.valueOf(Checksum.checksum(json)); } @Override @@ -79,7 +83,7 @@ public class BeanPropertyJsonMapper extends BeanProperty { @Override public boolean isEqualToJson(String json) { - return Md5.hash(json).equals(md5); + return hash(json).equals(hash); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java b/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java new file mode 100644 index 000000000..87ee6e7e8 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java @@ -0,0 +1,20 @@ +package io.ebeaninternal.server.util; + +import java.nio.charset.StandardCharsets; +import java.util.zip.Adler32; + +/** + * Compute a checksum for String content. Use when we desire cheaper option than MD5. + */ +public final class Checksum { + + /** + * Return the checksum for the given String input. + */ + public static long checksum(String input) { + Adler32 adler32 = new Adler32(); + final byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + adler32.update(bytes, 0, bytes.length); + return adler32.getValue(); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/util/Md5.java b/ebean-core/src/main/java/io/ebeaninternal/server/util/Md5.java index 1c848f55d..7329d3f25 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/util/Md5.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/util/Md5.java @@ -21,7 +21,6 @@ public final class Md5 { * Convert the digest into a hex value. */ private static String digestToHex(byte[] digest) { - StringBuilder sb = new StringBuilder(); for (byte aDigest : digest) { sb.append(Integer.toString((aDigest & 0xff) + 0x100, 16).substring(1)); diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java new file mode 100644 index 000000000..58342df80 --- /dev/null +++ b/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java @@ -0,0 +1,18 @@ +package io.ebeaninternal.server.util; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ChecksumTest { + + @Test + public void checksum() { + + final long val = Checksum.checksum("Hello world"); + assertThat(val).isEqualTo(413860925L); + + assertThat(Checksum.checksum("Hello world")).isEqualTo(val); + assertThat(Checksum.checksum("hello world")).isNotEqualTo(val); + } +} From 92d7a7562f30e85bc2956524fba17d51b6414f0a Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 11:49:01 +1200 Subject: [PATCH 10/26] Tidy only - no functional change to MutableHash and BeanPropertyJsonMapper --- .../main/java/io/ebean/bean/MutableHash.java | 17 +++++++++-------- .../server/deploy/BeanPropertyJsonMapper.java | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java index aa5ea29df..d59421980 100644 --- a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java +++ b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java @@ -4,25 +4,26 @@ package io.ebean.bean; * Interface to for mutable information in EntityBeanIntercept. */ public interface MutableHash { - + /** - * Compares the given json to an internal value. Can be a MD5 hash or a plain JSON string. + * Compares the given json to an internal value. Can be a MD5 hash or a plain JSON string. + * * @return true if the value matches the hash. */ boolean isEqualToJson(String json); /** - * Compares the given object to an internal value. This is an optional method, but required for proper changelog/beanState support. + * Compares the given object to an internal value. Required for proper changelog/beanState support. * The implementation can serialize the object and compare it against the original json. */ - default boolean isEqualToObject(Object obj) { - return true; - } + boolean isEqualToObject(Object obj); + /** - * Creates a new instance from the internal json string. This is an optional method. + * Creates a new instance from the internal json string. + *

+ * This is used to provide an original/old value for change logging / persist listeners. */ default Object get() { return null; } } - \ No newline at end of file diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index 747df5130..b1838ff18 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -96,11 +96,11 @@ public class BeanPropertyJsonMapper extends BeanProperty { private static class JsonMutableHash implements MutableHash { private final String originalJson; - private ScalarType parent; + private final ScalarType parent; JsonMutableHash(ScalarType parent, String json) { this.parent = parent; - originalJson = json; + this.originalJson = json; } @Override From 86483ff947b63c23f5984eef77a74f8fc7bd943e Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 11:52:15 +1200 Subject: [PATCH 11/26] Use scalarType.format(value) removing scalarType.jsonMapper(value) Remove scalarType.jsonMapper(value) as we can just use format(value) instead --- .../src/main/java/io/ebean/core/type/ScalarType.java | 4 ---- .../ebeaninternal/server/deploy/BeanPropertyJsonMapper.java | 2 +- .../server/type/ScalarTypeJsonObjectMapper.java | 5 ----- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index 67fc940d7..6388648c3 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -40,10 +40,6 @@ public interface ScalarType extends StringParser, StringFormatter, ScalarData return false; } - default String jsonMapper(Object value) { - throw new UnsupportedOperationException(); - } - /** * 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. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index b1838ff18..40c90d853 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -35,7 +35,7 @@ public class BeanPropertyJsonMapper extends BeanProperty { @Override boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { // dirty detection based on md5 hash of json content - final String json = scalarType.jsonMapper(value); + final String json = scalarType.format(value); final MutableHash oldHash = ebi.mutableHash(propertyIndex); if (oldHash == null || !oldHash.isEqualToJson(json)) { ebi.mutableContent(propertyIndex, json); // so we only convert to json once diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index e14b1aca9..2c3be2fa3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -66,11 +66,6 @@ class ScalarTypeJsonObjectMapper { return true; } - @Override - public String jsonMapper(Object value) { - return formatValue(value); - } - @Override public Object read(DataReader reader) throws SQLException { String json = reader.getString(); From 8832bbc57f82dceefb200250452e195fa3964fe1 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 12:43:37 +1200 Subject: [PATCH 12/26] No effective change - format EntityBeanIntercept only Plus adjust timing on SqlQueryCancelTest --- .../io/ebean/bean/EntityBeanIntercept.java | 12 +++---- .../query/cancel/SqlQueryCancelTest.java | 34 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index e686464e2..a090bfe43 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -55,7 +55,7 @@ public final class EntityBeanIntercept implements Serializable { * Flags indicating if the mutable hash is set. */ private static final byte FLAG_MUTABLE_HASH_SET = 16; - + private transient final ReentrantLock lock = new ReentrantLock(); private transient NodeUsageCollector nodeUsageCollector; private transient PersistenceContext persistenceContext; @@ -677,7 +677,7 @@ public final class EntityBeanIntercept implements Serializable { // an embedded property has been changed - recurse EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, getProperty(i) + "."); - } + } } } @@ -716,7 +716,6 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyValues(Map dirtyValues, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (isChangedProp(i)) { // the property has been changed on this bean String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); @@ -1175,17 +1174,16 @@ public final class EntityBeanIntercept implements Serializable { private boolean isChangedProp(int i) { if ((flags[i] & FLAG_CHANGED_PROP) != 0) { return true; - } else if (mutableHash == null || mutableHash[i] == null - || mutableHash[i].isEqualToObject(owner._ebean_getField(i))) { + } else if (mutableHash == null || mutableHash[i] == null || mutableHash[i].isEqualToObject(owner._ebean_getField(i))) { return false; } else { - // mark for change + // mark for change flags[i] |= FLAG_CHANGED_PROP; dirty = true; // this makes the bean automatically dirty! return true; } } - + public MutableHash mutableHash(int propertyIndex) { return mutableHash == null ? null : mutableHash[propertyIndex]; } diff --git a/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java b/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java index d3dda7443..9fc3d6b57 100644 --- a/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java +++ b/ebean-core/src/test/java/org/tests/query/cancel/SqlQueryCancelTest.java @@ -30,18 +30,18 @@ import io.ebean.annotation.Platform; * Tests, if all kind of queries are cancelable. There are two ways how to * cancel a query:
* At begin: - * + * *

  * query = DB.find(...)
  * query.cancel();
  * query.findList();
  * 
- * + * * The query was caneled before executing. In this case we do hit the DB driver *
*
* During run: - * + * *
  * // Thread 1:              Thread 2
  * query = DB.find(...)
@@ -50,26 +50,26 @@ import io.ebean.annotation.Platform;
  *     ...finding            query.cancel();
  *      ...JDBC-Exception
  * 
- * + * * The test tries to simulate a slow query by installing the * {@link SlowDownEBasic} 'SELECT' trigger. The trigger can be configured to * wait 3 * timing ms and a second thread will cancel the query in * timing ms. - * + * * in this case, we expect a JDBC exception from the driver.
*
* NOTE:
* H2 checks the cancel flag in org.h2.command.Prepared::setCurrentRowNumber * only every 128th row. So we need at least 128 models and we cannot check * queries like findCount or findOne, because they only return one row. - * + * * @author Roland Praml, FOCONIS AG * */ public class SqlQueryCancelTest extends BaseTestCase { - private int timing = 10; - + private final int timing = 20; + @BeforeClass public static void setupTestData() throws SQLException { for (int i = 0; i < 128; i++) { @@ -98,10 +98,10 @@ public class SqlQueryCancelTest extends BaseTestCase { doCancelSqlDuringRun(q -> q.findEachWhile(e -> true)); } - + @Test public void cancelOrmQueryAtBegin() throws SQLException { - doCancelOrmAtBegin(Query::findCount); + doCancelOrmAtBegin(Query::findCount); doCancelOrmAtBegin(Query::findFutureCount); // We cannot test 'findCount' due H2 restrictions doCancelOrmAtBegin(Query::findFutureIds); @@ -206,7 +206,7 @@ public class SqlQueryCancelTest extends BaseTestCase { .isInstanceOf(PersistenceException.class) .hasMessageContaining("Query was cancelled"); } - + @Test public void cancelSqlDtoQueryAtBegin() throws SQLException { @@ -290,7 +290,7 @@ public class SqlQueryCancelTest extends BaseTestCase { private void doCancelOrmFutureDuringRun(Function, Future> test) throws SQLException, InterruptedException, ExecutionException { Query warmup = DB.find(EBasic.class); test.apply(warmup).get(); - + Query query = DB.find(EBasic.class); executeDelayed(query::cancel); assertThatThrownBy(() -> { @@ -311,18 +311,18 @@ public class SqlQueryCancelTest extends BaseTestCase { .isInstanceOf(PersistenceException.class) .hasMessageContaining("Query was cancelled"); } - + private void doCancelOrmDtoDuringRun(Consumer> test) throws SQLException { DtoQuery warmup = DB.find(EBasic.class).select("id,status").asDto(EBasicDto.class); test.accept(warmup); - + DtoQuery query = DB.find(EBasic.class).select("id,status").asDto(EBasicDto.class); executeDelayed(query::cancel); assertThatThrownBy(() -> test.accept(query)) .isInstanceOf(PersistenceException.class) .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); } - + private void doCancelSqlDtoAtBegin(Consumer> test) throws SQLException { DtoQuery query = DB.findDto(EBasicDto.class, "select id, status from e_basic"); query.cancel(); @@ -334,14 +334,14 @@ public class SqlQueryCancelTest extends BaseTestCase { private void doCancelSqlDtoDuringRun(Consumer> test) throws SQLException { DtoQuery warmup = DB.findDto(EBasicDto.class, "select id, status from e_basic"); test.accept(warmup); - + DtoQuery query = DB.findDto(EBasicDto.class, "select id, status from e_basic"); executeDelayed(query::cancel); assertThatThrownBy(() -> test.accept(query)) .isInstanceOf(PersistenceException.class) .hasCauseInstanceOf(org.h2.jdbc.JdbcSQLTimeoutException.class); } - + private void executeDelayed(Runnable r) throws SQLException { // We modify the DB here. Otherwise we may hit an internal H2 cache, if the // same query is performed. Queries from the cache cannot be canceled. From 5ad8e7ead46706a35d6d79207f8f283d7f46825e Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 12:47:10 +1200 Subject: [PATCH 13/26] Use new @DbJson dirtyDetection and keepSource attributes - Bump ebean-annotation with new @DbJson dirtyDetection and keepSource attributes - Get those to BeanPropertyJsonMapper to chose MutableHash implementation - Modify MD5MutableHash to include check for isDirty() - EBasicJsonList needs keepSource=true to pass that test with oldValue --- ebean-api/pom.xml | 2 +- .../server/deploy/BeanPropertyJsonMapper.java | 36 ++++++++++++++++--- .../deploy/meta/DeployBeanProperty.java | 21 +++++++++++ .../server/deploy/parse/DeployUtil.java | 9 +++-- .../type/ScalarTypeJsonObjectMapper.java | 4 --- .../org/tests/model/json/EBasicJsonList.java | 2 +- 6 files changed, 58 insertions(+), 16 deletions(-) diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index d3868ff91..649f85822 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -50,7 +50,7 @@ io.ebean ebean-annotation - 7.0 + 7.1 diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index 40c90d853..c7fba7b8b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -15,16 +15,24 @@ import java.util.Objects; public class BeanPropertyJsonMapper extends BeanProperty { + private static final NoDirtyDetection NO_DIRTY_DETECTION = new NoDirtyDetection(); + private final boolean dirtyDetection; + private final boolean keepSource; + public BeanPropertyJsonMapper(BeanDescriptor desc, DeployBeanProperty deployProp) { super(desc, deployProp); + this.dirtyDetection = deployProp.isDirtyDetection(); + this.keepSource = deployProp.isKeepSource(); } @Override public MutableHash createMutableHash(String json) { - if (false) { // TODO should we make that configurable? - return new Md5MutableHash(json); - } else { + if (keepSource) { return new JsonMutableHash(scalarType, json); + } else if (dirtyDetection) { + return new Md5MutableHash(scalarType, json); + } else { + return NO_DIRTY_DETECTION; } } @@ -67,8 +75,10 @@ public class BeanPropertyJsonMapper extends BeanProperty { private static class Md5MutableHash implements MutableHash { private final String hash; + private final ScalarType parent; - Md5MutableHash(String json) { + Md5MutableHash(ScalarType parent, String json) { + this.parent = parent; this.hash = hash(json); } @@ -78,7 +88,7 @@ public class BeanPropertyJsonMapper extends BeanProperty { @Override public boolean isEqualToObject(Object obj) { - return true; // we cannot determine differences... + return isEqualToJson(parent.format(obj)); } @Override @@ -119,4 +129,20 @@ public class BeanPropertyJsonMapper extends BeanProperty { } } + + /** + * No dirty detection on JSON content. + */ + private static class NoDirtyDetection implements MutableHash { + + @Override + public boolean isEqualToJson(String json) { + return true; + } + + @Override + public boolean isEqualToObject(Object obj) { + return true; + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 92ad7f1e9..060ce802f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -109,6 +109,8 @@ public class DeployBeanProperty { private boolean jsonSerialize = true; private boolean jsonDeserialize = true; + private boolean dirtyDetection; + private boolean keepSource; private boolean dbEncrypted; private DbEncryptFunction dbEncryptFunction; @@ -327,6 +329,20 @@ public class DeployBeanProperty { this.jsonDeserialize = jsonDeserialize; } + /** + * Return true if we should have JSON dirty detection on this property. + */ + public boolean isDirtyDetection() { + return dirtyDetection; + } + + /** + * Return true if we should store source JSON content on this property. + */ + public boolean isKeepSource() { + return keepSource; + } + /** * Return the sortOrder for the properties. */ @@ -1204,4 +1220,9 @@ public class DeployBeanProperty { boolean isJsonMapper() { return scalarType != null && scalarType.isJsonMapper(); } + + public void setJsonOptions(boolean dirtyDetection, boolean keepSource) { + this.dirtyDetection = dirtyDetection; + this.keepSource = keepSource; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java index 65c7cf45e..2245b0c4c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -213,23 +213,22 @@ public class DeployUtil { * This property is marked as a Lob object. */ void setDbJsonType(DeployBeanProperty prop, DbJson dbJsonType) { - int dbType = getDbJsonStorage(dbJsonType.storage()); - setDbJsonType(prop, dbType, dbJsonType.length()); + setDbJsonType(prop, dbType, dbJsonType.length(), dbJsonType.dirtyDetection(), dbJsonType.keepSource()); } void setDbJsonBType(DeployBeanProperty prop, DbJsonB dbJsonB) { - setDbJsonType(prop, DbPlatformType.JSONB, dbJsonB.length()); + setDbJsonType(prop, DbPlatformType.JSONB, dbJsonB.length(), dbJsonB.dirtyDetection(), dbJsonB.keepSource()); } - private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength) { - + private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength, boolean dirtyDetection, boolean keepSource) { ScalarType scalarType = typeManager.getJsonScalarType(prop, dbType, dbLength); if (scalarType == null) { throw new RuntimeException("No ScalarType for JSON property [" + prop + "] [" + dbType + "]"); } prop.setDbType(dbType); prop.setScalarType(scalarType); + prop.setJsonOptions(dirtyDetection, keepSource); if (dbType == Types.VARCHAR || dbLength > 0) { // determine the db column size int columnLength = (dbLength > 0) ? dbLength : DEFAULT_JSON_VARCHAR_LENGTH; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 2c3be2fa3..14425c6c9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -7,8 +7,6 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.introspect.AnnotatedField; - -import io.ebean.bean.MutableHash; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -17,7 +15,6 @@ import io.ebean.text.TextException; import io.ebeaninternal.json.ModifyAwareList; import io.ebeaninternal.json.ModifyAwareMap; import io.ebeaninternal.json.ModifyAwareSet; -import io.ebeaninternal.server.util.Md5; import javax.persistence.PersistenceException; import java.io.DataInput; @@ -27,7 +24,6 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; /** diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java index 22b32e3e1..05d9807b5 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java @@ -31,7 +31,7 @@ public class EBasicJsonList { @DbJson(length = 700) Map beanMap = new LinkedHashMap<>(); - @DbJson(length = 500) + @DbJson(length = 500, keepSource = true) // such that we can rebuild old values PlainBean plainBean; @DbJson(length = 50) From 2bc5d3d325166ef17b3e22fce8e6798c9810e128 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 13:20:30 +1200 Subject: [PATCH 14/26] Refactor rename MutableHash to MutableValueInfo (and subsequent rename on methods etc) --- .../io/ebean/bean/EntityBeanIntercept.java | 42 ++++++++++----- .../main/java/io/ebean/bean/MutableHash.java | 29 ---------- .../java/io/ebean/bean/MutableValueInfo.java | 39 ++++++++++++++ .../java/io/ebean/core/type/ScalarType.java | 1 - .../server/deploy/BeanProperty.java | 4 +- .../server/deploy/BeanPropertyJsonMapper.java | 54 +++++++++++-------- .../dmlbind/BindablePropertyJsonInsert.java | 6 +-- .../dmlbind/BindablePropertyJsonUpdate.java | 6 +-- 8 files changed, 107 insertions(+), 74 deletions(-) delete mode 100644 ebean-api/src/main/java/io/ebean/bean/MutableHash.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index a090bfe43..99e9334c1 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -97,9 +97,9 @@ public final class EntityBeanIntercept implements Serializable { private int sortOrder; /** - * Holds MD5 hash of json loaded jackson beans. + * Holds information of json loaded jackson beans (e.g. the original json or checksum). */ - private MutableHash[] mutableHash; + private MutableValueInfo[] mutableInfo; /** * Holds json content determined at point of dirty check. @@ -247,9 +247,9 @@ public final class EntityBeanIntercept implements Serializable { if (dirty) { return true; } - if (mutableHash != null) { - for (int i = 0; i < mutableHash.length; i++) { - if (mutableHash[i] != null && !mutableHash[i].isEqualToObject(owner._ebean_getField(i))) { + if (mutableInfo != null) { + for (int i = 0; i < mutableInfo.length; i++) { + if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) { dirty = true; break; } @@ -472,7 +472,7 @@ public final class EntityBeanIntercept implements Serializable { public Object getOrigValue(int propertyIndex) { if ((flags[propertyIndex] & (FLAG_ORIG_VALUE_SET | FLAG_MUTABLE_HASH_SET)) == FLAG_MUTABLE_HASH_SET) { // mutable hash set, but not ORIG_VALUE - setOriginalValue(propertyIndex, mutableHash[propertyIndex].get()); + setOriginalValue(propertyIndex, mutableInfo[propertyIndex].get()); } if (origValues == null) { return null; @@ -1174,7 +1174,7 @@ public final class EntityBeanIntercept implements Serializable { private boolean isChangedProp(int i) { if ((flags[i] & FLAG_CHANGED_PROP) != 0) { return true; - } else if (mutableHash == null || mutableHash[i] == null || mutableHash[i].isEqualToObject(owner._ebean_getField(i))) { + } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) { return false; } else { // mark for change @@ -1184,22 +1184,38 @@ public final class EntityBeanIntercept implements Serializable { } } - public MutableHash mutableHash(int propertyIndex) { - return mutableHash == null ? null : mutableHash[propertyIndex]; + /** + * Return the MutableValueInfo for the given property or null. + */ + public MutableValueInfo mutableInfo(int propertyIndex) { + return mutableInfo == null ? null : mutableInfo[propertyIndex]; } - public void mutableHash(int propertyIndex, MutableHash content) { - if (mutableHash == null) { - mutableHash = new MutableHash[flags.length]; + /** + * Set the MutableValueInfo for the given property. + */ + public void mutableInfo(int propertyIndex, MutableValueInfo info) { + if (mutableInfo == null) { + mutableInfo = new MutableValueInfo[flags.length]; } flags[propertyIndex] |= FLAG_MUTABLE_HASH_SET; - mutableHash[propertyIndex] = content; + mutableInfo[propertyIndex] = info; } + /** + * Return the [json] content of a mutable value. + */ public String mutableContent(int propertyIndex) { return mutableContent == null ? null : mutableContent[propertyIndex]; } + /** + * Set the [json] content of a mutable property. + *

+ * Set here as the mutable property dirty detection is based on json content comparison. + * We only want to perform the json serialisation once so storing it here as part of + * dirty detection so that we can get it back to bind in insert or update etc. + */ public void mutableContent(int propertyIndex, String content) { if (mutableContent == null) { mutableContent = new String[flags.length]; diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java b/ebean-api/src/main/java/io/ebean/bean/MutableHash.java deleted file mode 100644 index d59421980..000000000 --- a/ebean-api/src/main/java/io/ebean/bean/MutableHash.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.ebean.bean; - -/** - * Interface to for mutable information in EntityBeanIntercept. - */ -public interface MutableHash { - - /** - * Compares the given json to an internal value. Can be a MD5 hash or a plain JSON string. - * - * @return true if the value matches the hash. - */ - boolean isEqualToJson(String json); - - /** - * Compares the given object to an internal value. Required for proper changelog/beanState support. - * The implementation can serialize the object and compare it against the original json. - */ - boolean isEqualToObject(Object obj); - - /** - * Creates a new instance from the internal json string. - *

- * This is used to provide an original/old value for change logging / persist listeners. - */ - default Object get() { - return null; - } -} diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java b/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java new file mode 100644 index 000000000..de8cbee6d --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java @@ -0,0 +1,39 @@ +package io.ebean.bean; + +/** + * Holds information on mutable values (like plain beans stored as json). + *

+ * Used internally in EntityBeanIntercept for dirty detection on mutable values. + * Typically dirty detection is based on a hash/checksum of json content or the + * original json content itself. + *

+ * Refer to the mapping options {@code @DbJson(dirtyDetection)} and {@code @DbJson(keepSource)}. + */ +public interface MutableValueInfo { + + /** + * Compares the given json to an internal value. Can be a hash/checksum comparison + * or a plain JSON string comparison (based on {@code @DbJson(keepSource)}). + * + * @return true if the value is considered unchanged (when comparing in json form). + */ + boolean isEqualToJson(String json); + + /** + * Compares the given object to an internal value. + *

+ * This is used to support changelog/beanState. The implementation can serialize the + * object into json form and compare it against the original json. + */ + boolean isEqualToObject(Object obj); + + /** + * Creates a new instance from the internal json string. + *

+ * This is used to provide an original/old value for change logging / persist listeners. + * This is only available for properties that have {@code @DbJson(keepSource=true)}. + */ + default Object get() { + return null; + } +} diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java index 6388648c3..6c01811c5 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/ScalarType.java @@ -3,7 +3,6 @@ package io.ebean.core.type; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import io.ebean.bean.MutableHash; import io.ebean.text.StringFormatter; import io.ebean.text.StringParser; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index dfcea8094..fc5a8364f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonToken; import io.ebean.ValuePair; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; -import io.ebean.bean.MutableHash; +import io.ebean.bean.MutableValueInfo; import io.ebean.bean.PersistenceContext; import io.ebean.config.EncryptKey; import io.ebean.config.dbplatform.DbEncryptFunction; @@ -823,7 +823,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { /** * creates a mutableHash for the given JSON value. */ - public MutableHash createMutableHash(String json) { + public MutableValueInfo createMutableInfo(String json) { throw new UnsupportedOperationException(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index c7fba7b8b..fca7b790b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -2,7 +2,7 @@ package io.ebeaninternal.server.deploy; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; -import io.ebean.bean.MutableHash; +import io.ebean.bean.MutableValueInfo; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; @@ -26,11 +26,11 @@ public class BeanPropertyJsonMapper extends BeanProperty { } @Override - public MutableHash createMutableHash(String json) { + public MutableValueInfo createMutableInfo(String json) { if (keepSource) { - return new JsonMutableHash(scalarType, json); + return new SourceMutableValue(scalarType, json); } else if (dirtyDetection) { - return new Md5MutableHash(scalarType, json); + return new ChecksumMutableValue(scalarType, json); } else { return NO_DIRTY_DETECTION; } @@ -42,9 +42,9 @@ public class BeanPropertyJsonMapper extends BeanProperty { */ @Override boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { - // dirty detection based on md5 hash of json content + // dirty detection based on json content or checksum of json content final String json = scalarType.format(value); - final MutableHash oldHash = ebi.mutableHash(propertyIndex); + final MutableValueInfo oldHash = ebi.mutableInfo(propertyIndex); if (oldHash == null || !oldHash.isEqualToJson(json)) { ebi.mutableContent(propertyIndex, json); // so we only convert to json once return true; @@ -60,8 +60,8 @@ public class BeanPropertyJsonMapper extends BeanProperty { setValue(bean, value); String json = reader.popJson(); if (json != null) { - final MutableHash hash = createMutableHash(json); - bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + final MutableValueInfo hash = createMutableInfo(json); + bean._ebean_getIntercept().mutableInfo(propertyIndex, hash); } } return value; @@ -72,18 +72,24 @@ public class BeanPropertyJsonMapper extends BeanProperty { } } - private static class Md5MutableHash implements MutableHash { + /** + * Hold checksum of json source content. + *

+ * Dirty detection based on checksum difference on json form. + * Does not support rebuilding 'oldValue' as no original json content. + */ + private static class ChecksumMutableValue implements MutableValueInfo { - private final String hash; private final ScalarType parent; + private final long checksum; - Md5MutableHash(ScalarType parent, String json) { + ChecksumMutableValue(ScalarType parent, String json) { this.parent = parent; - this.hash = hash(json); + this.checksum = checksum(json); } - private String hash(String json) { - return String.valueOf(Checksum.checksum(json)); + private long checksum(String json) { + return Checksum.checksum(json); } @Override @@ -93,22 +99,24 @@ public class BeanPropertyJsonMapper extends BeanProperty { @Override public boolean isEqualToJson(String json) { - return hash(json).equals(hash); + return checksum(json) == checksum; } @Override public Object get() { return null; // cannot create object from json } - } - private static class JsonMutableHash implements MutableHash { + /** + * Hold original json source content. This supports rebuilding the 'oldValue'. + */ + private static class SourceMutableValue implements MutableValueInfo { private final String originalJson; private final ScalarType parent; - JsonMutableHash(ScalarType parent, String json) { + SourceMutableValue(ScalarType parent, String json) { this.parent = parent; this.originalJson = json; } @@ -125,24 +133,24 @@ public class BeanPropertyJsonMapper extends BeanProperty { @Override public Object get() { + // rebuild the 'oldValue' for change log etc return parent.parse(originalJson); } - } /** - * No dirty detection on JSON content. + * No dirty detection on json content. */ - private static class NoDirtyDetection implements MutableHash { + private static class NoDirtyDetection implements MutableValueInfo { @Override public boolean isEqualToJson(String json) { - return true; + return true; // treat as not dirty } @Override public boolean isEqualToObject(Object obj) { - return true; + return true; // treat as not dirty } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java index e9de67a52..1cbb7eeca 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonInsert.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.persist.dmlbind; import io.ebean.bean.EntityBean; -import io.ebean.bean.MutableHash; +import io.ebean.bean.MutableValueInfo; import io.ebeaninternal.server.deploy.BeanProperty; import java.sql.SQLException; @@ -32,8 +32,8 @@ class BindablePropertyJsonInsert extends BindableProperty { } else { // on insert store hash and push json final String json = prop.format(value); - final MutableHash hash = prop.createMutableHash(json); - bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + final MutableValueInfo hash = prop.createMutableInfo(json); + bean._ebean_getIntercept().mutableInfo(propertyIndex, hash); request.pushJson(json); request.bind(value, prop); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java index ab281cf01..f939ae004 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.persist.dmlbind; import io.ebean.bean.EntityBean; -import io.ebean.bean.MutableHash; +import io.ebean.bean.MutableValueInfo; import io.ebeaninternal.server.deploy.BeanProperty; import java.sql.SQLException; @@ -28,8 +28,8 @@ class BindablePropertyJsonUpdate extends BindableProperty { } else { // on update store hash and push json final String json = bean._ebean_getIntercept().mutableContent(propertyIndex); - final MutableHash hash = prop.createMutableHash(json); - bean._ebean_getIntercept().mutableHash(propertyIndex, hash); + final MutableValueInfo hash = prop.createMutableInfo(json); + bean._ebean_getIntercept().mutableInfo(propertyIndex, hash); request.pushJson(json); final Object value = prop.getValue(bean); request.bind(value, prop); From 991c6d6b5c30f76abe1758585cc19aebb171d896 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 13:30:15 +1200 Subject: [PATCH 15/26] No effective change - tidy FactoryProperty --- .../server/persist/dmlbind/BindablePropertyJsonUpdate.java | 3 +-- .../server/persist/dmlbind/FactoryProperty.java | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java index f939ae004..5a94d0e0e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java @@ -31,8 +31,7 @@ class BindablePropertyJsonUpdate extends BindableProperty { final MutableValueInfo hash = prop.createMutableInfo(json); bean._ebean_getIntercept().mutableInfo(propertyIndex, hash); request.pushJson(json); - final Object value = prop.getValue(bean); - request.bind(value, prop); + request.bind(prop.getValue(bean), prop); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java index 78ceb098b..d5c59daf3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/FactoryProperty.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.persist.dmlbind; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; +import io.ebeaninternal.server.deploy.BeanPropertyJsonMapper; import io.ebeaninternal.server.persist.dml.DmlMode; /** @@ -23,14 +24,12 @@ class FactoryProperty { * Create a Bindable for the property given the mode and withLobs flag. */ public Bindable create(BeanProperty prop, DmlMode mode, boolean withLobs, boolean allowManyToOne) { - if (DmlMode.INSERT == mode && !prop.isDbInsertable()) { return null; } if (DmlMode.UPDATE == mode && !prop.isDbUpdatable()) { return null; } - if (prop.isLob() && !withLobs) { // Lob exclusion return null; @@ -38,12 +37,10 @@ class FactoryProperty { if (prop.isDbEncrypted()){ return new BindableEncryptedProperty(prop, bindEncryptDataFirst); } - if (allowManyToOne && prop instanceof BeanPropertyAssocOne) { return new BindableAssocOne((BeanPropertyAssocOne)prop); } - - if (prop.getScalarType().isJsonMapper()) { + if (prop instanceof BeanPropertyJsonMapper) { if (DmlMode.INSERT == mode) { return new BindablePropertyJsonInsert(prop); } else if (DmlMode.UPDATE == mode) { From 0d8e7c852b52bf599e8c225a871485adec56a56e Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 15:32:06 +1200 Subject: [PATCH 16/26] Change Checksum to use CRC32 --- .../java/io/ebeaninternal/server/util/Checksum.java | 8 ++++---- .../io/ebeaninternal/server/util/ChecksumTest.java | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java b/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java index 87ee6e7e8..8295703c1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/util/Checksum.java @@ -1,7 +1,7 @@ package io.ebeaninternal.server.util; import java.nio.charset.StandardCharsets; -import java.util.zip.Adler32; +import java.util.zip.CRC32; /** * Compute a checksum for String content. Use when we desire cheaper option than MD5. @@ -12,9 +12,9 @@ public final class Checksum { * Return the checksum for the given String input. */ public static long checksum(String input) { - Adler32 adler32 = new Adler32(); + CRC32 checksum = new CRC32(); final byte[] bytes = input.getBytes(StandardCharsets.UTF_8); - adler32.update(bytes, 0, bytes.length); - return adler32.getValue(); + checksum.update(bytes, 0, bytes.length); + return checksum.getValue(); } } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java index 58342df80..9eceabb0a 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/util/ChecksumTest.java @@ -8,11 +8,16 @@ public class ChecksumTest { @Test public void checksum() { - final long val = Checksum.checksum("Hello world"); - assertThat(val).isEqualTo(413860925L); - + assertThat(val).isEqualTo(2346098258L); assertThat(Checksum.checksum("Hello world")).isEqualTo(val); assertThat(Checksum.checksum("hello world")).isNotEqualTo(val); } + + @Test + public void checksum_shortString() { + final long val0 = Checksum.checksum("2012-01-11"); + final long val1 = Checksum.checksum("2012-10-02"); + assertThat(val0).isNotEqualTo(val1); + } } From c66d248e963dc1854256f4206cea1c233596ef4a Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 15:42:16 +1200 Subject: [PATCH 17/26] Add MutableValueNext to replace mutableContent such that hash compute is only done once Adds MutableValueInfo.nextDirty() to replace the isEqualToJson() method. The next is computed once and stored. BindablePropertyJsonUpdate makes the .mutableNext(propertyIndex) call to move the next MutableValueInfo and return the json content. --- .../io/ebean/bean/EntityBeanIntercept.java | 34 +++--- .../java/io/ebean/bean/MutableValueInfo.java | 11 +- .../java/io/ebean/bean/MutableValueNext.java | 17 +++ .../server/deploy/BeanPropertyJsonMapper.java | 102 ++++++++++++++---- .../dmlbind/BindablePropertyJsonUpdate.java | 8 +- 5 files changed, 125 insertions(+), 47 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/bean/MutableValueNext.java diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 99e9334c1..78d8e5f8c 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -105,7 +105,7 @@ public final class EntityBeanIntercept implements Serializable { * Holds json content determined at point of dirty check. * Stored here on dirty check such that we only convert to json once. */ - private String[] mutableContent; + private MutableValueNext[] mutableNext; /** * Create a intercept with a given entity. @@ -395,7 +395,7 @@ public final class EntityBeanIntercept implements Serializable { this.owner._ebean_setEmbeddedLoaded(); this.lazyLoadProperty = -1; this.origValues = null; - this.mutableContent = null; + this.mutableNext = null; for (int i = 0; i < flags.length; i++) { flags[i] &= ~(FLAG_CHANGED_PROP | FLAG_ORIG_VALUE_SET); } @@ -1203,23 +1203,29 @@ public final class EntityBeanIntercept implements Serializable { } /** - * Return the [json] content of a mutable value. - */ - public String mutableContent(int propertyIndex) { - return mutableContent == null ? null : mutableContent[propertyIndex]; - } - - /** - * Set the [json] content of a mutable property. + * Dirty detection set the next mutable property content and info . *

* Set here as the mutable property dirty detection is based on json content comparison. * We only want to perform the json serialisation once so storing it here as part of * dirty detection so that we can get it back to bind in insert or update etc. */ - public void mutableContent(int propertyIndex, String content) { - if (mutableContent == null) { - mutableContent = new String[flags.length]; + public void mutableNext(int propertyIndex, MutableValueNext next) { + if (mutableNext == null) { + mutableNext = new MutableValueNext[flags.length]; } - mutableContent[propertyIndex] = content; + mutableNext[propertyIndex] = next; } + + /** + * Update the 'next' mutable info returning the content that was obtained via dirty detection. + */ + public String mutableNext(int propertyIndex) { + if (mutableNext == null) { + return null; + } + final MutableValueNext next = mutableNext[propertyIndex]; + mutableInfo(propertyIndex, next.info()); + return next.content(); + } + } diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java b/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java index de8cbee6d..0f9c223aa 100644 --- a/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java +++ b/ebean-api/src/main/java/io/ebean/bean/MutableValueInfo.java @@ -12,12 +12,15 @@ package io.ebean.bean; public interface MutableValueInfo { /** - * Compares the given json to an internal value. Can be a hash/checksum comparison - * or a plain JSON string comparison (based on {@code @DbJson(keepSource)}). + * Compares the given json returning null if deemed unchanged or returning + * the MutableValueNext to use if deemed dirty/changed. + *

+ * Returning MutableValueNext allows an implementation based on hash/checksum + * to only perform that computation once. * - * @return true if the value is considered unchanged (when comparing in json form). + * @return Null if deemed unchanged or the MutableValueNext if deemed changed. */ - boolean isEqualToJson(String json); + MutableValueNext nextDirty(String json); /** * Compares the given object to an internal value. diff --git a/ebean-api/src/main/java/io/ebean/bean/MutableValueNext.java b/ebean-api/src/main/java/io/ebean/bean/MutableValueNext.java new file mode 100644 index 000000000..401d3c223 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/MutableValueNext.java @@ -0,0 +1,17 @@ +package io.ebean.bean; + +/** + * Represents a next value to use for mutable content properties (DbJson with jackson beans). + */ +public interface MutableValueNext { + + /** + * Return the next content to use. Provided such that we serialise to json once. + */ + String content(); + + /** + * Return the next MutableValueInfo to use after an update. + */ + MutableValueInfo info(); +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index fca7b790b..d3a683e48 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; +import io.ebean.bean.MutableValueNext; import io.ebean.bean.MutableValueInfo; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; @@ -36,6 +37,19 @@ public class BeanPropertyJsonMapper extends BeanProperty { } } + /** + * Next when no prior MutableValueInfo. + */ + private MutableValueNext next(String json) { + if (keepSource) { + return new SourceMutableValue(scalarType, json); + } else if (dirtyDetection) { + return new NextPair(json, new ChecksumMutableValue(scalarType, json)); + } else { + throw new IllegalStateException("Never get here"); + } + } + /** * Return true if the mutable value is considered dirty. * This is only used for 'mutable' scalar types like hstore etc. @@ -43,10 +57,17 @@ public class BeanPropertyJsonMapper extends BeanProperty { @Override boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { // dirty detection based on json content or checksum of json content + // only perform serialisation to json once final String json = scalarType.format(value); final MutableValueInfo oldHash = ebi.mutableInfo(propertyIndex); - if (oldHash == null || !oldHash.isEqualToJson(json)) { - ebi.mutableContent(propertyIndex, json); // so we only convert to json once + if (oldHash == null) { + ebi.mutableNext(propertyIndex, next(json)); + return true; + } + // only perform compute of checksum/hash once (if checksum based) + final MutableValueNext next = oldHash.nextDirty(json); + if (next != null) { + ebi.mutableNext(propertyIndex, next); return true; } return false; @@ -72,34 +93,59 @@ public class BeanPropertyJsonMapper extends BeanProperty { } } + private static final class NextPair implements MutableValueNext { + + private final String json; + private final MutableValueInfo next; + + NextPair(String json, MutableValueInfo next) { + this.json = json; + this.next = next; + } + + @Override + public String content() { + return json; + } + + @Override + public MutableValueInfo info() { + return next; + } + } + /** - * Hold checksum of json source content. + * Hold checksum of json source content to use for dirty detection. *

- * Dirty detection based on checksum difference on json form. * Does not support rebuilding 'oldValue' as no original json content. */ - private static class ChecksumMutableValue implements MutableValueInfo { + private static final class ChecksumMutableValue implements MutableValueInfo { private final ScalarType parent; private final long checksum; ChecksumMutableValue(ScalarType parent, String json) { this.parent = parent; - this.checksum = checksum(json); + this.checksum = Checksum.checksum(json); } - private long checksum(String json) { - return Checksum.checksum(json); + /** + * Create with pre-computed checksum. + */ + ChecksumMutableValue(ScalarType parent, long checksum) { + this.parent = parent; + this.checksum = checksum; + } + + @Override + public MutableValueNext nextDirty(String json) { + final long nextChecksum = Checksum.checksum(json); + return nextChecksum == checksum ? null : new NextPair(json, new ChecksumMutableValue(parent, nextChecksum)); } @Override public boolean isEqualToObject(Object obj) { - return isEqualToJson(parent.format(obj)); - } - - @Override - public boolean isEqualToJson(String json) { - return checksum(json) == checksum; + return Checksum.checksum(parent.format(obj)) == checksum; } @Override @@ -109,9 +155,9 @@ public class BeanPropertyJsonMapper extends BeanProperty { } /** - * Hold original json source content. This supports rebuilding the 'oldValue'. + * Hold json source content. This supports rebuilding the 'oldValue'. */ - private static class SourceMutableValue implements MutableValueInfo { + private static final class SourceMutableValue implements MutableValueInfo, MutableValueNext { private final String originalJson; private final ScalarType parent; @@ -122,13 +168,13 @@ public class BeanPropertyJsonMapper extends BeanProperty { } @Override - public boolean isEqualToObject(Object obj) { - return isEqualToJson(parent.format(obj)); + public MutableValueNext nextDirty(String json) { + return Objects.equals(originalJson, json) ? null : new SourceMutableValue(parent, json); } @Override - public boolean isEqualToJson(String json) { - return Objects.equals(originalJson, json); + public boolean isEqualToObject(Object obj) { + return Objects.equals(originalJson, parent.format(obj)); } @Override @@ -136,16 +182,26 @@ public class BeanPropertyJsonMapper extends BeanProperty { // rebuild the 'oldValue' for change log etc return parent.parse(originalJson); } + + @Override + public String content() { + return originalJson; + } + + @Override + public MutableValueInfo info() { + return this; + } } /** * No dirty detection on json content. */ - private static class NoDirtyDetection implements MutableValueInfo { + private static final class NoDirtyDetection implements MutableValueInfo { @Override - public boolean isEqualToJson(String json) { - return true; // treat as not dirty + public MutableValueNext nextDirty(String json) { + return null; // treat as not dirty } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java index 5a94d0e0e..cfa056a63 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/dmlbind/BindablePropertyJsonUpdate.java @@ -1,7 +1,6 @@ package io.ebeaninternal.server.persist.dmlbind; import io.ebean.bean.EntityBean; -import io.ebean.bean.MutableValueInfo; import io.ebeaninternal.server.deploy.BeanProperty; import java.sql.SQLException; @@ -26,11 +25,8 @@ class BindablePropertyJsonUpdate extends BindableProperty { if (bean == null) { request.bind(null, prop); } else { - // on update store hash and push json - final String json = bean._ebean_getIntercept().mutableContent(propertyIndex); - final MutableValueInfo hash = prop.createMutableInfo(json); - bean._ebean_getIntercept().mutableInfo(propertyIndex, hash); - request.pushJson(json); + // update mutableInfo and push json + request.pushJson(bean._ebean_getIntercept().mutableNext(propertyIndex)); request.bind(prop.getValue(bean), prop); } } From 5aa8557168add6acf9bc7d016dc30eddb57c2e25 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 16:29:10 +1200 Subject: [PATCH 18/26] Update tests only - TestJacksonPlainBean with dirtyDetection = false property --- .../org/tests/model/json/EBasicPlain.java | 11 ++++++++ .../model/json/TestJacksonPlainBean.java | 26 ++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java b/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java index f3705aa16..401e66fa1 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java @@ -17,6 +17,9 @@ public class EBasicPlain { @DbJson(length = 500) PlainBean plainBean; + @DbJson(length = 500, dirtyDetection = false) + PlainBean plainBean2; + @Version long version; @@ -44,6 +47,14 @@ public class EBasicPlain { this.plainBean = plainBean; } + public PlainBean getPlainBean2() { + return plainBean2; + } + + public void setPlainBean2(PlainBean plainBean2) { + this.plainBean2 = plainBean2; + } + public long getVersion() { return version; } diff --git a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java index 0e3d22aa7..f77f2f8b6 100644 --- a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java +++ b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java @@ -16,25 +16,20 @@ public class TestJacksonPlainBean { DB.getDefault(); LoggedSqlCollector.start(); - PlainBean content = new PlainBean(); - content.setAlong(42); - content.setName("foo"); - + PlainBean content = new PlainBean("foo", 42); EBasicPlain bean = new EBasicPlain(); bean.setAttr("attr0"); bean.setPlainBean(content); - + bean.setPlainBean2(new PlainBean("bar", 27)); DB.save(bean); - expectedSql(0, "insert into ebasic_plain (attr, plain_bean, version) values (?,?,?)"); - + expectedSql(0, "insert into ebasic_plain (attr, plain_bean, plain_bean2, version) values (?,?,?,?)"); // inserted plainBean has not been mutated bean.setAttr("attr1"); DB.save(bean); expectedSql(0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); - // inserted plainBean has now been mutated content.setName("notFoo"); bean.setAttr("attr2"); @@ -50,17 +45,24 @@ public class TestJacksonPlainBean { DB.save(found); expectedSql(1, "update ebasic_plain set plain_bean=?, version=? where id=? and version=?"); - - // update bean, mutate PlainBean only + // dirtyDetection = false, so not included in update + found.getPlainBean2().setName("Modification Ignored"); + // dirtyDetection = true, mutation detected plainBean.setName("mod2"); DB.save(found); expectedSql(0, "update ebasic_plain set plain_bean=?, version=? where id=? and version=?"); - // update bean, not mutating PlainBean found.setAttr("attr3"); DB.save(found); - expectedSql(LoggedSqlCollector.stop(), 0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); + expectedSql(0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); + + // dirtyDetection = false, set a new plainBean2 instance, included in update + found.setPlainBean2(new PlainBean("bar", 27)); + DB.save(found); + expectedSql( 0, "update ebasic_plain set plain_bean2=?, version=? where id=? and version=?"); + + LoggedSqlCollector.stop(); } private void expectedSql(int i, String s) { From 20becf951d02754f4003cb88a1b5aa1ea522de3c Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 23:11:42 +1200 Subject: [PATCH 19/26] Use MutationDetection replacing dirtyDetection and keepSource Also adds NoMutationDetection to support NONE --- ebean-api/pom.xml | 2 +- .../server/deploy/BeanPropertyJsonMapper.java | 46 ++++++------------- .../deploy/meta/DeployBeanProperty.java | 39 ++++------------ .../deploy/meta/DeployBeanPropertyLists.java | 42 ++--------------- .../server/deploy/parse/DeployUtil.java | 16 +++---- .../server/type/DefaultTypeManager.java | 3 +- .../type/ScalarTypeJsonObjectMapper.java | 35 +++++++++++++- .../org/tests/model/json/EBasicJsonList.java | 11 ++--- .../org/tests/model/json/EBasicPlain.java | 4 +- .../model/json/TestJacksonPlainBean.java | 5 +- 10 files changed, 76 insertions(+), 127 deletions(-) diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index 649f85822..503c2f2c5 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -50,7 +50,7 @@ io.ebean ebean-annotation - 7.1 + 7.2 diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index d3a683e48..f1efc39f7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -1,9 +1,10 @@ package io.ebeaninternal.server.deploy; +import io.ebean.annotation.MutationDetection; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; -import io.ebean.bean.MutableValueNext; import io.ebean.bean.MutableValueInfo; +import io.ebean.bean.MutableValueNext; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; @@ -14,26 +15,24 @@ import javax.persistence.PersistenceException; import java.sql.SQLException; import java.util.Objects; +/** + * Handle json property with MutationDetection of SOURCE or HASH only. + */ public class BeanPropertyJsonMapper extends BeanProperty { - private static final NoDirtyDetection NO_DIRTY_DETECTION = new NoDirtyDetection(); - private final boolean dirtyDetection; - private final boolean keepSource; + private final boolean sourceDetection; public BeanPropertyJsonMapper(BeanDescriptor desc, DeployBeanProperty deployProp) { super(desc, deployProp); - this.dirtyDetection = deployProp.isDirtyDetection(); - this.keepSource = deployProp.isKeepSource(); + this.sourceDetection = deployProp.getMutationDetection() == MutationDetection.SOURCE; } @Override public MutableValueInfo createMutableInfo(String json) { - if (keepSource) { + if (sourceDetection) { return new SourceMutableValue(scalarType, json); - } else if (dirtyDetection) { - return new ChecksumMutableValue(scalarType, json); } else { - return NO_DIRTY_DETECTION; + return new ChecksumMutableValue(scalarType, json); } } @@ -41,22 +40,19 @@ public class BeanPropertyJsonMapper extends BeanProperty { * Next when no prior MutableValueInfo. */ private MutableValueNext next(String json) { - if (keepSource) { + if (sourceDetection) { return new SourceMutableValue(scalarType, json); - } else if (dirtyDetection) { - return new NextPair(json, new ChecksumMutableValue(scalarType, json)); } else { - throw new IllegalStateException("Never get here"); + return new NextPair(json, new ChecksumMutableValue(scalarType, json)); } } /** - * Return true if the mutable value is considered dirty. - * This is only used for 'mutable' scalar types like hstore etc. + * Return true if the json property is considered dirty. */ @Override boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { - // dirty detection based on json content or checksum of json content + // mutation detection based on json content or checksum of json content // only perform serialisation to json once final String json = scalarType.format(value); final MutableValueInfo oldHash = ebi.mutableInfo(propertyIndex); @@ -193,20 +189,4 @@ public class BeanPropertyJsonMapper extends BeanProperty { return this; } } - - /** - * No dirty detection on json content. - */ - private static final class NoDirtyDetection implements MutableValueInfo { - - @Override - public MutableValueNext nextDirty(String json) { - return null; // treat as not dirty - } - - @Override - public boolean isEqualToObject(Object obj) { - return true; // treat as not dirty - } - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 060ce802f..3da06d559 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -1,18 +1,6 @@ package io.ebeaninternal.server.deploy.meta; -import io.ebean.annotation.CreatedTimestamp; -import io.ebean.annotation.DocCode; -import io.ebean.annotation.DocProperty; -import io.ebean.annotation.DocSortable; -import io.ebean.annotation.Formula; -import io.ebean.annotation.Platform; -import io.ebean.annotation.SoftDelete; -import io.ebean.annotation.UpdatedTimestamp; -import io.ebean.annotation.WhenCreated; -import io.ebean.annotation.WhenModified; -import io.ebean.annotation.Where; -import io.ebean.annotation.WhoCreated; -import io.ebean.annotation.WhoModified; +import io.ebean.annotation.*; import io.ebean.config.ScalarTypeConverter; import io.ebean.config.dbplatform.DbDefaultValue; import io.ebean.config.dbplatform.DbEncrypt; @@ -39,7 +27,6 @@ import java.lang.reflect.Field; import java.lang.reflect.Type; import java.sql.Types; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -109,8 +96,7 @@ public class DeployBeanProperty { private boolean jsonSerialize = true; private boolean jsonDeserialize = true; - private boolean dirtyDetection; - private boolean keepSource; + private MutationDetection mutationDetection; private boolean dbEncrypted; private DbEncryptFunction dbEncryptFunction; @@ -329,18 +315,15 @@ public class DeployBeanProperty { this.jsonDeserialize = jsonDeserialize; } - /** - * Return true if we should have JSON dirty detection on this property. - */ - public boolean isDirtyDetection() { - return dirtyDetection; + public MutationDetection getMutationDetection() { + if (mutationDetection == null) { + mutationDetection = MutationDetection.DEFAULT; + } + return mutationDetection; } - /** - * Return true if we should store source JSON content on this property. - */ - public boolean isKeepSource() { - return keepSource; + public void setMutationDetection(MutationDetection dirtyDetection) { + this.mutationDetection = dirtyDetection; } /** @@ -1221,8 +1204,4 @@ public class DeployBeanProperty { return scalarType != null && scalarType.isJsonMapper(); } - public void setJsonOptions(boolean dirtyDetection, boolean keepSource) { - this.dirtyDetection = dirtyDetection; - this.keepSource = keepSource; - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java index 9843f8e5c..90503c93f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyLists.java @@ -22,47 +22,28 @@ public class DeployBeanPropertyLists { private static final NoopSetter NOOP_SETTER = new NoopSetter(); private BeanProperty versionProperty; - private BeanProperty unmappedJson; - private BeanProperty draft; - private BeanProperty draftDirty; - private BeanProperty tenant; - private final BeanDescriptor desc; - private final LinkedHashMap propertyMap; - private BeanProperty id; private final List local = new ArrayList<>(); - private final List mutable = new ArrayList<>(); - private final List> manys = new ArrayList<>(); - private final List nonManys = new ArrayList<>(); - private final List aggs = new ArrayList<>(); - private final List> ones = new ArrayList<>(); - private final List> onesImported = new ArrayList<>(); - private final List> embedded = new ArrayList<>(); - private final List baseScalar = new ArrayList<>(); - private final List transients = new ArrayList<>(); - private final List nonTransients = new ArrayList<>(); - private final BeanPropertyAssocOne unidirectional; private final BeanProperty orderColumn; - @SuppressWarnings({"unchecked"}) public DeployBeanPropertyLists(BeanDescriptorMap owner, BeanDescriptor desc, DeployBeanDescriptor deploy) { this.desc = desc; @@ -78,7 +59,7 @@ public class DeployBeanPropertyLists { this.orderColumn = deployOrderColumn != null ? new BeanPropertyOrderColumn(desc, deployOrderColumn) : null; DeployBeanPropertyAssocOne deployUnidirectional = deploy.getUnidirectional(); - this.unidirectional = deployUnidirectional == null ? null : new BeanPropertyAssocOne(owner, desc, deployUnidirectional); + this.unidirectional = deployUnidirectional == null ? null : new BeanPropertyAssocOne<>(owner, desc, deployUnidirectional); this.propertyMap = new LinkedHashMap<>(); @@ -119,7 +100,7 @@ public class DeployBeanPropertyLists { } if (orderColumn != null) { - orderColumn.setDeployOrder(order++); + orderColumn.setDeployOrder(order); allocateToList(orderColumn); propertyMap.put(orderColumn.getName(), orderColumn); } @@ -146,7 +127,6 @@ public class DeployBeanPropertyLists { } private void setImportedPrimaryKeysFor(DeployBeanDescriptor deploy, DeployBeanPropertyAssocOne id) { - for (DeployBeanProperty prop : id.getTargetDeploy().properties()) { DeployBeanProperty match = findImported(deploy, prop); if (match != null) { @@ -156,7 +136,6 @@ public class DeployBeanPropertyLists { } private DeployBeanProperty findImported(DeployBeanDescriptor deploy, DeployBeanProperty embeddedScalar) { - // the logical name and db column we are looking for a match on String name = embeddedScalar.getName(); String dbColumn = embeddedScalar.getDbColumn(); @@ -172,7 +151,6 @@ public class DeployBeanPropertyLists { return assocOne; } } - return null; } @@ -360,7 +338,6 @@ public class DeployBeanPropertyLists { } public BeanProperty getSoftDeleteProperty() { - for (BeanProperty prop : nonManys) { if (prop.isSoftDelete()) { return prop; @@ -377,7 +354,6 @@ public class DeployBeanPropertyLists { * Return the properties set via generated values on insert. */ public BeanProperty[] getGeneratedInsert() { - List list = new ArrayList<>(); for (BeanProperty prop : nonTransients) { GeneratedProperty gen = prop.getGeneratedProperty(); @@ -392,7 +368,6 @@ public class DeployBeanPropertyLists { * Return the properties set via generated values on update. */ public BeanProperty[] getGeneratedUpdate() { - List list = new ArrayList<>(); for (BeanProperty prop : nonTransients) { GeneratedProperty gen = prop.getGeneratedProperty(); @@ -430,8 +405,7 @@ public class DeployBeanPropertyLists { } } } - - return (BeanPropertyAssocOne[]) list.toArray(new BeanPropertyAssocOne[0]); + return list.toArray(new BeanPropertyAssocOne[0]); } private BeanPropertyAssocMany[] getMany2Many() { @@ -441,8 +415,7 @@ public class DeployBeanPropertyLists { list.add(prop); } } - - return (BeanPropertyAssocMany[]) list.toArray(new BeanPropertyAssocMany[0]); + return list.toArray(new BeanPropertyAssocMany[0]); } private BeanPropertyAssocMany[] getMany(Mode mode) { @@ -463,25 +436,20 @@ public class DeployBeanPropertyLists { break; } } - - return (BeanPropertyAssocMany[]) list.toArray(new BeanPropertyAssocMany[0]); + return list.toArray(new BeanPropertyAssocMany[0]); } @SuppressWarnings({"unchecked", "rawtypes"}) private BeanProperty createBeanProperty(BeanDescriptorMap owner, DeployBeanProperty deployProp) { - if (deployProp instanceof DeployBeanPropertyAssocOne) { return new BeanPropertyAssocOne(owner, desc, (DeployBeanPropertyAssocOne) deployProp); } - if (deployProp instanceof DeployBeanPropertySimpleCollection) { return new BeanPropertySimpleCollection(desc, (DeployBeanPropertySimpleCollection) deployProp); } - if (deployProp instanceof DeployBeanPropertyAssocMany) { return new BeanPropertyAssocMany(desc, (DeployBeanPropertyAssocMany) deployProp); } - if (deployProp.isJsonMapper()) { return new BeanPropertyJsonMapper(desc, deployProp); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java index 2245b0c4c..0792c66d5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -1,10 +1,6 @@ package io.ebeaninternal.server.deploy.parse; -import io.ebean.annotation.DbArray; -import io.ebean.annotation.DbJson; -import io.ebean.annotation.DbJsonB; -import io.ebean.annotation.DbJsonType; -import io.ebean.annotation.DbMap; +import io.ebean.annotation.*; import io.ebean.config.DatabaseConfig; import io.ebean.config.EncryptDeploy; import io.ebean.config.EncryptDeployManager; @@ -214,21 +210,21 @@ public class DeployUtil { */ void setDbJsonType(DeployBeanProperty prop, DbJson dbJsonType) { int dbType = getDbJsonStorage(dbJsonType.storage()); - setDbJsonType(prop, dbType, dbJsonType.length(), dbJsonType.dirtyDetection(), dbJsonType.keepSource()); + setDbJsonType(prop, dbType, dbJsonType.length(), dbJsonType.mutationDetection()); } void setDbJsonBType(DeployBeanProperty prop, DbJsonB dbJsonB) { - setDbJsonType(prop, DbPlatformType.JSONB, dbJsonB.length(), dbJsonB.dirtyDetection(), dbJsonB.keepSource()); + setDbJsonType(prop, DbPlatformType.JSONB, dbJsonB.length(), dbJsonB.mutationDetection()); } - private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength, boolean dirtyDetection, boolean keepSource) { + private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength, MutationDetection mutationDetection) { + prop.setDbType(dbType); + prop.setMutationDetection(mutationDetection); ScalarType scalarType = typeManager.getJsonScalarType(prop, dbType, dbLength); if (scalarType == null) { throw new RuntimeException("No ScalarType for JSON property [" + prop + "] [" + dbType + "]"); } - prop.setDbType(dbType); prop.setScalarType(scalarType); - prop.setJsonOptions(dirtyDetection, keepSource); if (dbType == Types.VARCHAR || dbLength > 0) { // determine the db column size int columnLength = (dbLength > 0) ? dbLength : DEFAULT_JSON_VARCHAR_LENGTH; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index befe46b20..28999e838 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -2,7 +2,6 @@ package io.ebeaninternal.server.type; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.introspect.AnnotatedField; import io.ebean.annotation.*; import io.ebean.config.DatabaseConfig; import io.ebean.config.JsonConfig; @@ -426,7 +425,7 @@ public final class DefaultTypeManager implements TypeManager { if (objectMapper == null) { throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping - Jackson ObjectMapper not present"); } - return ScalarTypeJsonObjectMapper.createTypeFor(jsonManager, (AnnotatedField) prop.getJacksonField(), dbType, docType); + return ScalarTypeJsonObjectMapper.createTypeFor(jsonManager, prop, dbType, docType); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java index 14425c6c9..7fb3aa4b4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonObjectMapper.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.introspect.AnnotatedField; +import io.ebean.annotation.MutationDetection; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -15,6 +16,7 @@ import io.ebean.text.TextException; import io.ebeaninternal.json.ModifyAwareList; import io.ebeaninternal.json.ModifyAwareMap; import io.ebeaninternal.json.ModifyAwareSet; +import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import javax.persistence.PersistenceException; import java.io.DataInput; @@ -34,8 +36,16 @@ class ScalarTypeJsonObjectMapper { /** * Create and return the appropriate ScalarType. */ - static ScalarType createTypeFor(TypeJsonManager jsonManager, AnnotatedField field, int dbType, DocPropertyType docType) { + static ScalarType createTypeFor(TypeJsonManager jsonManager, DeployBeanProperty prop, int dbType, DocPropertyType docType) { + AnnotatedField field = (AnnotatedField) prop.getJacksonField(); Class type = field.getRawType(); + + MutationDetection mode = prop.getMutationDetection(); + if (mode == MutationDetection.NONE) { + return new NoMutationDetection(jsonManager, field, dbType, type); + } else if (mode != MutationDetection.DEFAULT) { + return new GenericObject(jsonManager, field, dbType, type); + } if (Set.class.equals(type)) { return new OmSet(jsonManager, field, dbType, docType); } @@ -45,11 +55,32 @@ class ScalarTypeJsonObjectMapper { if (Map.class.equals(type)) { return new OmMap(jsonManager, field, dbType); } + prop.setMutationDetection(MutationDetection.HASH); return new GenericObject(jsonManager, field, dbType, type); } /** - * Maps any type (Object) using Jackson ObjectMapper. + * No mutation detection on this json property. + */ + private static class NoMutationDetection extends Base { + + NoMutationDetection(TypeJsonManager jsonManager, AnnotatedField field, int dbType, Class rawType) { + super(Object.class, jsonManager, field, dbType, DocPropertyType.OBJECT, rawType); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public boolean isDirty(Object value) { + return false; + } + } + + /** + * Supports HASH and SOURCE dirty detection modes. */ private static class GenericObject extends Base { diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java index 05d9807b5..d5ce2c309 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java @@ -7,12 +7,9 @@ import io.ebean.annotation.DbJsonType; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Version; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; + +import static io.ebean.annotation.MutationDetection.SOURCE; @Entity public class EBasicJsonList { @@ -31,7 +28,7 @@ public class EBasicJsonList { @DbJson(length = 700) Map beanMap = new LinkedHashMap<>(); - @DbJson(length = 500, keepSource = true) // such that we can rebuild old values + @DbJson(length = 500, mutationDetection = SOURCE) // such that we can rebuild old values PlainBean plainBean; @DbJson(length = 50) diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java b/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java index 401e66fa1..051b4a344 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicPlain.java @@ -6,6 +6,8 @@ import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Version; +import static io.ebean.annotation.MutationDetection.NONE; + @Entity public class EBasicPlain { @@ -17,7 +19,7 @@ public class EBasicPlain { @DbJson(length = 500) PlainBean plainBean; - @DbJson(length = 500, dirtyDetection = false) + @DbJson(length = 500, mutationDetection = NONE) // only update when property set PlainBean plainBean2; @Version diff --git a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java index f77f2f8b6..c0fe82b26 100644 --- a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java +++ b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java @@ -60,7 +60,7 @@ public class TestJacksonPlainBean { // dirtyDetection = false, set a new plainBean2 instance, included in update found.setPlainBean2(new PlainBean("bar", 27)); DB.save(found); - expectedSql( 0, "update ebasic_plain set plain_bean2=?, version=? where id=? and version=?"); + expectedSql(0, "update ebasic_plain set plain_bean2=?, version=? where id=? and version=?"); LoggedSqlCollector.stop(); } @@ -69,7 +69,4 @@ public class TestJacksonPlainBean { assertThat(LoggedSqlCollector.current().get(i)).contains(s); } - private void expectedSql(List sql, int i, String s) { - assertThat(sql.get(i)).contains(s); - } } From 20e0eb102193d4b7f69f4fd45295e345055ca013 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 23:17:11 +1200 Subject: [PATCH 20/26] Re-enable TestDbJson_Jackson3 via adding mutationDetection = HASH to beanList property @DbJsonB(mutationDetection = HASH) List beanList; --- .../src/test/java/org/tests/json/TestDbJson_Jackson3.java | 7 ++----- .../src/test/java/org/tests/model/json/EBasicJsonList.java | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index e8d8abdfd..2f74be109 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -161,10 +161,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { final EBasicJsonList found = DB.find(EBasicJsonList.class, bean.getId()); found.getBeanList().get(0).setName("p1-mod"); -// BeanState state = DB.getBeanState(found); -// assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); - // this test fails, because we have a OmList instead of a GenericObject - // TODO: Can/Should we enhance the @DbJson/@DbJsonB annotations with a property "dirtyDetection" - + BeanState state = DB.getBeanState(found); + assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); } } diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java index d5ce2c309..f76d282f7 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonList.java @@ -9,6 +9,7 @@ import javax.persistence.Id; import javax.persistence.Version; import java.util.*; +import static io.ebean.annotation.MutationDetection.HASH; import static io.ebean.annotation.MutationDetection.SOURCE; @Entity @@ -22,7 +23,7 @@ public class EBasicJsonList { @DbJson(length = 700, name = "beans") Set beanSet; - @DbJsonB + @DbJsonB(mutationDetection = HASH) List beanList; @DbJson(length = 700) From 822fb7325a244abf3a390ed59bf9ca2ce755a7c8 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 29 Jul 2021 23:37:29 +1200 Subject: [PATCH 21/26] Due to mutableNext handling, with checkMutableProperties() even when known dirty go into beanProperty.checkMutable() As per rPraml's PR and comment Due to handling of mutableNext we need checkMutableProperties() to call into what is now beanProperty.checkMutable() even when we already know it's dirty. --- .../java/io/ebeaninternal/server/deploy/BeanDescriptor.java | 4 ++-- .../java/io/ebeaninternal/server/deploy/BeanProperty.java | 4 ++-- .../ebeaninternal/server/deploy/BeanPropertyJsonMapper.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index d165b1489..8f0fea0f2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -3198,9 +3198,9 @@ public class BeanDescriptor implements BeanType, STreeType, SpiBeanType { public void checkMutableProperties(EntityBeanIntercept ebi) { for (BeanProperty beanProperty : propertiesMutable) { int propertyIndex = beanProperty.getPropertyIndex(); - if (!ebi.isDirtyProperty(propertyIndex) && ebi.isLoadedProperty(propertyIndex)) { + if (ebi.isLoadedProperty(propertyIndex)) { Object value = beanProperty.getValue(ebi.getOwner()); - if (value != null && beanProperty.isDirtyValue(value, ebi)) { + if (beanProperty.checkMutable(value, ebi.isDirtyProperty(propertyIndex), ebi)) { // mutable scalar value which is considered dirty so mark // it as such so that it is included in an update ebi.markPropertyAsChanged(propertyIndex); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index fc5a8364f..a33d183ea 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -1018,8 +1018,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { * Return true if the mutable value is considered dirty. * This is only used for 'mutable' scalar types like hstore etc. */ - boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { - return scalarType.isDirty(value); + boolean checkMutable(Object value, boolean alreadyDirty, EntityBeanIntercept ebi) { + return alreadyDirty || value != null && scalarType.isDirty(value); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index f1efc39f7..c9280c8c0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -51,7 +51,7 @@ public class BeanPropertyJsonMapper extends BeanProperty { * Return true if the json property is considered dirty. */ @Override - boolean isDirtyValue(Object value, EntityBeanIntercept ebi) { + boolean checkMutable(Object value, boolean alreadyDirty, EntityBeanIntercept ebi) { // mutation detection based on json content or checksum of json content // only perform serialisation to json once final String json = scalarType.format(value); From c43e2e695686d861f8d4982f52f28f73bd27d99f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 30 Jul 2021 15:34:54 +1200 Subject: [PATCH 22/26] Update TestDbJson_Jackson3 --- .../org/tests/json/TestDbJson_Jackson3.java | 62 +++++++++++++++++++ .../tests/model/json/EBasicJsonJackson3.java | 21 ++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 2f74be109..145790457 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -164,4 +164,66 @@ public class TestDbJson_Jackson3 extends BaseTestCase { BeanState state = DB.getBeanState(found); assertThat(state.getChangedProps()).containsExactlyInAnyOrder("beanList"); } + + @Test + public void update_with_differentDbJsonSettings() { + PlainBeanDirtyAware contentBean1 = new PlainBeanDirtyAware("x", 42); + PlainBeanDirtyAware contentBean2 = new PlainBeanDirtyAware("y", 43); + PlainBeanDirtyAware contentBean3 = new PlainBeanDirtyAware("z", 44); + + EBasicJsonJackson3 bean = new EBasicJsonJackson3(); + bean.setName("b1"); + bean.setPlainValue(contentBean1); + bean.setPlainValue2(contentBean2); + bean.setPlainValue3(contentBean3); + + BeanState state = DB.getBeanState(bean); + // a new bean is not considered as dirty (thus have no changed props) + assertThat(state.isDirty()).isFalse(); + assertThat(state.isNewOrDirty()).isTrue(); + assertThat(state.getChangedProps()).isEmpty(); + + bean.save(); + + bean = DB.find(EBasicJsonJackson3.class, bean.getId()); + state = DB.getBeanState(bean); + // a fresh loaded bean is also not considered as dirty + assertThat(state.isDirty()).isFalse(); + assertThat(state.isNewOrDirty()).isFalse(); + assertThat(state.getChangedProps()).isEmpty(); + + bean.getPlainValue().setName("a"); // has keepSource=true + + assertThat(state.isDirty()).isTrue(); + assertThat(state.getChangedProps()).containsExactly("plainValue"); + + bean.getPlainValue2().setName("b"); + assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainValue", "plainValue2"); + + bean.getPlainValue3().setName("c"); // has dirtyDetection = false + + Map dirtyValues = state.getDirtyValues(); + assertThat(dirtyValues).hasSize(2).containsKeys("plainValue", "plainValue2"); + + assertThat(dirtyValues.get("plainValue")).hasToString("name:a,name:x"); // SOURCE -> origValue present + assertThat(dirtyValues.get("plainValue2")).hasToString("name:b,null"); // without SOURCE no origValue present + + LoggedSql.start(); + bean.save(); + List sql = LoggedSql.collect(); + assertThat(sql.get(0)).contains("update ebasic_json_jackson3 set plain_value=?, plain_value2=?, version=? where id=?"); + + bean = DB.find(EBasicJsonJackson3.class, bean.getId()); + LoggedSql.collect(); // ignore the select + assertThat(bean.getPlainValue().getName()).isEqualTo("a"); + assertThat(bean.getPlainValue2().getName()).isEqualTo("b"); + assertThat(bean.getPlainValue3().getName()).isEqualTo("z"); // value is not updated + + bean.getPlainValue3().setName("c"); + bean.getPlainValue3().setMarkedDirty(true); // This is ignored because it is MutationDetection.NONE + bean.save(); + // no update as plainValue3 has MutationDetection.NONE (ModifyAwareType = NONE isn't an expected combination to me) + assertThat(LoggedSql.collect()).isEmpty(); + LoggedSql.stop(); + } } diff --git a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java index 45375b516..cee12d9e7 100644 --- a/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java +++ b/ebean-core/src/test/java/org/tests/model/json/EBasicJsonJackson3.java @@ -2,11 +2,15 @@ package org.tests.model.json; import io.ebean.Model; import io.ebean.annotation.DbJson; +import io.ebean.annotation.MutationDetection; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Version; +import static io.ebean.annotation.MutationDetection.NONE; +import static io.ebean.annotation.MutationDetection.SOURCE; + @Entity public class EBasicJsonJackson3 extends Model { @@ -15,12 +19,15 @@ public class EBasicJsonJackson3 extends Model { String name; - @DbJson(length = 500) + @DbJson(length = 500, mutationDetection = SOURCE) PlainBeanDirtyAware plainValue; @DbJson(length = 500) PlainBeanDirtyAware plainValue2; - + + @DbJson(length = 500, mutationDetection = NONE) + PlainBeanDirtyAware plainValue3; + @Version long version; @@ -55,7 +62,15 @@ public class EBasicJsonJackson3 extends Model { public void setPlainValue2(PlainBeanDirtyAware plainValue2) { this.plainValue2 = plainValue2; } - + + public PlainBeanDirtyAware getPlainValue3() { + return plainValue3; + } + + public void setPlainValue3(PlainBeanDirtyAware plainValue3) { + this.plainValue3 = plainValue3; + } + public long getVersion() { return version; } From ea741d01699d107f7f88fed1057c91abe2d578c4 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 30 Jul 2021 15:41:52 +1200 Subject: [PATCH 23/26] Update TestDbJson_Jackson3 with comments --- .../src/test/java/org/tests/json/TestDbJson_Jackson3.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index 145790457..ac10f2a16 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -66,7 +66,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { found.setName("b1-mod2"); found.getPlainValue().setName("b"); - found.getPlainValue().setMarkedDirty(true); + // found.getPlainValue().setMarkedDirty(true); // Irrelevant for SOURCE or HASH based mutation detection found.save(); @@ -192,7 +192,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { assertThat(state.isNewOrDirty()).isFalse(); assertThat(state.getChangedProps()).isEmpty(); - bean.getPlainValue().setName("a"); // has keepSource=true + bean.getPlainValue().setName("a"); // has SOURCE assertThat(state.isDirty()).isTrue(); assertThat(state.getChangedProps()).containsExactly("plainValue"); @@ -200,7 +200,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { bean.getPlainValue2().setName("b"); assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainValue", "plainValue2"); - bean.getPlainValue3().setName("c"); // has dirtyDetection = false + bean.getPlainValue3().setName("c"); // has mutationDetection = NONE Map dirtyValues = state.getDirtyValues(); assertThat(dirtyValues).hasSize(2).containsKeys("plainValue", "plainValue2"); From ca1636ff9edb0c22177875af8d5f26643ccba83c Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 30 Jul 2021 17:16:05 +1200 Subject: [PATCH 24/26] Fix when json/jackson bean inserted as null and not changed Expectation is that it is not included in update (still null, no change) --- .../server/deploy/BeanPropertyJsonMapper.java | 3 +++ .../model/json/TestJacksonPlainBean.java | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java index c9280c8c0..bc7392287 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyJsonMapper.java @@ -57,6 +57,9 @@ public class BeanPropertyJsonMapper extends BeanProperty { final String json = scalarType.format(value); final MutableValueInfo oldHash = ebi.mutableInfo(propertyIndex); if (oldHash == null) { + if (value == null) { + return false; // no change, still null + } ebi.mutableNext(propertyIndex, next(json)); return true; } diff --git a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java index c0fe82b26..24f0a35f8 100644 --- a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java +++ b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java @@ -10,6 +10,31 @@ import static org.assertj.core.api.Assertions.assertThat; public class TestJacksonPlainBean { + @Test + public void insertNullStayNull() { + + // insert with jackson beans as null + EBasicPlain bean = new EBasicPlain(); + bean.setAttr("n0"); + DB.save(bean); + + LoggedSqlCollector.start(); + bean.setAttr("n1"); + DB.save(bean); + expectedSql(0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); + + bean.setPlainBean(new PlainBean("x", 1)); + DB.save(bean); + expectedSql(0, "update ebasic_plain set plain_bean=?, version=? where id=? and version=?"); + + final EBasicPlain found = DB.find(EBasicPlain.class, bean.getId()); + found.setAttr("n2"); + DB.save(found); + expectedSql(1, "update ebasic_plain set attr=?, version=? where id=? and version=?"); + + LoggedSqlCollector.stop(); + } + @Test public void insertUpdate() { From 7bd7640c631e586c953439ec60104e66baa52330 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 30 Jul 2021 17:18:28 +1200 Subject: [PATCH 25/26] Update test TestDbJson_Jackson3 showing HASH mode is used even on ModifyAwareType --- .../src/test/java/org/tests/json/TestDbJson_Jackson3.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index ac10f2a16..bc89a0eb5 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -224,6 +224,12 @@ public class TestDbJson_Jackson3 extends BaseTestCase { bean.save(); // no update as plainValue3 has MutationDetection.NONE (ModifyAwareType = NONE isn't an expected combination to me) assertThat(LoggedSql.collect()).isEmpty(); + + bean.getPlainValue2().setName("b2"); // effectively HASH mode mutation detection + bean.save(); + sql = LoggedSql.collect(); + assertThat(sql.get(0)).contains("update ebasic_json_jackson3 set plain_value2=?, version=? where id=? and version=?"); + LoggedSql.stop(); } } From d9f7531e81ee29459aab29386cfb6eb34baf637d Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 30 Jul 2021 17:26:46 +1200 Subject: [PATCH 26/26] Tidy tests only - TestDbJson_Jackson3 TestJacksonPlainBean --- .../org/tests/json/TestDbJson_Jackson3.java | 33 ++++++++----------- .../model/json/TestJacksonPlainBean.java | 14 ++++---- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java index bc89a0eb5..c1cc3c40d 100644 --- a/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java +++ b/ebean-core/src/test/java/org/tests/json/TestDbJson_Jackson3.java @@ -59,20 +59,15 @@ public class TestDbJson_Jackson3 extends BaseTestCase { LoggedSql.start(); found.save(); - - List sql = LoggedSql.collect(); - assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("update ebasic_json_jackson3 set name=?, version=? where id=? and version=?"); + expectedSql(0, "update ebasic_json_jackson3 set name=?, version=? where id=? and version=?"); found.setName("b1-mod2"); found.getPlainValue().setName("b"); // found.getPlainValue().setMarkedDirty(true); // Irrelevant for SOURCE or HASH based mutation detection found.save(); - - sql = LoggedSql.stop(); - assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("update ebasic_json_jackson3 set name=?, plain_value=?, version=? where id=? and version=?"); + expectedSql(0, "update ebasic_json_jackson3 set name=?, plain_value=?, version=? where id=? and version=?"); + LoggedSql.stop(); final EBasicJsonJackson3 found2 = DB.find(EBasicJsonJackson3.class, bean.getId()); @@ -112,20 +107,16 @@ public class TestDbJson_Jackson3 extends BaseTestCase { LoggedSql.start(); DB.save(found); - List sql = LoggedSql.stop(); - assertThat(sql).hasSize(1); // plain_bean=?, no longer included with MD5 dirty detection - assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, version=? where id=?"); + expectedSql(0, "update ebasic_json_list set name=?, bean_list=?, version=? where id=?"); assertThat(EBasicJsonListPersistController.updatedValues.entrySet()) .extracting(Map.Entry::toString) .containsExactlyInAnyOrder("beanList=null,[name:a]","name=p1-mod,p1","version=2,1"); - assertThat(DB.getBeanState(found).isDirty()).isFalse(); found.getPlainBean().setName("b"); - assertThat(DB.getBeanState(found).isDirty()).isTrue(); state = DB.getBeanState(found); @@ -138,14 +129,14 @@ public class TestDbJson_Jackson3 extends BaseTestCase { LoggedSql.start(); DB.save(found); - sql = LoggedSql.stop(); - assertThat(sql).hasSize(1); // plain_bean=?, no longer included with MD5 dirty detection - assertThat(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, version=? where id=?"); + expectedSql(0, "update ebasic_json_list set plain_bean=?, version=? where id=?"); assertThat(EBasicJsonListPersistController.updatedValues.entrySet()) .extracting(Map.Entry::toString) .containsExactlyInAnyOrder("plainBean=name:b,name:a", "version=3,2"); + + LoggedSql.stop(); } @Test @@ -210,8 +201,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase { LoggedSql.start(); bean.save(); - List sql = LoggedSql.collect(); - assertThat(sql.get(0)).contains("update ebasic_json_jackson3 set plain_value=?, plain_value2=?, version=? where id=?"); + expectedSql(0, "update ebasic_json_jackson3 set plain_value=?, plain_value2=?, version=? where id=?"); bean = DB.find(EBasicJsonJackson3.class, bean.getId()); LoggedSql.collect(); // ignore the select @@ -227,9 +217,12 @@ public class TestDbJson_Jackson3 extends BaseTestCase { bean.getPlainValue2().setName("b2"); // effectively HASH mode mutation detection bean.save(); - sql = LoggedSql.collect(); - assertThat(sql.get(0)).contains("update ebasic_json_jackson3 set plain_value2=?, version=? where id=? and version=?"); + expectedSql(0, "update ebasic_json_jackson3 set plain_value2=?, version=? where id=? and version=?"); LoggedSql.stop(); } + + private void expectedSql(int i, String s) { + assertThat(LoggedSql.collect().get(i)).contains(s); + } } diff --git a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java index 24f0a35f8..b2237731d 100644 --- a/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java +++ b/ebean-core/src/test/java/org/tests/model/json/TestJacksonPlainBean.java @@ -1,11 +1,9 @@ package org.tests.model.json; import io.ebean.DB; -import org.ebeantest.LoggedSqlCollector; +import io.ebeantest.LoggedSql; import org.junit.Test; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; public class TestJacksonPlainBean { @@ -18,7 +16,7 @@ public class TestJacksonPlainBean { bean.setAttr("n0"); DB.save(bean); - LoggedSqlCollector.start(); + LoggedSql.start(); bean.setAttr("n1"); DB.save(bean); expectedSql(0, "update ebasic_plain set attr=?, version=? where id=? and version=?"); @@ -32,14 +30,14 @@ public class TestJacksonPlainBean { DB.save(found); expectedSql(1, "update ebasic_plain set attr=?, version=? where id=? and version=?"); - LoggedSqlCollector.stop(); + LoggedSql.stop(); } @Test public void insertUpdate() { DB.getDefault(); - LoggedSqlCollector.start(); + LoggedSql.start(); PlainBean content = new PlainBean("foo", 42); EBasicPlain bean = new EBasicPlain(); @@ -87,11 +85,11 @@ public class TestJacksonPlainBean { DB.save(found); expectedSql(0, "update ebasic_plain set plain_bean2=?, version=? where id=? and version=?"); - LoggedSqlCollector.stop(); + LoggedSql.stop(); } private void expectedSql(int i, String s) { - assertThat(LoggedSqlCollector.current().get(i)).contains(s); + assertThat(LoggedSql.collect().get(i)).contains(s); } }