From 67f427b83e880d56abc552f86759dd21342e460f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Thu, 28 May 2015 22:54:32 +1200 Subject: [PATCH] #299 - ENH: Add support for mapping Jackson JsonNode as JSON content to DB including Postgres JSON and JSON types --- pom.xml | 5 +- .../server/type/DefaultTypeManager.java | 71 ++++++ .../server/type/ScalarTypeJsonNode.java | 227 ++++++++++++++++++ .../type/ScalarTypeJsonNodePostgres.java | 59 +++++ .../server/type/ScalarTypePostgresHstore.java | 2 - .../avaje/tests/json/TestJsonNodeBasic.java | 102 ++++++++ .../avaje/tests/json/TestJsonNodeBlob.java | 69 ++++++ .../avaje/tests/json/TestJsonNodeJsonb.java | 69 ++++++ .../avaje/tests/json/TestJsonNodeVarchar.java | 69 ++++++ .../tests/model/json/EBasicJsonNode.java | 56 +++++ .../tests/model/json/EBasicJsonNodeBlob.java | 56 +++++ .../tests/model/json/EBasicJsonNodeJsonB.java | 58 +++++ .../model/json/EBasicJsonNodeVarchar.java | 56 +++++ 13 files changed, 895 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java create mode 100644 src/test/java/com/avaje/tests/json/TestJsonNodeBasic.java create mode 100644 src/test/java/com/avaje/tests/json/TestJsonNodeBlob.java create mode 100644 src/test/java/com/avaje/tests/json/TestJsonNodeJsonb.java create mode 100644 src/test/java/com/avaje/tests/json/TestJsonNodeVarchar.java create mode 100644 src/test/java/com/avaje/tests/model/json/EBasicJsonNode.java create mode 100644 src/test/java/com/avaje/tests/model/json/EBasicJsonNodeBlob.java create mode 100644 src/test/java/com/avaje/tests/model/json/EBasicJsonNodeJsonB.java create mode 100644 src/test/java/com/avaje/tests/model/json/EBasicJsonNodeVarchar.java diff --git a/pom.xml b/pom.xml index d80270f84..d1e34ec9f 100644 --- a/pom.xml +++ b/pom.xml @@ -55,18 +55,19 @@ 1.7.7 + com.fasterxml.jackson.core jackson-databind 2.5.3 + provided - + com.fasterxml.jackson.core jackson-core 2.5.3 - provided 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 8b27cab90..24825de3c 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/DefaultTypeManager.java @@ -9,6 +9,7 @@ import com.avaje.ebeaninternal.api.ClassUtil; import com.avaje.ebeaninternal.server.core.BootupClasses; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.type.reflect.*; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.joda.time.*; import org.joda.time.LocalDate; @@ -122,6 +123,29 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { private final boolean objectMapperPresent; + // OPTIONAL ScalarTypes registered if Jackson/JsonNode is in the classpath + + /** + * Jackson's JsonNode storage to Clob. + */ + private ScalarType jsonNodeClob; + /** + * Jackson's JsonNode storage to Blob. + */ + private ScalarType jsonNodeBlob; + /** + * Jackson's JsonNode storage to Varchar. + */ + private ScalarType jsonNodeVarchar; + /** + * Jackson's JsonNode storage to Postgres JSON or Clob. + */ + private ScalarType jsonNodeJson; + /** + * Jackson's JsonNode storage to Postgres JSONB or Clob. + */ + private ScalarType jsonNodeJsonb; + /** * Create the DefaultTypeManager. */ @@ -142,6 +166,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { initialiseStandard(jsonDateTime, config); initialiseJavaTimeTypes(jsonDateTime, config); initialiseJodaTypes(jsonDateTime); + initialiseJacksonTypes(config); if (isPostgres(config.getDatabasePlatform())) { // Postgres has special DB types for JSON/JSONB @@ -291,6 +316,7 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { public ScalarType getJsonScalarType(Class type, int dbType) { if (type.equals(Map.class)) { + // @DbJson Map property switch (dbType) { case Types.VARCHAR : return jsonMapVarchar; case Types.BLOB: return jsonMapBlob; @@ -302,6 +328,19 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { } } + if (type.equals(JsonNode.class)) { + // @DbJson JsonNode property (Jackson) + switch (dbType) { + case Types.VARCHAR : return jsonNodeVarchar; + case Types.BLOB: return jsonNodeBlob; + case Types.CLOB : return jsonNodeClob; + case DbType.JSONB: return jsonNodeJsonb; + case DbType.JSON: return jsonNodeJson; + default: + return jsonNodeJson; + } + } + throw new IllegalArgumentException("Type [" + type + "] unsupported for @DbJson mapping"); } @@ -645,6 +684,38 @@ public final class DefaultTypeManager implements TypeManager, KnownImmutable { return propParamTypes[1]; } + + /** + * Add support for Jackson's JsonNode mapping to Clob, Blob, Varchar, JSON and JSONB. + */ + protected void initialiseJacksonTypes(ServerConfig config) { + + if (ClassUtil.isPresent("com.fasterxml.jackson.databind.ObjectMapper", this.getClass())) { + + logger.trace("Registering JsonNode type support"); + + ObjectMapper objectMapper = (ObjectMapper)config.getObjectMapper(); + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + config.setObjectMapper(objectMapper); + } + + jsonNodeClob = new ScalarTypeJsonNode.Clob(objectMapper); + jsonNodeBlob = new ScalarTypeJsonNode.Blob(objectMapper); + jsonNodeVarchar = new ScalarTypeJsonNode.Varchar(objectMapper); + jsonNodeJson = jsonNodeClob; // Default for non-Postgres databases + jsonNodeJsonb = jsonNodeClob; // Default for non-Postgres databases + + if (isPostgres(config.getDatabasePlatform())) { + jsonNodeJson = new ScalarTypeJsonNodePostgres.JSON(objectMapper); + jsonNodeJsonb = new ScalarTypeJsonNodePostgres.JSONB(objectMapper); + } + + // add as default mapping for JsonNode (when not annotated with @DbJson etc) + typeMap.put(JsonNode.class, jsonNodeJson); + } + } + protected void initialiseJavaTimeTypes(JsonConfig.DateTime mode, ServerConfig config) { if (ClassUtil.isPresent("java.time.LocalDate", this.getClass())) { logger.debug("Registering java.time data types"); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java new file mode 100644 index 000000000..9dc44579e --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNode.java @@ -0,0 +1,227 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.text.TextException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Type which maps Jackson's JsonNode to various DB types (Clob, Varchar, Blob) in JSON format. + */ +public abstract class ScalarTypeJsonNode extends ScalarTypeBase { + + /** + * Clob storage based implementation. + */ + public static class Clob extends ScalarTypeJsonNode { + + public Clob(ObjectMapper objectMapper) { + super(objectMapper, Types.CLOB); + } + + @Override + public JsonNode read(DataReader dataReader) throws SQLException { + + Reader reader = dataReader.getClobReader(); + if (reader == null) { + return null; + } + try { + JsonNode tree = parse(reader); + reader.close(); + return tree; + } catch (IOException e) { + throw new SQLException("Error reading Clob stream from DB", e); + } + } + } + + /** + * Varchar storage based implementation. + */ + public static class Varchar extends ScalarTypeJsonNode { + + public Varchar(ObjectMapper objectMapper) { + super(objectMapper, Types.VARCHAR); + } + } + + /** + * Blob storage based implementation. + */ + public static class Blob extends ScalarTypeJsonNode { + + public Blob(ObjectMapper objectMapper) { + super(objectMapper, Types.BLOB); + } + + @Override + public JsonNode read(DataReader dataReader) throws SQLException { + + InputStream is = dataReader.getBlobInputStream(); + if (is == null) { + return null; + } + try { + InputStreamReader reader = new InputStreamReader(is); + JsonNode tree = parse(reader); + reader.close(); + return tree; + } catch (IOException e) { + throw new SQLException("Error reading Blob stream from DB", e); + } + } + + @Override + public void bind(DataBind dataBind, JsonNode value) throws SQLException { + + if (value == null) { + dataBind.setNull(Types.BLOB); + } else { + String rawJson = formatValue(value); + InputStream stream = new ByteArrayInputStream(rawJson.getBytes(StandardCharsets.UTF_8)); + dataBind.setBlob(stream); + } + } + } + + /** + * Jackson's ObjectMapper used to read / write JsonNode + */ + final ObjectMapper objectMapper; + + public ScalarTypeJsonNode(ObjectMapper objectMapper, int jdbcType) { + super(JsonNode.class, false, jdbcType); + this.objectMapper = objectMapper; + } + + /** + * Map is a mutable type. Use the isDirty() method to check for dirty state. + */ + @Override + public boolean isMutable() { + return true; + } + + /** + * Return true if the value should be considered dirty (and included in an update). + */ + @Override + public boolean isDirty(Object value) { + return true; + } + + @Override + public JsonNode read(DataReader dataReader) throws SQLException { + + String rawJson = dataReader.getString(); + if (rawJson == null) { + return null; + } + return parse(rawJson); + } + + @Override + public void bind(DataBind dataBind, JsonNode value) throws SQLException { + + if (value == null) { + dataBind.setNull(Types.VARCHAR); + } else { + String rawJson = formatValue(value); + dataBind.setString(rawJson); + } + } + + @Override + public Object toJdbcType(Object value) { + return value; + } + + @Override + public JsonNode toBeanType(Object value) { + return (JsonNode) value; + } + + @Override + public String formatValue(JsonNode jsonNode) { + try { + return objectMapper.writeValueAsString(jsonNode); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public JsonNode parse(String value) { + try { + return objectMapper.readValue(value, JsonNode.class); + } catch (IOException e) { + throw new TextException(e); + } + } + + public JsonNode parse(Reader reader) { + try { + return objectMapper.readValue(reader, JsonNode.class); + } catch (IOException e) { + throw new TextException(e); + } + } + + @Override + public JsonNode convertFromMillis(long dateTime) { + throw new RuntimeException("Should never be called"); + } + + @Override + public boolean isDateTimeCapable() { + return false; + } + + @Override + public JsonNode readData(DataInput dataInput) throws IOException { + if (!dataInput.readBoolean()) { + return null; + } else { + String json = dataInput.readUTF(); + return parse(json); + } + } + + @Override + public void writeData(DataOutput dataOutput, JsonNode value) throws IOException { + if (value == null) { + dataOutput.writeBoolean(false); + } else { + dataOutput.writeBoolean(true); + String json = format(value); + dataOutput.writeUTF(json); + } + } + + @Override + public void jsonWrite(JsonGenerator ctx, String name, JsonNode value) throws IOException { + // write the field name followed by the JsonNode object + if (value == null) { + ctx.writeNullField(name); + } else { + ctx.writeFieldName(name); + objectMapper.writeTree(ctx, value); + } + } + + @Override + public JsonNode jsonRead(JsonParser ctx, JsonToken event) throws IOException { + // at this point the BeanProperty has read the START_OBJECT token + // to check for a null value. + return objectMapper.readValue(ctx, JsonNode.class); + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java new file mode 100644 index 000000000..ae12e8fe4 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypeJsonNodePostgres.java @@ -0,0 +1,59 @@ +package com.avaje.ebeaninternal.server.type; + +import com.avaje.ebean.config.dbplatform.DbType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.postgresql.util.PGobject; + +import java.sql.SQLException; + +/** + * Support for mapping JsonNode to Postgres DB types JSON and JSONB. + */ +public abstract class ScalarTypeJsonNodePostgres extends ScalarTypeJsonNode { + + private static final String POSTGRES_TYPE_JSON = "json"; + + private static final String POSTGRES_TYPE_JSONB = "jsonb"; + + final ObjectMapper objectMapper; + + final String postgresType; + + ScalarTypeJsonNodePostgres(ObjectMapper objectMapper, int jdbcType, String postgresType) { + super(objectMapper, jdbcType); + this.objectMapper = objectMapper; + this.postgresType = postgresType; + } + + @Override + public void bind(DataBind dataBind, JsonNode value) throws SQLException { + + String rawJson = (value == null) ? null : formatValue(value); + + PGobject pgo = new PGobject(); + pgo.setType(postgresType); + pgo.setValue(rawJson); + dataBind.setObject(pgo); + } + + /** + * ScalarType mapping JsonNode to Postgres JSON database type. + */ + public static class JSON extends ScalarTypeJsonNodePostgres { + + public JSON(ObjectMapper objectMapper) { + super(objectMapper, DbType.JSON, POSTGRES_TYPE_JSON); + } + } + + /** + * ScalarType mapping JsonNode to Postgres JSONB database type. + */ + public static class JSONB extends ScalarTypeJsonNodePostgres { + + public JSONB(ObjectMapper objectMapper) { + super(objectMapper, DbType.JSONB, POSTGRES_TYPE_JSONB); + } + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java index d91def549..2c04d0175 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/ScalarTypePostgresHstore.java @@ -19,8 +19,6 @@ import java.util.Map; @SuppressWarnings("rawtypes") public class ScalarTypePostgresHstore extends ScalarTypeBase { - public static final String KEY = "hstore"; - public ScalarTypePostgresHstore() { super(Map.class, false, DbType.HSTORE); } diff --git a/src/test/java/com/avaje/tests/json/TestJsonNodeBasic.java b/src/test/java/com/avaje/tests/json/TestJsonNodeBasic.java new file mode 100644 index 000000000..e6ad3d8dd --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonNodeBasic.java @@ -0,0 +1,102 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.json.EBasicJsonNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.Assert.*; + +public class TestJsonNodeBasic extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + String s1 = "{\"docId\":99,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNode bean = new EBasicJsonNode(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonNode bean1 = Ebean.find(EBasicJsonNode.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + + assertEquals(bean.getContent().path("contentType").asText(), bean1.getContent().path("contentType").asText()); + assertEquals(18L, bean1.getContent().get("docId").asLong()); + + bean1.setName("just change name"); + Ebean.save(bean1); + + JsonNode content1 = objectMapper.readTree(s1); + + bean1.setName("two"); + bean1.setContent(content1); + Ebean.save(bean1); + + EBasicJsonNode bean2 = Ebean.find(EBasicJsonNode.class, bean.getId()); + + // name changed and docId changed + assertEquals("two", bean2.getName()); + assertEquals(99L, bean2.getContent().path("docId").asLong()); + + // content persisted even though it has not changed (as it's loaded) + // we can't do dirty detection on JsonNode (like we do on Map) + bean1.setName("three"); + Ebean.save(bean1); + + EBasicJsonNode bean3 = Ebean.find(EBasicJsonNode.class, bean.getId()); + + assertEquals("three", bean3.getName()); + assertEquals(99L, bean3.getContent().path("docId").asLong()); + + // write whole bean with content as JSON + String fullBeanJson = Ebean.json().toJson(bean3); + + // parse JSON into whole bean with content + EBasicJsonNode beanJsonRestored = Ebean.json().toBean(EBasicJsonNode.class, fullBeanJson); + + assertEquals(99L,beanJsonRestored.getContent().path("docId").asLong()); + } + + @Test + public void testLazyLoading() throws IOException { + + String s0 = "{\"docId\":19,\"contentId\":\"asd\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNode bean = new EBasicJsonNode(); + bean.setName("lazyLoadTest"); + bean.setContent(content); + Ebean.save(bean); + + EBasicJsonNode bean1 = Ebean.find(EBasicJsonNode.class) + .select("name") + .setId(bean.getId()) + .findUnique(); + + Set loadedProps = Ebean.getBeanState(bean1).getLoadedProps(); + assertTrue(loadedProps.contains("name")); + assertFalse(loadedProps.contains("content")); + + JsonNode lazyLoadedContent = bean1.getContent(); + assertNotNull(lazyLoadedContent); + + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonNodeBlob.java b/src/test/java/com/avaje/tests/json/TestJsonNodeBlob.java new file mode 100644 index 000000000..1bbd17c21 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonNodeBlob.java @@ -0,0 +1,69 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.json.EBasicJsonNodeBlob; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.Assert.*; + +public class TestJsonNodeBlob extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNodeBlob bean = new EBasicJsonNodeBlob(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonNodeBlob bean1 = Ebean.find(EBasicJsonNodeBlob.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + + assertEquals(bean.getContent().path("contentType").asText(), bean1.getContent().path("contentType").asText()); + assertEquals(18L, bean1.getContent().get("docId").asLong()); + + } + + @Test + public void testLazyLoading() throws IOException { + + String s0 = "{\"docId\":19,\"contentId\":\"asd\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNodeBlob bean = new EBasicJsonNodeBlob(); + bean.setName("lazyLoadTest"); + bean.setContent(content); + Ebean.save(bean); + + EBasicJsonNodeBlob bean1 = Ebean.find(EBasicJsonNodeBlob.class) + .select("name") + .setId(bean.getId()) + .findUnique(); + + Set loadedProps = Ebean.getBeanState(bean1).getLoadedProps(); + assertTrue(loadedProps.contains("name")); + assertFalse(loadedProps.contains("content")); + + JsonNode lazyLoadedContent = bean1.getContent(); + assertNotNull(lazyLoadedContent); + + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonNodeJsonb.java b/src/test/java/com/avaje/tests/json/TestJsonNodeJsonb.java new file mode 100644 index 000000000..ab455814b --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonNodeJsonb.java @@ -0,0 +1,69 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.json.EBasicJsonNodeJsonB; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.Assert.*; + +public class TestJsonNodeJsonb extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNodeJsonB bean = new EBasicJsonNodeJsonB(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonNodeJsonB bean1 = Ebean.find(EBasicJsonNodeJsonB.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + + assertEquals(bean.getContent().path("contentType").asText(), bean1.getContent().path("contentType").asText()); + assertEquals(18L, bean1.getContent().get("docId").asLong()); + + } + + @Test + public void testLazyLoading() throws IOException { + + String s0 = "{\"docId\":19,\"contentId\":\"asd\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNodeJsonB bean = new EBasicJsonNodeJsonB(); + bean.setName("lazyLoadTest"); + bean.setContent(content); + Ebean.save(bean); + + EBasicJsonNodeJsonB bean1 = Ebean.find(EBasicJsonNodeJsonB.class) + .select("name") + .setId(bean.getId()) + .findUnique(); + + Set loadedProps = Ebean.getBeanState(bean1).getLoadedProps(); + assertTrue(loadedProps.contains("name")); + assertFalse(loadedProps.contains("content")); + + JsonNode lazyLoadedContent = bean1.getContent(); + assertNotNull(lazyLoadedContent); + + } +} diff --git a/src/test/java/com/avaje/tests/json/TestJsonNodeVarchar.java b/src/test/java/com/avaje/tests/json/TestJsonNodeVarchar.java new file mode 100644 index 000000000..b4b341154 --- /dev/null +++ b/src/test/java/com/avaje/tests/json/TestJsonNodeVarchar.java @@ -0,0 +1,69 @@ +package com.avaje.tests.json; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.json.EBasicJsonNodeVarchar; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.util.Set; + +import static org.junit.Assert.*; + +public class TestJsonNodeVarchar extends BaseTestCase { + + @Test + public void testInsertUpdateDelete() throws IOException { + + String s0 = "{\"docId\":18,\"contentId\":\"asd\",\"active\":true,\"contentType\":\"pg-hello\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNodeVarchar bean = new EBasicJsonNodeVarchar(); + bean.setName("one"); + bean.setContent(content); + + Ebean.save(bean); + + EBasicJsonNodeVarchar bean1 = Ebean.find(EBasicJsonNodeVarchar.class, bean.getId()); + + assertEquals(bean.getId(), bean1.getId()); + assertEquals(bean.getName(), bean1.getName()); + + assertEquals(bean.getContent().path("contentType").asText(), bean1.getContent().path("contentType").asText()); + assertEquals(18L, bean1.getContent().get("docId").asLong()); + + } + + @Test + public void testLazyLoading() throws IOException { + + String s0 = "{\"docId\":19,\"contentId\":\"asd\",\"content\":{\"name\":\"rob\",\"age\":45}}"; + + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode content = objectMapper.readTree(s0); + + EBasicJsonNodeVarchar bean = new EBasicJsonNodeVarchar(); + bean.setName("lazyLoadTest"); + bean.setContent(content); + Ebean.save(bean); + + EBasicJsonNodeVarchar bean1 = Ebean.find(EBasicJsonNodeVarchar.class) + .select("name") + .setId(bean.getId()) + .findUnique(); + + Set loadedProps = Ebean.getBeanState(bean1).getLoadedProps(); + assertTrue(loadedProps.contains("name")); + assertFalse(loadedProps.contains("content")); + + JsonNode lazyLoadedContent = bean1.getContent(); + assertNotNull(lazyLoadedContent); + + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonNode.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonNode.java new file mode 100644 index 000000000..5af19f564 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonNode.java @@ -0,0 +1,56 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; +import com.fasterxml.jackson.databind.JsonNode; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; +import java.util.Map; + +@Entity +public class EBasicJsonNode { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson + JsonNode content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public JsonNode getContent() { + return content; + } + + public void setContent(JsonNode content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeBlob.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeBlob.java new file mode 100644 index 000000000..0b8e55219 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeBlob.java @@ -0,0 +1,56 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonType; +import com.fasterxml.jackson.databind.JsonNode; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; + +@Entity +public class EBasicJsonNodeBlob { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.BLOB) + JsonNode content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public JsonNode getContent() { + return content; + } + + public void setContent(JsonNode content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeJsonB.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeJsonB.java new file mode 100644 index 000000000..924175149 --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeJsonB.java @@ -0,0 +1,58 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonB; +import com.avaje.ebean.annotation.DbJsonType; +import com.fasterxml.jackson.databind.JsonNode; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; + +@Entity +public class EBasicJsonNodeJsonB { + + @Id + Long id; + + @Version + Long version; + + String name; + + //@DbJson(storage = DbJsonType.JSONB) + @DbJsonB + JsonNode content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public JsonNode getContent() { + return content; + } + + public void setContent(JsonNode content) { + this.content = content; + } +} diff --git a/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeVarchar.java b/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeVarchar.java new file mode 100644 index 000000000..9888356ab --- /dev/null +++ b/src/test/java/com/avaje/tests/model/json/EBasicJsonNodeVarchar.java @@ -0,0 +1,56 @@ +package com.avaje.tests.model.json; + +import com.avaje.ebean.annotation.DbJson; +import com.avaje.ebean.annotation.DbJsonType; +import com.fasterxml.jackson.databind.JsonNode; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Version; + +@Entity +public class EBasicJsonNodeVarchar { + + @Id + Long id; + + @Version + Long version; + + String name; + + @DbJson(storage = DbJsonType.VARCHAR, length = 1000) + JsonNode content; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public JsonNode getContent() { + return content; + } + + public void setContent(JsonNode content) { + this.content = content; + } +}