Proof of concept to support also bean change detection / changelog on mutable properties

This commit is contained in:
Roland Praml committed 2021-07-28 14:54:56 +02:00
1 parent 49b86d07d3
commit 5e595ec5f8
7 files changed
+180 -16

No files matched your search

@@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.MutableJson;
import io.ebean.core.type.DataReader;
import io.ebean.text.TextException;
import io.ebeaninternal.server.deploy.meta.DeployBeanProperty;
@@ -25,11 +26,11 @@ public class BeanPropertyJsonMapper extends BeanProperty {
boolean isDirtyValue(Object value, EntityBeanIntercept ebi) {
// dirty detection based on md5 hash of json content
final String json = scalarType.jsonMapper(value);
final String newHash = Md5.hash(json);
final String oldHash = ebi.mutableHash(propertyIndex);
if (!Objects.equals(newHash, oldHash)) {
final MutableJson oldHash = ebi.mutableHash(propertyIndex);
if (oldHash == null || !oldHash.isEqualToJson(json)) {
ebi.mutableContent(propertyIndex, json); // so we only convert to json once
ebi.mutableHash(propertyIndex, newHash); // for dirty detection next time
//ebi.mutableHash(propertyIndex, scalarType.jsonMutable(json)); // for dirty detection next time
//must be done AFTER persistControllers are called.
return true;
}
return false;
@@ -43,8 +44,8 @@ public class BeanPropertyJsonMapper extends BeanProperty {
setValue(bean, value);
String json = reader.popJson();
if (json != null) {
final String hash = Md5.hash(json);
bean._ebean_getIntercept().mutableHash(propertyIndex, hash);
final String hash = scalarType.format(value);
bean._ebean_getIntercept().mutableHash(propertyIndex, scalarType.jsonMutable(hash));
}
}
return value;
@@ -32,8 +32,7 @@ class BindablePropertyJsonInsert extends BindableProperty {
} else {
// on insert store MD5 hash and push json
final String json = prop.format(value);
final String hash = Md5.hash(json);
bean._ebean_getIntercept().mutableHash(propertyIndex, hash);
bean._ebean_getIntercept().mutableHash(propertyIndex, prop.getScalarType().jsonMutable(json));
request.pushJson(json);
request.bind(value, prop);
}
@@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.introspect.AnnotatedField;
import io.ebean.bean.MutableJson;
import io.ebean.core.type.DataBinder;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.DocPropertyType;
@@ -15,6 +17,7 @@ import io.ebean.text.TextException;
import io.ebeaninternal.json.ModifyAwareList;
import io.ebeaninternal.json.ModifyAwareMap;
import io.ebeaninternal.json.ModifyAwareSet;
import io.ebeaninternal.server.util.Md5;
import javax.persistence.PersistenceException;
import java.io.DataInput;
@@ -24,6 +27,7 @@ import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
@@ -66,6 +70,66 @@ class ScalarTypeJsonObjectMapper {
public String jsonMapper(Object value) {
return formatValue(value);
}
private class Md5MutableJson implements MutableJson {
private String md5;
Md5MutableJson(String json) {
md5 = Md5.hash(json);
}
@Override
public boolean isEqualToObject(Object obj) {
return true; // we cannot determine differences...
}
@Override
public boolean isEqualToJson(String json) {
return Md5.hash(json).equals(md5);
}
@Override
public Object get() {
return null; // cannot create object from json
}
@Override
public void update(Object obj) {
md5 = Md5.hash(format(obj));
}
}
private class PlainMutableJson implements MutableJson {
private String originalJson;
PlainMutableJson(String json) {
originalJson = json;
}
@Override
public boolean isEqualToObject(Object obj) {
return isEqualToJson(format(obj));
}
@Override
public boolean isEqualToJson(String json) {
return Objects.equals(originalJson, json);
}
@Override
public Object get() {
return parse(originalJson);
}
@Override
public void update(Object obj) {
originalJson = format(obj);
}
}
@Override
public MutableJson jsonMutable(String originalJson) {
if (false) {
return new Md5MutableJson(originalJson);
} else {
return new PlainMutableJson(originalJson);
}
}
@Override
public Object read(DataReader reader) throws SQLException {
@@ -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() {
@@ -69,12 +95,54 @@ public class TestDbJson_Jackson3 extends BaseTestCase {
found.setName("p1-mod");
found.setBeanList(null);
BeanState state = DB.getBeanState(found);
assertThat(state.getChangedProps()).containsExactlyInAnyOrder("name", "beanList");
ValuePair pair = state.getDirtyValues().get("name");
assertThat(pair.getNewValue()).isEqualTo("p1-mod");
assertThat(pair.getOldValue()).isEqualTo("p1");
pair = state.getDirtyValues().get("beanList");
assertThat(pair.getNewValue()).isEqualTo(null);
assertThat((List<PlainBean>)pair.getOldValue()).hasSize(1)
.extracting(PlainBean::getName).containsExactly("a");
LoggedSql.start();
DB.save(found);
final List<String> sql = LoggedSql.stop();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
// plain_bean=?, no longer included with MD5 dirty detection
assertThat(sql.get(0)).contains("update ebasic_json_list set name=?, bean_list=?, version=? where id=?");
assertThat(EBasicJsonListPersistController.updatedValues.entrySet())
.extracting(Map.Entry::toString)
.containsExactlyInAnyOrder("beanList=null,[name:a]","name=p1-mod,p1","version=2,1");
found.getPlainBean().setName("b");
// CHECKME: How do we get these checks to work?
// assertThat(DB.getBeanState(found).isDirty()).isTrue();
state = DB.getBeanState(found);
assertThat(state.getChangedProps()).containsExactlyInAnyOrder("plainBean");
pair = state.getDirtyValues().get("plainBean");
assertThat(pair.getNewValue()).hasToString("name:b");
assertThat(pair.getOldValue()).hasToString("name:a");
LoggedSql.start();
DB.save(found);
sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
// plain_bean=?, no longer included with MD5 dirty detection
assertThat(sql.get(0)).contains("update ebasic_json_list set plain_bean=?, version=? where id=?");
assertThat(EBasicJsonListPersistController.updatedValues.entrySet())
.extracting(Map.Entry::toString)
.containsExactlyInAnyOrder("plainBean=name:b,name:a", "version=3,2");
}
}