mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-11-12 22:41:35 +00:00
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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-14
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+9
-46
@@ -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;
|
||||
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
+8
-4
@@ -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();
|
||||
|
||||
+76
-6
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user