From 60f69e5ca663cc60930538eb9c1a2d2e8738e1c1 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 30 Aug 2023 09:47:34 +0200 Subject: [PATCH 1/2] Provided showcase for planned removal of @DocStore and @Inheritance --- .../tests/docstore/CustomerReportTest.java | 94 ++++++++++++++++++- .../java/org/tests/model/docstore/Report.java | 6 ++ .../tests/model/docstore/ReportContainer.java | 31 ++++++ .../docstore/ReportJsonDeserializer.java | 33 +++++++ .../model/docstore/ReportJsonSerializer.java | 52 ++++++++++ 5 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 ebean-test/src/test/java/org/tests/model/docstore/ReportContainer.java create mode 100644 ebean-test/src/test/java/org/tests/model/docstore/ReportJsonDeserializer.java create mode 100644 ebean-test/src/test/java/org/tests/model/docstore/ReportJsonSerializer.java diff --git a/ebean-test/src/test/java/org/tests/docstore/CustomerReportTest.java b/ebean-test/src/test/java/org/tests/docstore/CustomerReportTest.java index 1dc65930a7..1b45aef06b 100644 --- a/ebean-test/src/test/java/org/tests/docstore/CustomerReportTest.java +++ b/ebean-test/src/test/java/org/tests/docstore/CustomerReportTest.java @@ -1,14 +1,18 @@ package org.tests.docstore; -import io.ebean.xtest.BaseTestCase; +import io.ebean.BeanState; +import io.ebean.DB; +import io.ebean.test.LoggedSql; import io.ebean.text.json.JsonReadOptions; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Product; import org.tests.model.basic.ResetBasicData; import org.tests.model.docstore.CustomerReport; import org.tests.model.docstore.ProductReport; +import org.tests.model.docstore.ReportContainer; import java.util.Arrays; @@ -21,7 +25,6 @@ public void testToJson() throws Exception { ResetBasicData.reset(); - String json = server().json().toJson(getCustomerReport()); assertThat(json).isEqualTo("{\"dtype\":\"CR\",\"friends\":[{\"id\":2},{\"id\":3}],\"customer\":{\"id\":1}}"); } @@ -52,9 +55,9 @@ public void testEmbeddedDocs() throws Exception { String json = server().json().toJson(report); assertThat(json).isEqualTo("{\"dtype\":\"CR\"," - + "\"embeddedReports\":[{\"dtype\":\"PR\",\"title\":\"This is a good product\",\"product\":{\"id\":1}}]," - + "\"friends\":[{\"id\":2},{\"id\":3}]," - + "\"customer\":{\"id\":1}}"); + + "\"embeddedReports\":[{\"dtype\":\"PR\",\"title\":\"This is a good product\",\"product\":{\"id\":1}}]," + + "\"friends\":[{\"id\":2},{\"id\":3}]," + + "\"customer\":{\"id\":1}}"); JsonReadOptions opts = new JsonReadOptions(); opts.setEnableLazyLoading(true); @@ -85,4 +88,85 @@ private ProductReport getProductReport() { report.setProduct(product); return report; } + + /** + * Tests the inheritance support for DocStore/Jsons. + * We do not use default jackson serialization. In Report-class + * there are (de)serializers, that will delegate the (de)serialization + * back to ebean. + *

+ * Big advantage: Ebean supports Inheritance with JSONS and some kind + * of "autodiscovery". + *

+ * In theory, Jackson could do serialization with `@JsonSubTypes`, but + * they have to be specified in the top class. See + * https://github.com/FasterXML/jackson-databind/issues/2104 + */ + @Test + public void testInheritance() { + ReportContainer container = new ReportContainer(); + container.setReport(new ProductReport()); + DB.save(container); + + ReportContainer container2 = new ReportContainer(); + container2.setReport(new CustomerReport()); + DB.save(container2); + + container = DB.find(ReportContainer.class, container.getId()); + container2 = DB.find(ReportContainer.class, container2.getId()); + + assertThat(container.getReport()).isInstanceOf(ProductReport.class); + assertThat(container2.getReport()).isInstanceOf(CustomerReport.class); + } + + /** + * This test shows how we do the (de)serialization when we reference entites, that are persisted in the DB + */ + @Test + public void testReferenceBean() { + ResetBasicData.reset(); + ReportContainer container = new ReportContainer(); + CustomerReport report = new CustomerReport(); + container.setReport(report); + + Object robId = DB.find(Customer.class).where().eq("name", "Rob").findIds().get(0); + report.setFriends(DB.find(Customer.class).where().eq("name", "Fiona").findList()); + LoggedSql.start(); + report.setCustomer(DB.reference(Customer.class, robId)); + DB.save(container); + assertThat(LoggedSql.stop()).hasSize(1); // no lazy load + + container = DB.find(ReportContainer.class, container.getId()); + assertThat(((CustomerReport) container.getReport()).getCustomer().getName()).isEqualTo("Rob"); + assertThat(((CustomerReport) container.getReport()).getFriends().get(0).getName()).isEqualTo("Fiona"); + } + + /** + * This test shows the dirty detection on docstore beans. + */ + @Test + public void testJsonBeanState() { + ResetBasicData.reset(); + ReportContainer container = new ReportContainer(); + CustomerReport report = new CustomerReport(); + report.setTitle("Foo"); + container.setReport(report); + + BeanState state = DB.beanState(container.getReport()); + assertThat(state.isNew()).isTrue(); + + DB.save(container); + container = DB.find(ReportContainer.class, container.getId()); + + state = DB.beanState(container.getReport()); + assertThat(state.isDirty()).isFalse(); + assertThat(state.isNew()).isFalse(); // this detection does not work on JSONs + + container.getReport().setTitle("Bar"); + state = DB.beanState(container.getReport()); + assertThat(state.isDirty()).isTrue(); + //BTW: ValuePair has no equals & hashcode + //assertThat(state.dirtyValues()).hasSize(1).containsEntry("title", new ValuePair("Bar", "Foo")); + assertThat(state.dirtyValues()).hasSize(1).hasToString("{title=Bar,Foo}"); + } } diff --git a/ebean-test/src/test/java/org/tests/model/docstore/Report.java b/ebean-test/src/test/java/org/tests/model/docstore/Report.java index 707a56bd84..17c278f62b 100644 --- a/ebean-test/src/test/java/org/tests/model/docstore/Report.java +++ b/ebean-test/src/test/java/org/tests/model/docstore/Report.java @@ -1,13 +1,19 @@ package org.tests.model.docstore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.ebean.annotation.DocStore; +import javax.persistence.DiscriminatorColumn; import javax.persistence.Inheritance; import javax.persistence.OneToMany; import java.util.List; @DocStore @Inheritance +@DiscriminatorColumn(name = "type") +@JsonSerialize(using = ReportJsonSerializer.class) +@JsonDeserialize(using = ReportJsonDeserializer.class) public class Report { private String title; diff --git a/ebean-test/src/test/java/org/tests/model/docstore/ReportContainer.java b/ebean-test/src/test/java/org/tests/model/docstore/ReportContainer.java new file mode 100644 index 0000000000..4f768223c8 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/docstore/ReportContainer.java @@ -0,0 +1,31 @@ +package org.tests.model.docstore; + +import io.ebean.annotation.DbJson; +import org.tests.model.basic.BasicDomain; + +import javax.persistence.Entity; + +/** + * The reportcontainer itself persists the Report JSON in the database. + * + * @author Roland Praml, FOCONIS AG + */ +@Entity +public class ReportContainer extends BasicDomain { + + // By default, ebean will use Jackson as serializer here. + // It would be great, if ebean will serialize docstore beans automatically with DB.json() + // This is currently achieved through the JsonSerialize/Deserialize annotations in the report class + // (ebean -> jackson -> ebean) + @DbJson(length = 1024 * 1024) // we do not expect report definitions bigger than 1M + private Report report; + + + public Report getReport() { + return report; + } + + public void setReport(Report report) { + this.report = report; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/docstore/ReportJsonDeserializer.java b/ebean-test/src/test/java/org/tests/model/docstore/ReportJsonDeserializer.java new file mode 100644 index 0000000000..76fe07d4e8 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/docstore/ReportJsonDeserializer.java @@ -0,0 +1,33 @@ +/* + * Licensed Materials - Property of FOCONIS AG + * (C) Copyright FOCONIS AG. + */ + +package org.tests.model.docstore; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import io.ebean.DB; +import io.ebean.bean.EntityBean; +import io.ebean.text.json.JsonReadOptions; + +import java.io.IOException; + +/** + * Deserializer, that uses ebean deserialization when deserializing beans from DB. + */ +public class ReportJsonDeserializer extends JsonDeserializer { + + + @Override + public Report deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException { + JsonReadOptions options = new JsonReadOptions(); + options.setEnableLazyLoading(true); + Report bean = DB.json().toBean(Report.class, parser, options); + ((EntityBean) bean)._ebean_getIntercept().setLoaded(); // to make beanstate work + return bean; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/docstore/ReportJsonSerializer.java b/ebean-test/src/test/java/org/tests/model/docstore/ReportJsonSerializer.java new file mode 100644 index 0000000000..30ba17946d --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/docstore/ReportJsonSerializer.java @@ -0,0 +1,52 @@ +package org.tests.model.docstore; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.ebean.DB; +import io.ebean.FetchPath; +import io.ebean.Query; +import io.ebean.text.json.JsonWriteOptions; + +import java.io.IOException; +import java.util.Set; + +/** + * Serializer, that advises Jackson to use ebean serialization instead. + *

+ * It also handles the "id" serialization of referenced properties. + *

+ * Note: This is a very simple class stripped down to provide a test case + * (we use a more generic one in our code) + */ +public class ReportJsonSerializer extends JsonSerializer { + + private static final FetchPath SERIALIZE_TO_DISK = new FetchPath() { + @Override + public boolean hasPath(final String path) { + return true; + } + + @Override + public Set getProperties(final String path) { + if (path == null) { + return Set.of("*"); // for json model itself, serialize all properties + } else { + return Set.of("id"); // for referenced DB-beans (e.g. customers), serialize id only + } + } + + @Override + public void apply(final Query query) { + throw new UnsupportedOperationException(); + } + }; + + @Override + public void serialize(final Report value, final JsonGenerator jgen, final SerializerProvider serializers) throws IOException { + JsonWriteOptions options = new JsonWriteOptions(); + options.setPathProperties(SERIALIZE_TO_DISK); + DB.json().toJson(value, jgen, options); + } + +} From 1a7bd71269e1c955133969f25a3ab53446370916 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 30 Aug 2023 10:20:18 +0200 Subject: [PATCH 2/2] FIX broken tests --- ebean-test/src/test/java/org/tests/model/docstore/Report.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebean-test/src/test/java/org/tests/model/docstore/Report.java b/ebean-test/src/test/java/org/tests/model/docstore/Report.java index 17c278f62b..24ca800d60 100644 --- a/ebean-test/src/test/java/org/tests/model/docstore/Report.java +++ b/ebean-test/src/test/java/org/tests/model/docstore/Report.java @@ -11,7 +11,7 @@ @DocStore @Inheritance -@DiscriminatorColumn(name = "type") +@DiscriminatorColumn @JsonSerialize(using = ReportJsonSerializer.class) @JsonDeserialize(using = ReportJsonDeserializer.class) public class Report {