From 3051412d909951dff9bdb08eae3617d942e3785c Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 10 Mar 2017 20:50:07 +1300 Subject: [PATCH] #989 - ENH: Support @DbArray with Set ... (addition to the existing List support) --- src/main/java/io/ebean/text/json/EJson.java | 8 + .../server/deploy/parse/DeployUtil.java | 6 +- .../server/type/DefaultTypeManager.java | 35 +++- .../server/type/ScalarTypeArray.java | 13 ++ .../server/type/ScalarTypeArrayList.java | 2 +- .../server/type/ScalarTypeArraySet.java | 142 ++++++++++++++++ .../server/type/ScalarTypeArraySetH2.java | 68 ++++++++ .../server/type/ScalarTypeJsonSet.java | 2 +- .../org/tests/model/array/EArraySetBean.java | 91 +++++++++++ .../tests/model/array/TestDbArray_asSet.java | 152 ++++++++++++++++++ 10 files changed, 507 insertions(+), 12 deletions(-) create mode 100644 src/main/java/io/ebeaninternal/server/type/ScalarTypeArray.java create mode 100644 src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java create mode 100644 src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2.java create mode 100644 src/test/java/org/tests/model/array/EArraySetBean.java create mode 100644 src/test/java/org/tests/model/array/TestDbArray_asSet.java diff --git a/src/main/java/io/ebean/text/json/EJson.java b/src/main/java/io/ebean/text/json/EJson.java index 8c01b4fbd..28bca0dc1 100644 --- a/src/main/java/io/ebean/text/json/EJson.java +++ b/src/main/java/io/ebean/text/json/EJson.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -159,4 +160,11 @@ public class EJson { } return ((ModifyAwareList) list).asSet(); } + + /** + * Parse the json returning as a Set taking into account the current token. + */ + public static Set parseSet(JsonParser parser, JsonToken currentToken) throws IOException { + return new LinkedHashSet<>(parseList(parser, currentToken)); + } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java b/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java index 8470ee17d..214e607a1 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -17,7 +17,7 @@ import io.ebean.config.dbplatform.DbPlatformType; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; import io.ebeaninternal.server.type.DataEncryptSupport; import io.ebeaninternal.server.type.ScalarType; -import io.ebeaninternal.server.type.ScalarTypeArrayList; +import io.ebeaninternal.server.type.ScalarTypeArray; import io.ebeaninternal.server.type.ScalarTypeEnumStandard; import io.ebeaninternal.server.type.SimpleAesEncryptor; import io.ebeaninternal.server.type.TypeManager; @@ -244,8 +244,8 @@ public class DeployUtil { int dbType = scalarType.getJdbcType(); prop.setDbType(dbType); prop.setScalarType(scalarType); - if (scalarType instanceof ScalarTypeArrayList) { - prop.setDbColumnDefn(((ScalarTypeArrayList) scalarType).getDbColumnDefn()); + if (scalarType instanceof ScalarTypeArray) { + prop.setDbColumnDefn(((ScalarTypeArray) scalarType).getDbColumnDefn()); } if (dbType == Types.VARCHAR) { // determine the db column size diff --git a/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 3ab82e80d..ec03ca026 100644 --- a/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -165,7 +165,8 @@ public final class DefaultTypeManager implements TypeManager { */ private ScalarType jsonNodeJsonb; - private final PlatformArrayTypeFactory arrayTypeFactory; + private final PlatformArrayTypeFactory arrayTypeListFactory; + private final PlatformArrayTypeFactory arrayTypeSetFactory; /** * Create the DefaultTypeManager. @@ -182,7 +183,8 @@ public final class DefaultTypeManager implements TypeManager { this.extraTypeFactory = new DefaultTypeFactory(config); this.postgres = isPostgres(config.getDatabasePlatform()); - this.arrayTypeFactory = arrayTypeFactory(postgres, config.getDatabasePlatform()); + this.arrayTypeListFactory = arrayTypeListFactory(postgres, config.getDatabasePlatform()); + this.arrayTypeSetFactory = arrayTypeSetFactory(postgres, config.getDatabasePlatform()); this.offlineMigrationGeneration = DbOffline.isGenerateMigration(); @@ -203,7 +205,7 @@ public final class DefaultTypeManager implements TypeManager { /** * Return the factory to use to support DB ARRAY types. */ - private PlatformArrayTypeFactory arrayTypeFactory(boolean postgres, DatabasePlatform databasePlatform) { + private PlatformArrayTypeFactory arrayTypeListFactory(boolean postgres, DatabasePlatform databasePlatform) { if (postgres) { return ScalarTypeArrayList.factory(); } else if (databasePlatform.isPlatform(Platform.H2)) { @@ -213,6 +215,19 @@ public final class DefaultTypeManager implements TypeManager { return null; } + /** + * Return the factory to use to support DB ARRAY types. + */ + private PlatformArrayTypeFactory arrayTypeSetFactory(boolean postgres, DatabasePlatform databasePlatform) { + if (postgres) { + return ScalarTypeArraySet.factory(); + } else if (databasePlatform.isPlatform(Platform.H2)) { + return ScalarTypeArraySetH2.factory(); + } + // not supported for this DB platform + return null; + } + /** * Load custom scalar types registered via ExtraTypeFactory and ServiceLoader. */ @@ -318,13 +333,19 @@ public final class DefaultTypeManager implements TypeManager { @Override public ScalarType getArrayScalarType(Class type, DbArray dbArray, Type genericType) { + Type valueType = getValueType(genericType); if (type.equals(List.class)) { - if (arrayTypeFactory != null) { - Type valueType = getValueType(genericType); - return arrayTypeFactory.typeFor(valueType); + if (arrayTypeListFactory != null) { + return arrayTypeListFactory.typeFor(valueType); } // fallback to JSON storage in VARCHAR column - return new ScalarTypeJsonList.Varchar(getDocType(getValueType(genericType))); + return new ScalarTypeJsonList.Varchar(getDocType(valueType)); + } else if (type.equals(Set.class)) { + if (arrayTypeSetFactory != null) { + return arrayTypeSetFactory.typeFor(valueType); + } + // fallback to JSON storage in VARCHAR column + return new ScalarTypeJsonSet.Varchar(getDocType(valueType)); } throw new IllegalStateException("Type [" + type + "] not supported for @DbArray"); } diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeArray.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArray.java new file mode 100644 index 000000000..04d8c25c9 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArray.java @@ -0,0 +1,13 @@ +package io.ebeaninternal.server.type; + +/** + * DB Array types. + */ +public interface ScalarTypeArray { + + /** + * Return the underlying DB column type. + */ + String getDbColumnDefn(); + +} diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java index b697a3dd1..e7429a77e 100644 --- a/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArrayList.java @@ -19,7 +19,7 @@ import java.util.UUID; /** * Type mapped for DB ARRAY type (Postgres only effectively). */ -public class ScalarTypeArrayList extends ScalarTypeJsonCollection { +public class ScalarTypeArrayList extends ScalarTypeJsonCollection implements ScalarTypeArray { private static ScalarTypeArrayList UUID = new ScalarTypeArrayList("uuid", DocPropertyType.UUID, ArrayElementConverter.UUID); private static ScalarTypeArrayList LONG = new ScalarTypeArrayList("bigint", DocPropertyType.LONG, ArrayElementConverter.LONG); diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java new file mode 100644 index 000000000..e2e47aedf --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySet.java @@ -0,0 +1,142 @@ +package io.ebeaninternal.server.type; + + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import io.ebean.text.json.EJson; +import io.ebeanservice.docstore.api.mapping.DocPropertyType; + +import javax.persistence.PersistenceException; +import java.io.IOException; +import java.lang.reflect.Type; +import java.sql.Array; +import java.sql.SQLException; +import java.sql.Types; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Type mapped for DB ARRAY type (Postgres only effectively). + */ +public class ScalarTypeArraySet extends ScalarTypeJsonCollection implements ScalarTypeArray { + + private static ScalarTypeArraySet UUID = new ScalarTypeArraySet("uuid", DocPropertyType.UUID, ArrayElementConverter.UUID); + private static ScalarTypeArraySet LONG = new ScalarTypeArraySet("bigint", DocPropertyType.LONG, ArrayElementConverter.LONG); + private static ScalarTypeArraySet INTEGER = new ScalarTypeArraySet("integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER); + private static ScalarTypeArraySet DOUBLE = new ScalarTypeArraySet("float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE); + private static ScalarTypeArraySet STRING = new ScalarTypeArraySet("varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING); + + static PlatformArrayTypeFactory factory() { + return new Factory(); + } + + static class Factory implements PlatformArrayTypeFactory { + + /** + * Return the ScalarType to use based on the List's generic parameter type. + */ + @Override + public ScalarTypeArraySet typeFor(Type valueType) { + if (valueType.equals(UUID.class)) { + return UUID; + } + if (valueType.equals(Long.class)) { + return LONG; + } + if (valueType.equals(Integer.class)) { + return INTEGER; + } + if (valueType.equals(Double.class)) { + return DOUBLE; + } + if (valueType.equals(String.class)) { + return STRING; + } + throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping on set"); + } + } + + private final String arrayType; + + private final ArrayElementConverter converter; + + public ScalarTypeArraySet(String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) { + super(Set.class, Types.ARRAY, docPropertyType); + this.arrayType = arrayType; + this.converter = converter; + } + + @Override + public DocPropertyType getDocType() { + return docPropertyType; + } + + /** + * Return the DB column definition for DDL generation. + */ + public String getDbColumnDefn() { + return arrayType + "[]"; + } + + @SuppressWarnings("unchecked") + private Set fromArray(Object[] array1) { + Set set = new LinkedHashSet(); + for (Object element : array1) { + set.add(converter.toElement(element)); + } + return new ModifyAwareSet(set); + } + + protected Object[] toArray(Set value) { + return value.toArray(); + } + + @Override + public Set read(DataReader reader) throws SQLException { + Array array = reader.getArray(); + if (array == null) { + return null; + } else { + return fromArray((Object[]) array.getArray()); + } + } + + @Override + public void bind(DataBind bind, Set value) throws SQLException { + if (value == null) { + bind.setNull(Types.ARRAY); + } else { + bind.setArray(arrayType, toArray(value)); + } + } + + @Override + public String formatValue(Set value) { + try { + return EJson.write(value); + } catch (IOException e) { + throw new PersistenceException("Failed to format List into JSON content", e); + } + } + + @Override + public Set parse(String value) { + try { + return EJson.parseSet(value, false); + } catch (IOException e) { + throw new PersistenceException("Failed to parse JSON content as List: [" + value + "]", e); + } + } + + @Override + public Set jsonRead(JsonParser parser) throws IOException { + return EJson.parseSet(parser, parser.getCurrentToken()); + } + + @Override + public void jsonWrite(JsonGenerator writer, Set value) throws IOException { + EJson.write(value, writer); + } + +} diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2.java new file mode 100644 index 000000000..279d57864 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeArraySetH2.java @@ -0,0 +1,68 @@ +package io.ebeaninternal.server.type; + +import io.ebeanservice.docstore.api.mapping.DocPropertyType; + +import java.lang.reflect.Type; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Set; + +/** + * H2 database support for DB ARRAY. + */ +class ScalarTypeArraySetH2 extends ScalarTypeArraySet { + + private static ScalarTypeArraySetH2 UUID = new ScalarTypeArraySetH2("uuid", DocPropertyType.UUID, ArrayElementConverter.UUID); + private static ScalarTypeArraySetH2 LONG = new ScalarTypeArraySetH2("bigint", DocPropertyType.LONG, ArrayElementConverter.LONG); + private static ScalarTypeArraySetH2 INTEGER = new ScalarTypeArraySetH2("integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER); + private static ScalarTypeArraySetH2 DOUBLE = new ScalarTypeArraySetH2("double", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE); + private static ScalarTypeArraySetH2 STRING = new ScalarTypeArraySetH2("varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING); + + static PlatformArrayTypeFactory factory() { + return new ScalarTypeArraySetH2.Factory(); + } + + static class Factory implements PlatformArrayTypeFactory { + + /** + * Return the ScalarType to use based on the List's generic parameter type. + */ + @Override + public ScalarTypeArraySetH2 typeFor(Type valueType) { + if (valueType.equals(java.util.UUID.class)) { + return UUID; + } + if (valueType.equals(Integer.class)) { + return INTEGER; + } + if (valueType.equals(Long.class)) { + return LONG; + } + if (valueType.equals(Double.class)) { + return DOUBLE; + } + if (valueType.equals(String.class)) { + return STRING; + } + throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping"); + } + } + + private ScalarTypeArraySetH2(String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) { + super(arrayType, docPropertyType, converter); + } + + @Override + public String getDbColumnDefn() { + return "array"; + } + + @Override + public void bind(DataBind bind, Set value) throws SQLException { + if (value == null) { + bind.setNull(Types.ARRAY); + } else { + bind.setObject(toArray(value)); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java b/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java index 9eaf2af19..66b688a41 100644 --- a/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java +++ b/src/main/java/io/ebeaninternal/server/type/ScalarTypeJsonSet.java @@ -37,7 +37,7 @@ public class ScalarTypeJsonSet { /** * List mapped to DB VARCHAR. */ - private static class Varchar extends ScalarTypeJsonSet.Base { + public static class Varchar extends ScalarTypeJsonSet.Base { public Varchar(DocPropertyType docPropertyType) { super(Types.VARCHAR, docPropertyType); } diff --git a/src/test/java/org/tests/model/array/EArraySetBean.java b/src/test/java/org/tests/model/array/EArraySetBean.java new file mode 100644 index 000000000..585256a79 --- /dev/null +++ b/src/test/java/org/tests/model/array/EArraySetBean.java @@ -0,0 +1,91 @@ +package org.tests.model.array; + + +import io.ebean.annotation.DbArray; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; + +@Entity +public class EArraySetBean { + + @Id + Long id; + + String name; + + @DbArray(length = 300) + Set phoneNumbers = new LinkedHashSet<>(); + + @DbArray + Set uids = new LinkedHashSet<>(); + + @DbArray + Set otherIds = new LinkedHashSet<>(); + + @DbArray + Set doubs; + + @Version + Long version; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(Set phoneNumbers) { + this.phoneNumbers = phoneNumbers; + } + + public Set getUids() { + return uids; + } + + public void setUids(Set uids) { + this.uids = uids; + } + + public Set getOtherIds() { + return otherIds; + } + + public void setOtherIds(Set otherIds) { + this.otherIds = otherIds; + } + + public Set getDoubs() { + return doubs; + } + + public void setDoubs(Set doubs) { + this.doubs = doubs; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/org/tests/model/array/TestDbArray_asSet.java b/src/test/java/org/tests/model/array/TestDbArray_asSet.java new file mode 100644 index 000000000..572673496 --- /dev/null +++ b/src/test/java/org/tests/model/array/TestDbArray_asSet.java @@ -0,0 +1,152 @@ +package org.tests.model.array; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Query; +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +public class TestDbArray_asSet extends BaseTestCase { + + private EArraySetBean bean = new EArraySetBean(); + + private EArraySetBean found; + + @Test + public void insert() { + + bean.setName("some stuff"); + + Set phNumbers = bean.getPhoneNumbers(); + phNumbers.add("4321"); + phNumbers.add("9823"); + + + Set doubles = new LinkedHashSet<>(); + doubles.add(1.3); + doubles.add(2.4); + + UUID first = UUID.randomUUID(); + bean.getUids().add(first); + bean.getUids().add(UUID.randomUUID()); + bean.getOtherIds().add(95L); + bean.getOtherIds().add(96L); + bean.getOtherIds().add(97L); + bean.setDoubs(doubles); + + Ebean.save(bean); + + found = Ebean.find(EArraySetBean.class, bean.getId()); + + assertThat(found.getPhoneNumbers()).containsExactly("4321", "9823"); + + if (isPostgres()) { + Query query = Ebean.find(EArraySetBean.class) + .where() + .arrayContains("otherIds", 96L, 97L) + .arrayContains("uids", first) + .arrayContains("phoneNumbers", "9823") + .arrayIsNotEmpty("phoneNumbers") + .query(); + + List list = query.findList(); + + assertThat(query.getGeneratedSql()).contains(" t0.other_ids @> array[?,?]::bigint[] "); + assertThat(query.getGeneratedSql()).contains(" t0.uids @> array[?] "); + assertThat(query.getGeneratedSql()).contains(" t0.phone_numbers @> array[?] "); + assertThat(query.getGeneratedSql()).contains(" coalesce(cardinality(t0.phone_numbers),0) <> 0"); + assertThat(list).hasSize(1); + + query = Ebean.find(EArraySetBean.class) + .where() + .arrayIsEmpty("otherIds") + .arrayNotContains("uids", first) + .query(); + query.findList(); + + assertThat(query.getGeneratedSql()).contains(" coalesce(cardinality(t0.other_ids),0) = 0"); + assertThat(query.getGeneratedSql()).contains(" not (t0.uids @> array[?])"); + } + + json_parse_format(); + update_when_notDirty(); + update_when_dirty(); + } + + //@Test//(dependsOnMethods = "insert") + public void json_parse_format() { + + String asJson = Ebean.json().toJson(found); + assertThat(asJson).contains("\"phoneNumbers\":[\"4321\",\"9823\"]"); + assertThat(asJson).contains("\"id\":"); + + EArraySetBean fromJson = Ebean.json().toBean(EArraySetBean.class, asJson); + assertEquals(found.getId(), fromJson.getId()); + assertEquals(found.getId(), fromJson.getId()); + assertEquals(found.getName(), fromJson.getName()); + assertThat(fromJson.getPhoneNumbers()).containsExactly("4321", "9823"); + } + + //@Test//(dependsOnMethods = "insert") + public void update_when_notDirty() { + + found.setName("jack"); + LoggedSqlCollector.start(); + Ebean.save(found); + List sql = LoggedSqlCollector.stop(); + + // we don't update the phone numbers (as they are not dirty) + assertThat(sql.get(0)).contains("update earray_set_bean set name=?, version=? where"); + } + + //@Test//(dependsOnMethods = "update_when_notDirty") + public void update_when_dirty() { + + found.getPhoneNumbers().add("9987"); + found.getUids().add(UUID.randomUUID()); + + LoggedSqlCollector.start(); + Ebean.save(found); + List sql = LoggedSqlCollector.stop(); + + assertThat(sql.get(0)).contains("update earray_set_bean set phone_numbers=?, uids=?, version=? where"); + } + + @Test + public void insertNulls() { + + EArraySetBean bean = new EArraySetBean(); + bean.setName("some nulls"); + bean.setPhoneNumbers(null); + bean.setOtherIds(null); + bean.setUids(null); + + Ebean.save(bean); + Ebean.delete(bean); + } + + @Test + public void insertAll_when_hasNulls() { + + EArraySetBean bean = new EArraySetBean(); + bean.setName("some nulls"); + bean.setPhoneNumbers(null); + bean.setOtherIds(null); + bean.setUids(null); + + Set all = new HashSet<>(); + all.add(bean); + + Ebean.saveAll(all); + Ebean.deleteAll(all); + } +}