diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 75112ab07a..d497656020 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -265,7 +265,7 @@ public GeoTypeBinder geoTypeBinder() { @Override public ScalarType dbMapType() { - return hstoreSupport() ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, false); + return hstoreSupport() ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, MutationDetection.DEFAULT); } @Override @@ -322,17 +322,17 @@ public ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength) { } Type genericType = prop.genericType(); if (type.equals(List.class) && isValueTypeSimple(genericType)) { - return ScalarTypeJsonList.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop)); + return ScalarTypeJsonList.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), collectionMutationDetection(prop)); } if (type.equals(Set.class) && isValueTypeSimple(genericType)) { - return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop)); + return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), collectionMutationDetection(prop)); } if (type.equals(Map.class) && isBuiltinJsonMap(genericType)) { Type keyType = TypeReflectHelper.getMapKeyTypeRaw(genericType); if (isEnumType(keyType)) { - return enumJsonMapType(postgres, dbType, keyType, keepSource(prop)); + return enumJsonMapType(postgres, dbType, keyType, collectionMutationDetection(prop)); } - return ScalarTypeJsonMap.typeFor(postgres, dbType, keepSource(prop)); + return ScalarTypeJsonMap.typeFor(postgres, dbType, collectionMutationDetection(prop)); } if (objectMapperPresent && prop.mutationDetection() == MutationDetection.DEFAULT) { ScalarTypeSet typeSet = typeSets.get(type); @@ -343,18 +343,25 @@ public ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength) { return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT); } - private boolean keepSource(DeployProperty prop) { - if (prop.mutationDetection() == MutationDetection.DEFAULT) { - prop.setMutationDetection(jsonManager != null ? jsonManager.mutationDetection() : MutationDetection.NONE); - } - return prop.mutationDetection() == MutationDetection.SOURCE; + /** + * Return the mutation detection mode to use for the built-in JSON collection types + * (Map, List, Set). + *

+ * Unlike {@code @DbJson} properties handled via the Jackson ObjectMapper, {@code DEFAULT} + * on these collection types is not resolved against the DatabaseConfig wide + * default - it always uses the legacy ModifyAware wrapper based dirty checking. Only an + * explicit {@code NONE}, {@code HASH} or {@code SOURCE} on the property itself switches + * these types away from ModifyAware based checking. + */ + private MutationDetection collectionMutationDetection(DeployProperty prop) { + return prop.mutationDetection(); } @SuppressWarnings("unchecked") - private ScalarType enumJsonMapType(boolean postgres, int dbType, Type keyType, boolean keepSource) { + private ScalarType enumJsonMapType(boolean postgres, int dbType, Type keyType, MutationDetection mutationDetection) { Class> enumClass = asEnumClass(keyType); ScalarType> enumScalarType = (ScalarType>) enumType(enumClass, null); - return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, keepSource); + return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, mutationDetection); } private DocPropertyType docPropertyType(DeployProperty prop, Class type) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java index 5df3364410..636d55b01b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.type; +import io.ebean.annotation.MutationDetection; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; @@ -27,15 +28,14 @@ private DocPropertyType docType(Type valueType) { @Override public ScalarType typeFor(Type valueType, boolean nullable) { if (valueType.equals(UUID.class)) { - // TODO: keepSource for @DbArray? - return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID); + return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, ArrayElementConverter.UUID); } - return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false); + return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, MutationDetection.DEFAULT); } @Override public ScalarType typeForEnum(ScalarType scalarType, boolean nullable) { final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType); - return new ScalarTypeJsonList.VarcharWithConverter(scalarType.docType(), nullable, false, converter); + return new ScalarTypeJsonList.VarcharWithConverter(scalarType.docType(), nullable, converter); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java index 314efc4747..b448829e4b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.type; +import io.ebean.annotation.MutationDetection; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; @@ -27,15 +28,14 @@ private DocPropertyType docType(Type valueType) { @Override public ScalarType typeFor(Type valueType, boolean nullable) { if (valueType.equals(UUID.class)) { - // TODO: keepSource for @DbArray? - return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID); + return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, ArrayElementConverter.UUID); } - return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false); + return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, MutationDetection.DEFAULT); } @Override public ScalarType typeForEnum(ScalarType scalarType, boolean nullable) { final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType); - return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.docType(), nullable, false, converter); + return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.docType(), nullable, converter); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java index 052638444a..fa2e2289cc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonCollectionValue.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.type; +import io.ebean.annotation.MutationDetection; import io.ebean.core.type.DocPropertyType; /** @@ -10,9 +11,9 @@ */ abstract class ScalarTypeJsonCollectionValue extends ScalarTypeJsonValue implements ScalarTypeArray { - ScalarTypeJsonCollectionValue(Class type, int jdbcType, JsonStorage storage, boolean keepSource, + ScalarTypeJsonCollectionValue(Class type, int jdbcType, JsonStorage storage, MutationDetection mutationDetection, boolean nullable, DocPropertyType docType) { - super(type, jdbcType, storage, keepSource, nullable, "[]", docType); + super(type, jdbcType, storage, mutationDetection, nullable, "[]", docType); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java index d994b214d0..5ad7326f37 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonList.java @@ -2,6 +2,7 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; +import io.ebean.annotation.MutationDetection; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.PostgresHelper; @@ -25,20 +26,20 @@ class ScalarTypeJsonList extends ScalarTypeJsonCollectionValue { /** * Return the appropriate ScalarType for the requested dbType and platform. */ - static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) { + static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) { if (postgres) { switch (dbType) { case DbPlatformType.JSONB: - return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource); + return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, mutationDetection); case DbPlatformType.JSON: - return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource); + return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, mutationDetection); } } - return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); + return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, mutationDetection); } - ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) { - super(List.class, jdbcType, storage, keepSource, nullable, docType); + ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) { + super(List.class, jdbcType, storage, mutationDetection, nullable, docType); } @Override @@ -87,8 +88,8 @@ static final class VarcharWithConverter extends ScalarTypeJsonList { private final ArrayElementConverter converter; - VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { - super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); + VarcharWithConverter(DocPropertyType docType, boolean nullable, ArrayElementConverter converter) { + super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, MutationDetection.DEFAULT); this.converter = converter; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java index 52374a6d97..f743a04dc8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMap.java @@ -2,6 +2,7 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; +import io.ebean.annotation.MutationDetection; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.PostgresHelper; @@ -22,8 +23,8 @@ class ScalarTypeJsonMap extends ScalarTypeJsonValue { /** * Return the ScalarType for the requested dbType and platform. */ - static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, boolean keepSource) { - return new ScalarTypeJsonMap(storageFor(postgres, dbType), keepSource); + static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, MutationDetection mutationDetection) { + return new ScalarTypeJsonMap(storageFor(postgres, dbType), mutationDetection); } /** @@ -47,8 +48,8 @@ static JsonStorage storageFor(boolean postgres, int dbType) { } } - ScalarTypeJsonMap(JsonStorage storage, boolean keepSource) { - super(Map.class, storage.jdbcType(), storage, keepSource, true, null, DocPropertyType.OBJECT); + ScalarTypeJsonMap(JsonStorage storage, MutationDetection mutationDetection) { + super(Map.class, storage.jdbcType(), storage, mutationDetection, true, null, DocPropertyType.OBJECT); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java index 3775467d65..d29a06d607 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonMapEnum.java @@ -2,6 +2,7 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; +import io.ebean.annotation.MutationDetection; import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebean.text.json.EJson; @@ -23,13 +24,13 @@ final class ScalarTypeJsonMapEnum> extends ScalarTypeJsonMap { private final ScalarType enumType; - static ScalarType typeFor(boolean postgres, int dbType, ScalarType> enumType, boolean keepSource) { - return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, keepSource); + static ScalarType typeFor(boolean postgres, int dbType, ScalarType> enumType, MutationDetection mutationDetection) { + return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, mutationDetection); } @SuppressWarnings("unchecked") - private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType enumType, boolean keepSource) { - super(storage, keepSource); + private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType enumType, MutationDetection mutationDetection) { + super(storage, mutationDetection); this.enumType = (ScalarType) enumType; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java index eb54f990f8..144a06138d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java @@ -2,6 +2,7 @@ import io.avaje.json.JsonReader; import io.avaje.json.JsonWriter; +import io.ebean.annotation.MutationDetection; import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.PostgresHelper; @@ -25,20 +26,20 @@ class ScalarTypeJsonSet extends ScalarTypeJsonCollectionValue { /** * Return the appropriate ScalarType for the requested dbType and platform. */ - static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) { + static ScalarType typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) { if (postgres) { switch (dbType) { case DbPlatformType.JSONB: - return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource); + return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, mutationDetection); case DbPlatformType.JSON: - return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource); + return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, mutationDetection); } } - return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); + return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, mutationDetection); } - ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) { - super(Set.class, jdbcType, storage, keepSource, nullable, docType); + ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) { + super(Set.class, jdbcType, storage, mutationDetection, nullable, docType); } @Override @@ -89,8 +90,8 @@ static final class VarcharWithConverter extends ScalarTypeJsonSet { private final ArrayElementConverter converter; - VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { - super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource); + VarcharWithConverter(DocPropertyType docType, boolean nullable, ArrayElementConverter converter) { + super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, MutationDetection.DEFAULT); this.converter = converter; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java index 81159c8c65..d82c85ead8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonValue.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.type; +import io.ebean.annotation.MutationDetection; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; import io.ebean.core.type.DocPropertyType; @@ -20,11 +21,18 @@ * backed {@code EJson} facade) * * This removes the previous explosion of per-storage and per-platform subclasses. + *

+ * Mutation detection: {@code DEFAULT} uses the legacy ModifyAware wrapper based dirty + * checking. {@code NONE} disables dirty checking entirely (mutable() is false and the + * property is only included in an update when explicitly set). {@code HASH} and + * {@code SOURCE} are handled via {@code BeanPropertyJsonMapper} (json content is kept + * for the read/bind round trip so that a checksum or the source content can be compared). */ abstract class ScalarTypeJsonValue extends ScalarTypeBase { protected final JsonStorage storage; protected final boolean keepSource; + private final boolean mutable; private final boolean nullable; private final String emptyJson; private final DocPropertyType docType; @@ -33,11 +41,12 @@ abstract class ScalarTypeJsonValue extends ScalarTypeBase { * @param emptyJson JSON bound when the value is null and the property is not nullable * (e.g. {@code "[]"} for collections), or null to always bind SQL null. */ - ScalarTypeJsonValue(Class type, int jdbcType, JsonStorage storage, boolean keepSource, + ScalarTypeJsonValue(Class type, int jdbcType, JsonStorage storage, MutationDetection mutationDetection, boolean nullable, String emptyJson, DocPropertyType docType) { super(type, false, jdbcType); this.storage = storage; - this.keepSource = keepSource; + this.keepSource = mutationDetection == MutationDetection.HASH || mutationDetection == MutationDetection.SOURCE; + this.mutable = mutationDetection != MutationDetection.NONE; this.nullable = nullable; this.emptyJson = emptyJson; this.docType = docType; @@ -51,7 +60,7 @@ abstract class ScalarTypeJsonValue extends ScalarTypeBase { @Override public final boolean mutable() { - return true; + return mutable; } @Override diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java index 27d8a7698a..ffff3b6fde 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/type/ScalarTypeJsonListTest.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.type; +import io.ebean.annotation.MutationDetection; import io.ebean.config.dbplatform.ExtraDbTypes; import io.ebean.core.type.DocPropertyType; import org.junit.jupiter.api.Test; @@ -11,19 +12,19 @@ public class ScalarTypeJsonListTest extends BasePlatformArrayTypeFactoryTest { @Test public void typeFor_expect_nullToEmpty_when_postgresNonNull() throws SQLException { - assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, false)); - assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, false)); + assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT)); + assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT)); - assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, false)); + assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT)); } @Test public void typeFor_expect_nullToNull_when_nullable() throws SQLException { - assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, false)); - assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, false)); + assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT)); + assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT)); - assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, false)); + assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT)); } } diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJsonMapMutationDetection.java b/ebean-test/src/test/java/org/tests/json/TestDbJsonMapMutationDetection.java new file mode 100644 index 0000000000..a3ecefeee7 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/json/TestDbJsonMapMutationDetection.java @@ -0,0 +1,148 @@ +package org.tests.json; + +import io.ebean.BeanState; +import io.ebean.DB; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import io.ebean.xtest.IgnorePlatform; +import io.ebean.annotation.Platform; +import org.junit.jupiter.api.Test; +import org.tests.model.json.EBasicJsonMapMutation; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@code Map} @DbJson properties honour mutationDetection + * rather than always using ModifyAware based dirty checking - see issue #2840. + */ +public class TestDbJsonMapMutationDetection extends BaseTestCase { + + private static Map content(String value) { + Map map = new LinkedHashMap<>(); + map.put("field", List.of(value)); + return map; + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void noneMode_inPlaceMutation_notDetected() { + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setNoneMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + // mutate the map content directly (without calling the setter) + found.getNoneMap().put("field", List.of("mutated")); + + LoggedSql.start(); + DB.save(found); + // NONE means no attempt to detect mutation - so no update at all + assertThat(LoggedSql.stop()).isEmpty(); + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void noneMode_setEqualValue_noUpdate() { + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setNoneMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + // set a brand new (but equal content) map instance via the setter + found.setNoneMap(content("a")); + + BeanState state = DB.beanState(found); + assertThat(state.changedProps()).isEmpty(); + + LoggedSql.start(); + DB.save(found); + assertThat(LoggedSql.stop()).isEmpty(); + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void noneMode_setDifferentValue_updates() { + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setNoneMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + found.setNoneMap(content("b")); + + LoggedSql.start(); + DB.save(found); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("none_map"); + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void hashMode_inPlaceMutation_detected() { + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setHashMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + found.getHashMap().put("field", List.of("mutated")); + + LoggedSql.start(); + DB.save(found); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("hash_map"); + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void hashMode_setEqualValue_noUpdate() { + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setHashMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + // brand new map instance with equal content - hash should match so no update + found.setHashMap(content("a")); + + LoggedSql.start(); + DB.save(found); + assertThat(LoggedSql.stop()).isEmpty(); + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void sourceMode_setEqualValue_noUpdate() { + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setSourceMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + found.setSourceMap(content("a")); + + LoggedSql.start(); + DB.save(found); + assertThat(LoggedSql.stop()).isEmpty(); + } + + @IgnorePlatform(Platform.MYSQL) + @Test + public void defaultMode_inPlaceMutation_stillDetectedViaModifyAware() { + // unchanged legacy behaviour - DEFAULT mode keeps using ModifyAware wrapper dirty checking + EBasicJsonMapMutation bean = new EBasicJsonMapMutation(); + bean.setDefaultMap(content("a")); + DB.save(bean); + + EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId()); + found.getDefaultMap().put("field", List.of("mutated")); + + LoggedSql.start(); + DB.save(found); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("default_map"); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/json/EBasicJsonMapMutation.java b/ebean-test/src/test/java/org/tests/model/json/EBasicJsonMapMutation.java new file mode 100644 index 0000000000..97b9cf2964 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/json/EBasicJsonMapMutation.java @@ -0,0 +1,87 @@ +package org.tests.model.json; + +import io.ebean.annotation.DbJson; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import java.util.Map; + +import static io.ebean.annotation.MutationDetection.HASH; +import static io.ebean.annotation.MutationDetection.NONE; +import static io.ebean.annotation.MutationDetection.SOURCE; + +/** + * Entity with {@code Map} @DbJson properties covering each + * {@code MutationDetection} mode - used to verify that the built-in Map JSON + * type honours mutationDetection rather than always using ModifyAware checking. + */ +@Entity +public class EBasicJsonMapMutation { + + @Id + Long id; + + @DbJson + Map defaultMap; + + @DbJson(mutationDetection = NONE) + Map noneMap; + + @DbJson(mutationDetection = HASH) + Map hashMap; + + @DbJson(mutationDetection = SOURCE) + Map sourceMap; + + @Version + Long version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Map getDefaultMap() { + return defaultMap; + } + + public void setDefaultMap(Map defaultMap) { + this.defaultMap = defaultMap; + } + + public Map getNoneMap() { + return noneMap; + } + + public void setNoneMap(Map noneMap) { + this.noneMap = noneMap; + } + + public Map getHashMap() { + return hashMap; + } + + public void setHashMap(Map hashMap) { + this.hashMap = hashMap; + } + + public Map getSourceMap() { + return sourceMap; + } + + public void setSourceMap(Map sourceMap) { + this.sourceMap = sourceMap; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +}