From 17042a9fafc76b68aad66d777d2cba48cd81ee96 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Mon, 9 May 2016 23:24:13 +1200 Subject: [PATCH] #696 - ENH: Add fallback for @DbHstore ... fallback to JSON storage in VARCHAR --- .../com/avaje/ebean/annotation/DbHstore.java | 4 + .../server/deploy/parse/AnnotationFields.java | 5 +- .../server/deploy/parse/DeployUtil.java | 20 +++-- .../server/type/DefaultTypeManager.java | 9 +- .../server/type/TypeManager.java | 5 ++ .../avaje/tests/model/json/EBasicHstore.java | 62 ++++++++++++++ .../com/avaje/tests/types/TestHstore.java | 83 +++++++++++++++++++ 7 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/avaje/tests/model/json/EBasicHstore.java create mode 100644 src/test/java/com/avaje/tests/types/TestHstore.java diff --git a/src/main/java/com/avaje/ebean/annotation/DbHstore.java b/src/main/java/com/avaje/ebean/annotation/DbHstore.java index 38ce6bafd..9ffd29760 100644 --- a/src/main/java/com/avaje/ebean/annotation/DbHstore.java +++ b/src/main/java/com/avaje/ebean/annotation/DbHstore.java @@ -23,4 +23,8 @@ import java.lang.annotation.Target; @Target(ElementType.FIELD) public @interface DbHstore { + /** + * For VARCHAR storage specify the column length (defaults to 1000). + */ + int length() default 0; } \ No newline at end of file diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java index ed04e512a..4325da31a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationFields.java @@ -175,8 +175,9 @@ public class AnnotationFields extends AnnotationParser { if (comment != null) { prop.setDbComment(comment.value()); } - if (get(prop, DbHstore.class) != null) { - util.setDbHstore(prop); + DbHstore dbHstore = get(prop, DbHstore.class); + if (dbHstore != null) { + util.setDbHstore(prop, dbHstore); } DbJson dbJson = get(prop, DbJson.class); if (dbJson != null) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java index 5887044bb..4515a44d2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -6,6 +6,7 @@ import javax.persistence.Enumerated; import javax.persistence.PersistenceException; import com.avaje.ebean.annotation.DbArray; +import com.avaje.ebean.annotation.DbHstore; import com.avaje.ebean.annotation.DbJson; import com.avaje.ebean.annotation.DbJsonB; import com.avaje.ebean.annotation.DbJsonType; @@ -203,15 +204,20 @@ public class DeployUtil { } /** - * Map to Postgres HSTORE type. + * Map to Postgres HSTORE type (with fallback to JSON storage in VARCHAR). */ - public void setDbHstore(DeployBeanProperty prop) { - ScalarType scalarType = typeManager.getScalarType(DbType.HSTORE); - if (scalarType == null) { - throw new RuntimeException("No ScalarType found for HSTORE on [" + prop.getFullBeanName() + "] ?"); - } - prop.setDbType(DbType.HSTORE); + public void setDbHstore(DeployBeanProperty prop, DbHstore dbHstore) { + + ScalarType scalarType = typeManager.getHstoreScalarType(); + int dbType = scalarType.getJdbcType(); + prop.setDbType(dbType); prop.setScalarType(scalarType); + if (dbType == Types.VARCHAR) { + // this is actually the fallback of JSON storage into VARCHAR + int dbLength = dbHstore.length(); + int columnLength = (dbLength > 0) ? dbLength : DEFAULT_JSON_VARCHAR_LENGTH; + prop.setDbLength(columnLength); + } } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java index 7c7e76203..2476370dc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -90,6 +90,8 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { private final DefaultTypeFactory extraTypeFactory; + private final ScalarType hstoreType = new ScalarTypePostgresHstore(); + private final ScalarTypeFile fileType = new ScalarTypeFile(); private final ScalarType charType = new ScalarTypeChar(); @@ -359,6 +361,11 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return reader; } + @Override + public ScalarType getHstoreScalarType() { + return (postgres) ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR); + } + @Override public ScalarType getArrayScalarType(Class type, DbArray dbArray, Type genericType) { @@ -930,7 +937,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { int platformClobType = databasePlatform.getClobDbType(); int platformBlobType = databasePlatform.getBlobDbType(); - nativeMap.put(DbType.HSTORE, new ScalarTypePostgresHstore()); + nativeMap.put(DbType.HSTORE, hstoreType); ScalarType utilDateType = extraTypeFactory.createUtilDate(mode); typeMap.put(java.util.Date.class, utilDateType); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java index ac967797c..7b880cd9a 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/TypeManager.java @@ -72,4 +72,9 @@ public interface TypeManager { * Return the ScalarType used to handle DB ARRAY. */ ScalarType getArrayScalarType(Class type, DbArray dbArray, Type genericType); + + /** + * Return the ScalarType used to handle HSTORE (Map). + */ + ScalarType getHstoreScalarType(); } diff --git a/src/test/java/com/avaje/tests/model/json/EBasicHstore.java b/src/test/java/com/avaje/tests/model/json/EBasicHstore.java new file mode 100644 index 000000000..0c6ccfea9 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicHstore.java @@ -0,0 +1,62 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbHstore; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.LinkedHashMap; +import java.util.Map; + +@Entity +public class EBasicHstore { + + @Id + Long id; + + String name; + + // fallback to varchar(800) for non-Postgres + @DbHstore(length = 800) + Map map; + + @Version + Long version; + + public EBasicHstore(String name) { + this.name = name; + this.map = new LinkedHashMap(); + } + + 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 Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } +} diff --git a/src/test/java/com/avaje/tests/types/TestHstore.java b/src/test/java/com/avaje/tests/types/TestHstore.java new file mode 100644 index 000000000..2bd8c9d44 --- /dev/null +++ b/src/test/java/com/avaje/tests/types/TestHstore.java @@ -0,0 +1,83 @@ +package com.avaje.tests.types; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.json.EBasicHstore; +import org.assertj.core.api.Assertions; +import org.avaje.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.StrictAssertions.assertThat; +import static org.junit.Assert.assertEquals; + +public class TestHstore extends BaseTestCase { + + private EBasicHstore bean; + + @Test + public void insert() { + + if (isPostgres()) { + // run this manually for Postgres with the HSTORE extension installed + // psql mydb -c 'create extension hstore;' + return; + } + + bean = new EBasicHstore("one"); + bean.getMap().put("home", "123"); + bean.getMap().put("work", "987"); + + Ebean.save(bean); + + json_parse_format(); + update_when_notDirty(); + update_when_dirty(); + insert_when_null(); + } + + void json_parse_format() { + + String asJson = Ebean.json().toJson(bean); + assertThat(asJson).contains("\"map\":{\"home\":\"123\",\"work\":\"987\"}"); + + EBasicHstore fromJson = Ebean.json().toBean(EBasicHstore.class, asJson); + assertEquals(bean.getId(), fromJson.getId()); + assertEquals(bean.getName(), fromJson.getName()); + Assertions.assertThat(fromJson.getMap().keySet()).containsExactly("home", "work"); + } + + void update_when_notDirty() { + + EBasicHstore found = Ebean.find(EBasicHstore.class, bean.getId()); + found.setName("modName"); + + LoggedSqlCollector.start(); + Ebean.save(found); + List sql = LoggedSqlCollector.stop(); + + // we don't update the map as it is not dirty + assertThat(sql.get(0)).contains("update ebasic_hstore set name=?, version=? where"); + } + + void update_when_dirty() { + + EBasicHstore found = Ebean.find(EBasicHstore.class, bean.getId()); + found.setName("modNamePlus"); + found.getMap().put("foo","9987"); + + LoggedSqlCollector.start(); + Ebean.save(found); + List sql = LoggedSqlCollector.stop(); + + assertThat(sql.get(0)).contains("update ebasic_hstore set name=?, map=?, version=? where id=? and version=?"); + } + + void insert_when_null() { + + EBasicHstore bean = new EBasicHstore("one"); + bean.setMap(null); + Ebean.save(bean); + } +}