From da6f7d671b8b777ca3f42f2534b307d516060280 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Tue, 31 Aug 2021 14:26:52 +1200 Subject: [PATCH] #2336 - @DbArray List or Set of UUID or enum returned as String (not converted) when DB does not support ARRAY --- .../server/type/DefaultTypeManager.java | 32 ++++--------- .../type/PlatformArrayTypeJsonList.java | 41 ++++++++++++++++ .../server/type/PlatformArrayTypeJsonSet.java | 41 ++++++++++++++++ .../server/type/ScalarTypeJsonList.java | 48 ++++++++++++++++++- .../server/type/ScalarTypeJsonSet.java | 47 +++++++++++++++++- .../tests/model/array/TestDbArray_asSet.java | 10 ++-- .../tests/model/array/TestDbArray_basic.java | 9 ++-- 7 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java 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 d58c284f3..5d95efd6e 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 @@ -162,30 +162,22 @@ public final class DefaultTypeManager implements TypeManager { } } - /** - * Return the factory to use to support DB ARRAY types. - */ private PlatformArrayTypeFactory arrayTypeListFactory(DatabasePlatform databasePlatform) { if (databasePlatform.isNativeArrayType()) { return ScalarTypeArrayList.factory(); } else if (databasePlatform.isPlatform(Platform.H2)) { return ScalarTypeArrayListH2.factory(); } - // not supported for this DB platform - return null; + return new PlatformArrayTypeJsonList(); } - /** - * Return the factory to use to support DB ARRAY types. - */ private PlatformArrayTypeFactory arrayTypeSetFactory(DatabasePlatform databasePlatform) { if (databasePlatform.isNativeArrayType()) { return ScalarTypeArraySet.factory(); } else if (databasePlatform.isPlatform(Platform.H2)) { return ScalarTypeArraySetH2.factory(); } - // not supported for this DB platform - return null; + return new PlatformArrayTypeJsonSet(); } /** @@ -305,25 +297,17 @@ public final class DefaultTypeManager implements TypeManager { } private ScalarType getArrayScalarTypeSet(Type valueType, boolean nullable) { - if (arrayTypeSetFactory != null) { - if (isEnumType(valueType)) { - return arrayTypeSetFactory.typeForEnum(createEnumScalarType(asEnumClass(valueType), null), nullable); - } - return arrayTypeSetFactory.typeFor(valueType, nullable); + if (isEnumType(valueType)) { + return arrayTypeSetFactory.typeForEnum(createEnumScalarType(asEnumClass(valueType), null), nullable); } - // fallback to JSON storage in VARCHAR column - return new ScalarTypeJsonSet.Varchar(getDocType(valueType), nullable, false); // TODO: keepSource for @DbArray? + return arrayTypeSetFactory.typeFor(valueType, nullable); } private ScalarType getArrayScalarTypeList(Type valueType, boolean nullable) { - if (arrayTypeListFactory != null) { - if (isEnumType(valueType)) { - return arrayTypeListFactory.typeForEnum(createEnumScalarType(asEnumClass(valueType), null), nullable); - } - return arrayTypeListFactory.typeFor(valueType, nullable); + if (isEnumType(valueType)) { + return arrayTypeListFactory.typeForEnum(createEnumScalarType(asEnumClass(valueType), null), nullable); } - // fallback to JSON storage in VARCHAR column - return new ScalarTypeJsonList.Varchar(getDocType(valueType), nullable, false); // TODO: keepSource for @DbArray? + return arrayTypeListFactory.typeFor(valueType, nullable); } private Class> asEnumClass(Type valueType) { 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 new file mode 100644 index 000000000..8b3232b28 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonList.java @@ -0,0 +1,41 @@ +package io.ebeaninternal.server.type; + +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.ScalarType; + +import java.lang.reflect.Type; +import java.util.UUID; + +/** + * Fallback for DbArray as Json List. + */ +class PlatformArrayTypeJsonList implements PlatformArrayTypeFactory { + + private DocPropertyType docType(Type valueType) { + if (valueType.equals(Long.class)) { + return DocPropertyType.LONG; + } + if (valueType.equals(Integer.class)) { + return DocPropertyType.INTEGER; + } + if (valueType.equals(Double.class)) { + return DocPropertyType.DOUBLE; + } + return DocPropertyType.TEXT; + } + + @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.Varchar(docType(valueType), nullable, false); + } + + @Override + public ScalarType typeForEnum(ScalarType scalarType, boolean nullable) { + final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType); + return new ScalarTypeJsonList.VarcharWithConverter(scalarType.getDocType(), nullable, false, 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 new file mode 100644 index 000000000..a5c7595f7 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/PlatformArrayTypeJsonSet.java @@ -0,0 +1,41 @@ +package io.ebeaninternal.server.type; + +import io.ebean.core.type.DocPropertyType; +import io.ebean.core.type.ScalarType; + +import java.lang.reflect.Type; +import java.util.UUID; + +/** + * Fallback for DbArray as Json Set. + */ +class PlatformArrayTypeJsonSet implements PlatformArrayTypeFactory { + + private DocPropertyType docType(Type valueType) { + if (valueType.equals(Long.class)) { + return DocPropertyType.LONG; + } + if (valueType.equals(Integer.class)) { + return DocPropertyType.INTEGER; + } + if (valueType.equals(Double.class)) { + return DocPropertyType.DOUBLE; + } + return DocPropertyType.TEXT; + } + + @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.Varchar(docType(valueType), nullable, false); + } + + @Override + public ScalarType typeForEnum(ScalarType scalarType, boolean nullable) { + final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType); + return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.getDocType(), nullable, false, converter); + } +} 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 71179dba7..4abd85931 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 @@ -9,11 +9,13 @@ import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebean.text.json.EJson; +import io.ebeaninternal.json.ModifyAwareList; import javax.persistence.PersistenceException; import java.io.IOException; import java.sql.SQLException; import java.sql.Types; +import java.util.ArrayList; import java.util.List; /** @@ -36,6 +38,46 @@ final class ScalarTypeJsonList { return new ScalarTypeJsonList.Varchar(docType, nullable, keepSource); } + @SuppressWarnings("rawtypes") + static final class VarcharWithConverter extends ScalarTypeJsonList.Base { + private final ArrayElementConverter converter; + + VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { + super(Types.VARCHAR, docType, nullable, keepSource); + this.converter = converter; + } + + @Override + List readJsonConvert(String json) { + try { + return convertElements(EJson.parseList(json, false)); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as List", json, e); + } + } + + @SuppressWarnings("unchecked") + private List convertElements(List rawList) { + if (rawList == null) { + return null; + } + final List result = new ArrayList<>(rawList.size()); + for (Object o : rawList) { + result.add(converter.fromSerialized(o)); + } + return new ModifyAwareList(result); + } + + @Override + public List parse(String value) { + try { + return convertElements(EJson.parseList(value, false)); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as List", value, e); + } + } + } + /** * List mapped to DB VARCHAR. */ @@ -86,6 +128,10 @@ final class ScalarTypeJsonList { if (keepSource) { reader.pushJson(json); } + return readJsonConvert(json); + } + + List readJsonConvert(String json) { try { // parse JSON into modifyAware list return EJson.parseList(json, true); @@ -130,7 +176,7 @@ final class ScalarTypeJsonList { } @Override - public final List parse(String value) { + public List parse(String value) { try { return EJson.parseList(value, false); } catch (IOException e) { 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 50cc7f0b3..354d793d7 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 @@ -9,6 +9,7 @@ import io.ebean.core.type.DocPropertyType; import io.ebean.core.type.ScalarType; import io.ebean.text.TextException; import io.ebean.text.json.EJson; +import io.ebeaninternal.json.ModifyAwareSet; import javax.persistence.PersistenceException; import java.io.IOException; @@ -38,6 +39,45 @@ final class ScalarTypeJsonSet { return new ScalarTypeJsonSet.Varchar(docPropertyType, nullable, keepSource); } + @SuppressWarnings("rawtypes") + static final class VarcharWithConverter extends ScalarTypeJsonSet.Base { + private final ArrayElementConverter converter; + + VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) { + super(Types.VARCHAR, docType, nullable, keepSource); + this.converter = converter; + } + + @Override + Set readJsonConvert(String json) { + try { + return convertElements(EJson.parseSet(json, false)); + } catch (IOException e) { + throw new TextException("Failed to parse JSON [{}] as List", json, e); + } + } + + @SuppressWarnings("unchecked") + private Set convertElements(Set rawSet) { + if (rawSet == null) { + return null; + } + final Set result = new LinkedHashSet(rawSet.size()); + for (Object o : rawSet) { + result.add(converter.fromSerialized(o)); + } + return new ModifyAwareSet(result); + } + + @Override + public Set parse(String value) { + try { + return convertElements(EJson.parseSet(value, false)); + } catch (IOException e) { + throw new PersistenceException("Failed to parse JSON content as Set: [" + value + "]", e); + } + } + } /** * List mapped to DB VARCHAR. */ @@ -86,8 +126,11 @@ final class ScalarTypeJsonSet { if (keepSource) { reader.pushJson(json); } + return readJsonConvert(json); + } + + Set readJsonConvert(String json) { try { - // parse JSON into modifyAware list return EJson.parseSet(json, true); } catch (IOException e) { throw new TextException("Failed to parse JSON [{}] as Set", json, e); @@ -130,7 +173,7 @@ final class ScalarTypeJsonSet { } @Override - public final Set parse(String value) { + public Set parse(String value) { try { return convertList(EJson.parseList(value)); } catch (IOException e) { diff --git a/ebean-core/src/test/java/org/tests/model/array/TestDbArray_asSet.java b/ebean-core/src/test/java/org/tests/model/array/TestDbArray_asSet.java index 36b917c36..5c1791056 100644 --- a/ebean-core/src/test/java/org/tests/model/array/TestDbArray_asSet.java +++ b/ebean-core/src/test/java/org/tests/model/array/TestDbArray_asSet.java @@ -179,11 +179,13 @@ public class TestDbArray_asSet extends BaseTestCase { Ebean.save(bean); // load cache - Ebean.find(EArraySetBean.class, bean.getId()); + final EArraySetBean entry = Ebean.find(EArraySetBean.class, bean.getId()); + assertThat(entry.getUids()).hasSameElementsAs(uids); + assertThat(entry.getStatus()).hasSameElementsAs(statuses); + // hit cache EArraySetBean found = Ebean.find(EArraySetBean.class, bean.getId()); - - assertThat(found.getUids()).isEqualTo(uids); - assertThat(found.getStatus()).isEqualTo(statuses); + assertThat(found.getUids()).hasSameElementsAs(uids); + assertThat(found.getStatus()).hasSameElementsAs(statuses); } } diff --git a/ebean-core/src/test/java/org/tests/model/array/TestDbArray_basic.java b/ebean-core/src/test/java/org/tests/model/array/TestDbArray_basic.java index a3b5ad3d0..f6c7b7da1 100644 --- a/ebean-core/src/test/java/org/tests/model/array/TestDbArray_basic.java +++ b/ebean-core/src/test/java/org/tests/model/array/TestDbArray_basic.java @@ -268,11 +268,12 @@ public class TestDbArray_basic extends BaseTestCase { DB.save(bean); // load cache - DB.find(EArrayBean.class, bean.getId()); + final EArrayBean entry = DB.find(EArrayBean.class, bean.getId()); + assertThat(entry.getUids()).hasSameElementsAs(uids); + assertThat(entry.getStatuses()).hasSameElementsAs(statuses); // hit cache EArrayBean found = DB.find(EArrayBean.class, bean.getId()); - - assertThat(found.getUids()).isEqualTo(uids); - assertThat(found.getStatuses()).isEqualTo(statuses); + assertThat(found.getUids()).hasSameElementsAs(uids); + assertThat(found.getStatuses()).hasSameElementsAs(statuses); } }