Merge pull request #2274 from ebean-orm/feature/json-dirty-detection-md5

Add support for JSON bean mutation detection via HASH and SOURCE of JSON string content
This commit is contained in:
Rob Bygrave authored and GitHub committed 2021-07-30 21:11:42 +12:00
commit 11dc7f64ba
33 files changed
+1040 -166

No files matched your search

@@ -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.
*/
@@ -3198,9 +3198,9 @@ public class BeanDescriptor<T> implements BeanType<T>, 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)) {
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);
@@ -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.MutableValueInfo;
import io.ebean.bean.PersistenceContext;
import io.ebean.config.EncryptKey;
import io.ebean.config.dbplatform.DbEncryptFunction;
@@ -13,6 +14,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 +33,7 @@ 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.util.ValueUtil;
import io.ebeanservice.docstore.api.mapping.DocMappingBuilder;
import io.ebeanservice.docstore.api.mapping.DocPropertyMapping;
@@ -632,6 +630,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
setValue(bean, value);
}
return value;
} catch (TextException e) {
throw e;
} catch (Exception e) {
throw new PersistenceException("Error readSet on " + descriptor + "." + name, e);
}
@@ -642,15 +642,7 @@ 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);
}
@SuppressWarnings("unchecked")
@@ -828,6 +820,13 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
return scalarType.parse(value);
}
/**
* creates a mutableHash for the given JSON value.
*/
public MutableValueInfo createMutableInfo(String json) {
throw new UnsupportedOperationException();
}
/**
* Read the value for this property from L2 cache entry and set it to the bean.
* <p>
@@ -909,7 +908,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
/**
* Return the name of the property.
*/
@Override @Nonnull
@Override
@Nonnull
public String getName() {
return name;
}
@@ -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) {
return scalarType.isDirty(value);
boolean checkMutable(Object value, boolean alreadyDirty, EntityBeanIntercept ebi) {
return alreadyDirty || value != null && scalarType.isDirty(value);
}
/**
@@ -1281,7 +1281,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
@Override
public Object localEncrypt(Object value) {
return ((LocalEncryptedType)scalarType).localEncrypt(value);
return ((LocalEncryptedType) scalarType).localEncrypt(value);
}
/**
@@ -1394,7 +1394,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
/**
* Return the property type.
*/
@Override @Nonnull
@Override
@Nonnull
public Class<?> getPropertyType() {
return propertyType;
}
@@ -0,0 +1,195 @@
package io.ebeaninternal.server.deploy;
import io.ebean.annotation.MutationDetection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
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;
import io.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import io.ebeaninternal.server.util.Checksum;
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 final boolean sourceDetection;
public BeanPropertyJsonMapper(BeanDescriptor<?> desc, DeployBeanProperty deployProp) {
super(desc, deployProp);
this.sourceDetection = deployProp.getMutationDetection() == MutationDetection.SOURCE;
}
@Override
public MutableValueInfo createMutableInfo(String json) {
if (sourceDetection) {
return new SourceMutableValue(scalarType, json);
} else {
return new ChecksumMutableValue(scalarType, json);
}
}
/**
* Next when no prior MutableValueInfo.
*/
private MutableValueNext next(String json) {
if (sourceDetection) {
return new SourceMutableValue(scalarType, json);
} else {
return new NextPair(json, new ChecksumMutableValue(scalarType, json));
}
}
/**
* Return true if the json property is considered dirty.
*/
@Override
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);
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;
}
// 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;
}
@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 MutableValueInfo hash = createMutableInfo(json);
bean._ebean_getIntercept().mutableInfo(propertyIndex, hash);
}
}
return value;
} catch (TextException e) {
throw e;
} catch (Exception e) {
throw new PersistenceException("Error readSet on " + descriptor + "." + name, e);
}
}
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 to use for dirty detection.
* <p>
* Does not support rebuilding 'oldValue' as no original json content.
*/
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.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 Checksum.checksum(parent.format(obj)) == checksum;
}
@Override
public Object get() {
return null; // cannot create object from json
}
}
/**
* Hold json source content. This supports rebuilding the 'oldValue'.
*/
private static final class SourceMutableValue implements MutableValueInfo, MutableValueNext {
private final String originalJson;
private final ScalarType<?> parent;
SourceMutableValue(ScalarType<?> parent, String json) {
this.parent = parent;
this.originalJson = json;
}
@Override
public MutableValueNext nextDirty(String json) {
return Objects.equals(originalJson, json) ? null : new SourceMutableValue(parent, json);
}
@Override
public boolean isEqualToObject(Object obj) {
return Objects.equals(originalJson, parent.format(obj));
}
@Override
public Object get() {
// rebuild the 'oldValue' for change log etc
return parent.parse(originalJson);
}
@Override
public String content() {
return originalJson;
}
@Override
public MutableValueInfo info() {
return this;
}
}
}
@@ -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,6 +96,7 @@ public class DeployBeanProperty {
private boolean jsonSerialize = true;
private boolean jsonDeserialize = true;
private MutationDetection mutationDetection;
private boolean dbEncrypted;
private DbEncryptFunction dbEncryptFunction;
@@ -327,6 +315,17 @@ public class DeployBeanProperty {
this.jsonDeserialize = jsonDeserialize;
}
public MutationDetection getMutationDetection() {
if (mutationDetection == null) {
mutationDetection = MutationDetection.DEFAULT;
}
return mutationDetection;
}
public void setMutationDetection(MutationDetection dirtyDetection) {
this.mutationDetection = dirtyDetection;
}
/**
* Return the sortOrder for the properties.
*/
@@ -1201,4 +1200,8 @@ public class DeployBeanProperty {
return false;
}
boolean isJsonMapper() {
return scalarType != null && scalarType.isJsonMapper();
}
}
@@ -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;
@@ -30,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<String, BeanProperty> propertyMap;
private BeanProperty id;
private final List<BeanProperty> local = new ArrayList<>();
private final List<BeanProperty> mutable = new ArrayList<>();
private final List<BeanPropertyAssocMany<?>> manys = new ArrayList<>();
private final List<BeanProperty> nonManys = new ArrayList<>();
private final List<BeanProperty> aggs = new ArrayList<>();
private final List<BeanPropertyAssocOne<?>> ones = new ArrayList<>();
private final List<BeanPropertyAssocOne<?>> onesImported = new ArrayList<>();
private final List<BeanPropertyAssocOne<?>> embedded = new ArrayList<>();
private final List<BeanProperty> baseScalar = new ArrayList<>();
private final List<BeanProperty> transients = new ArrayList<>();
private final List<BeanProperty> nonTransients = new ArrayList<>();
private final BeanPropertyAssocOne<?> unidirectional;
private final BeanProperty orderColumn;
@SuppressWarnings({"unchecked"})
public DeployBeanPropertyLists(BeanDescriptorMap owner, BeanDescriptor<?> desc, DeployBeanDescriptor<?> deploy) {
this.desc = desc;
@@ -86,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<>();
@@ -127,7 +100,7 @@ public class DeployBeanPropertyLists {
}
if (orderColumn != null) {
orderColumn.setDeployOrder(order++);
orderColumn.setDeployOrder(order);
allocateToList(orderColumn);
propertyMap.put(orderColumn.getName(), orderColumn);
}
@@ -154,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) {
@@ -164,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();
@@ -180,7 +151,6 @@ public class DeployBeanPropertyLists {
return assocOne;
}
}
return null;
}
@@ -368,7 +338,6 @@ public class DeployBeanPropertyLists {
}
public BeanProperty getSoftDeleteProperty() {
for (BeanProperty prop : nonManys) {
if (prop.isSoftDelete()) {
return prop;
@@ -385,7 +354,6 @@ public class DeployBeanPropertyLists {
* Return the properties set via generated values on insert.
*/
public BeanProperty[] getGeneratedInsert() {
List<BeanProperty> list = new ArrayList<>();
for (BeanProperty prop : nonTransients) {
GeneratedProperty gen = prop.getGeneratedProperty();
@@ -400,7 +368,6 @@ public class DeployBeanPropertyLists {
* Return the properties set via generated values on update.
*/
public BeanProperty[] getGeneratedUpdate() {
List<BeanProperty> list = new ArrayList<>();
for (BeanProperty prop : nonTransients) {
GeneratedProperty gen = prop.getGeneratedProperty();
@@ -438,8 +405,7 @@ public class DeployBeanPropertyLists {
}
}
}
return (BeanPropertyAssocOne[]) list.toArray(new BeanPropertyAssocOne[0]);
return list.toArray(new BeanPropertyAssocOne<?>[0]);
}
private BeanPropertyAssocMany<?>[] getMany2Many() {
@@ -449,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) {
@@ -471,25 +436,23 @@ 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);
}
return new BeanProperty(desc, deployProp);
}
@@ -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;
@@ -213,22 +209,21 @@ 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.mutationDetection());
}
void setDbJsonBType(DeployBeanProperty prop, DbJsonB dbJsonB) {
setDbJsonType(prop, DbPlatformType.JSONB, dbJsonB.length());
setDbJsonType(prop, DbPlatformType.JSONB, dbJsonB.length(), dbJsonB.mutationDetection());
}
private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength) {
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);
if (dbType == Types.VARCHAR || dbLength > 0) {
// determine the db column size
@@ -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;
@@ -0,0 +1,42 @@
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;
/**
* 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 hash and push json
final String json = prop.format(value);
final MutableValueInfo hash = prop.createMutableInfo(json);
bean._ebean_getIntercept().mutableInfo(propertyIndex, hash);
request.pushJson(json);
request.bind(value, prop);
}
}
}
}
@@ -0,0 +1,33 @@
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 {
// update mutableInfo and push json
request.pushJson(bean._ebean_getIntercept().mutableNext(propertyIndex));
request.bind(prop.getValue(bean), 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);
}
@@ -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,11 +37,16 @@ class FactoryProperty {
if (prop.isDbEncrypted()){
return new BindableEncryptedProperty(prop, bindEncryptDataFirst);
}
if (allowManyToOne && prop instanceof BeanPropertyAssocOne) {
return new BindableAssocOne((BeanPropertyAssocOne<?>)prop);
}
if (prop instanceof BeanPropertyJsonMapper) {
if (DmlMode.INSERT == mode) {
return new BindablePropertyJsonInsert(prop);
} else if (DmlMode.UPDATE == mode) {
return new BindablePropertyJsonUpdate(prop);
}
}
return new BindableProperty(prop);
}
}
@@ -18,19 +18,15 @@ 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) {
this.ctx = ctx;
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();
}
@@ -50,34 +46,21 @@ 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 {
Object dbVal = prop.read(ctx);
if (!refreshLoading) {
prop.setValue(bean, dbVal);
} else {
prop.setValueIntercept(bean, dbVal);
}
return dbVal;
return prop.readSet(ctx, bean);
} catch (Exception e) {
bean._ebean_getIntercept().setLoadError(prop.getPropertyIndex(), e);
ctx.handleLoadError(prop.getFullBeanName(), e);
@@ -89,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);
}
}
@@ -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,19 @@ public class DataBind implements DataBinder {
this.connection = connection;
}
@Override
public void pushJson(String json) {
assert this.json == null; // we can only push one value
this.json = json;
}
@Override
public String popJson() {
String ret = json;
json = null;
return ret;
}
@Override
public StringBuilder append(Object entry) {
return bindLog.append(entry);
@@ -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);
}
/**
@@ -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();
@@ -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,17 +55,77 @@ 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<Object> {
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<Object> {
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 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 +188,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;
@@ -0,0 +1,20 @@
package io.ebeaninternal.server.util;
import java.nio.charset.StandardCharsets;
import java.util.zip.CRC32;
/**
* 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) {
CRC32 checksum = new CRC32();
final byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
checksum.update(bytes, 0, bytes.length);
return checksum.getValue();
}
}
@@ -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));
@@ -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
@@ -0,0 +1,23 @@
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(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);
}
}
@@ -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<String, ValuePair> 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() {
@@ -24,6 +50,7 @@ public class TestDbJson_Jackson3 extends BaseTestCase {
EBasicJsonJackson3 bean = new EBasicJsonJackson3();
bean.setName("b1");
bean.setPlainValue(contentBean);
bean.setPlainValue2(contentBean);
bean.save();
@@ -32,20 +59,15 @@ public class TestDbJson_Jackson3 extends BaseTestCase {
LoggedSql.start();
found.save();
List<String> 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);
// 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());
@@ -69,11 +91,138 @@ 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<PlainBean>)pair.getOldValue()).hasSize(1)
.extracting(PlainBean::getName).containsExactly("a");
LoggedSql.start();
DB.save(found);
final List<String> 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
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);
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);
// plain_bean=?, no longer included with MD5 dirty detection
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
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");
}
@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 SOURCE
assertThat(state.isDirty()).isTrue();
assertThat(state.getChangedProps()).containsExactly("plainValue");
bean.getPlainValue2().setName("b");
assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainValue", "plainValue2");
bean.getPlainValue3().setName("c"); // has mutationDetection = NONE
Map<String, ValuePair> 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();
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
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();
bean.getPlainValue2().setName("b2"); // effectively HASH mode mutation detection
bean.save();
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);
}
}
@@ -113,7 +113,8 @@ public class TestDbJson_List extends BaseTestCase {
List<String> 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<String> 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<String> 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<String> 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
@@ -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,9 +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;
@@ -45,6 +55,22 @@ public class EBasicJsonJackson3 extends Model {
this.plainValue = plainValue;
}
public PlainBeanDirtyAware getPlainValue2() {
return plainValue2;
}
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;
}
@@ -7,12 +7,10 @@ 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.HASH;
import static io.ebean.annotation.MutationDetection.SOURCE;
@Entity
public class EBasicJsonList {
@@ -25,13 +23,13 @@ public class EBasicJsonList {
@DbJson(length = 700, name = "beans")
Set<PlainBean> beanSet;
@DbJsonB
@DbJsonB(mutationDetection = HASH)
List<PlainBean> beanList;
@DbJson(length = 700)
Map<String, PlainBean> beanMap = new LinkedHashMap<>();
@DbJson(length = 500)
@DbJson(length = 500, mutationDetection = SOURCE) // such that we can rebuild old values
PlainBean plainBean;
@DbJson(length = 50)
@@ -0,0 +1,67 @@
package org.tests.model.json;
import io.ebean.annotation.DbJson;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
import static io.ebean.annotation.MutationDetection.NONE;
@Entity
public class EBasicPlain {
@Id
long id;
String attr;
@DbJson(length = 500)
PlainBean plainBean;
@DbJson(length = 500, mutationDetection = NONE) // only update when property set
PlainBean plainBean2;
@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 PlainBean getPlainBean2() {
return plainBean2;
}
public void setPlainBean2(PlainBean plainBean2) {
this.plainBean2 = plainBean2;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
}
@@ -0,0 +1,95 @@
package org.tests.model.json;
import io.ebean.DB;
import io.ebeantest.LoggedSql;
import org.junit.Test;
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);
LoggedSql.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=?");
LoggedSql.stop();
}
@Test
public void insertUpdate() {
DB.getDefault();
LoggedSql.start();
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, 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");
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=?");
// 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(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=?");
LoggedSql.stop();
}
private void expectedSql(int i, String s) {
assertThat(LoggedSql.collect().get(i)).contains(s);
}
}