mirror of
https://github.com/ebean-orm/ebean.git
synced 2024-04-21 10:51:47 +00:00
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.
This commit is contained in:
@@ -2022,7 +2022,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
public boolean isTableManaged(String tableName) {
|
||||
return owner.isTableManaged(tableName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the order column property.
|
||||
*/
|
||||
@@ -3200,7 +3200,7 @@ public class BeanDescriptor<T> implements BeanType<T>, 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,7 @@ public class DataBind implements DataBinder {
|
||||
private List<InputStream> 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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
+48
-4
@@ -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<T> extends ScalarTypeBase<T> {
|
||||
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user