From 5136a5a708d0bad19014821ad87182001af44c8d Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 13 Mar 2025 22:52:45 +1300 Subject: [PATCH] Support unmodifiable Embedded and EmbeddedId --- .../java/io/ebean/core/type/DataReader.java | 2 ++ .../server/core/DtoQueryRequest.java | 2 +- .../server/deploy/AssocOneHelp.java | 2 +- .../server/deploy/AssocOneHelpEmbedded.java | 4 ++-- .../server/deploy/id/IdBinderEmbedded.java | 3 +-- .../ebeaninternal/server/persist/Binder.java | 4 ++-- .../io/ebeaninternal/server/query/CQuery.java | 2 +- .../query/CQueryFetchSingleAttribute.java | 2 +- .../server/query/CQueryPlan.java | 4 ++-- .../server/query/CQueryPlanRawSql.java | 4 ++-- .../query/DefaultRelationalQueryEngine.java | 6 +++--- .../server/type/RsetDataReader.java | 9 ++++++++- .../server/type/RsetDataReaderIndexed.java | 4 ++-- .../tests/compositekeys/TestCKeyDelete.java | 10 ++++++++++ .../composite/TestCompositeKeyUserClient.java | 19 +++++++++++++++++-- .../org/tests/query/TestQueryOrderById.java | 17 +++++++++++++++++ .../embedded/TestMultipleEmbeddedLoading.java | 19 +++++++++++++++++-- 17 files changed, 89 insertions(+), 24 deletions(-) diff --git a/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java b/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java index d9a1ade92..4deac7d30 100644 --- a/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java +++ b/ebean-core-type/src/main/java/io/ebean/core/type/DataReader.java @@ -7,6 +7,8 @@ import java.sql.SQLException; public interface DataReader { + boolean unmodifiable(); + void close() throws SQLException; boolean next() throws SQLException; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java index 3cb56ec27..0a28950c9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java @@ -67,7 +67,7 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { @Override protected void setResultSet(ResultSet resultSet, Object queryPlanKey) throws SQLException { this.resultSet = resultSet; - this.dataReader = new RsetDataReader(server.dataTimeZone(), resultSet); + this.dataReader = new RsetDataReader(false, server.dataTimeZone(), resultSet); obtainPlan(queryPlanKey); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelp.java index 449d66590..b68764409 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelp.java @@ -14,7 +14,7 @@ import java.sql.SQLException; abstract class AssocOneHelp { final BeanPropertyAssocOne property; - private final BeanDescriptor target; + protected final BeanDescriptor target; private final String path; AssocOneHelp(BeanPropertyAssocOne property) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelpEmbedded.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelpEmbedded.java index 228dbb599..36937211f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelpEmbedded.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/AssocOneHelpEmbedded.java @@ -32,7 +32,7 @@ final class AssocOneHelpEmbedded extends AssocOneHelp { @Override Object read(DataReader reader) throws SQLException { - EntityBean embeddedBean = property.targetDescriptor.createEntityBean(); + EntityBean embeddedBean = property.targetDescriptor.createEntityBean2(reader.unmodifiable()); boolean notNull = false; for (BeanProperty property : property.embeddedProps) { Object value = property.readSet(reader, embeddedBean); @@ -62,7 +62,7 @@ final class AssocOneHelpEmbedded extends AssocOneHelp { @Override Object read(DbReadContext ctx) throws SQLException { - EntityBean embeddedBean = property.targetDescriptor.createEntityBean(); + EntityBean embeddedBean = property.targetDescriptor.createEntityBean2(ctx.unmodifiable()); boolean notNull = false; for (BeanProperty property : property.embeddedProps) { Object value = property.readSet(ctx, embeddedBean); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java index 2199e236d..a648e02bc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java @@ -3,7 +3,6 @@ package io.ebeaninternal.server.deploy.id; import io.ebean.bean.EntityBean; import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiExpressionBind; -import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.core.DefaultSqlUpdate; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanProperty; @@ -324,7 +323,7 @@ public final class IdBinderEmbedded implements IdBinder { @Override public Object read(DbReadContext ctx) throws SQLException { - final EntityBean embId = idDesc.createEntityBean(); + final EntityBean embId = idDesc.createEntityBean2(ctx.unmodifiable()); boolean nullValue = true; for (BeanProperty prop : props) { final Object value = prop.read(ctx); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java index 304f163b8..b83fffe6d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -401,7 +401,7 @@ public final class Binder { return new DataBind(dataTimeZone, stmt, connection); } - public DataReader createDataReader(ResultSet resultSet) { - return new RsetDataReader(dataTimeZone, resultSet); + public DataReader createDataReader(boolean unmodifiable, ResultSet resultSet) { + return new RsetDataReader(unmodifiable, dataTimeZone, resultSet); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java index e1d0ddfe3..3fa2465e1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java @@ -309,7 +309,7 @@ public final class CQuery implements DbReadContext, CancelableQuery, SpiProfi if (resultSet == null) { return false; } - dataReader = queryPlan.createDataReader(resultSet); + dataReader = queryPlan.createDataReader(query.isUnmodifiable(), resultSet); return true; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java index 1f55bf292..3564eea07 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java @@ -137,7 +137,7 @@ final class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent, Ca } finally { lock.unlock(); } - dataReader = new RsetDataReader(request.dataTimeZone(), pstmt.executeQuery()); + dataReader = new RsetDataReader(query.isUnmodifiable(), request.dataTimeZone(), pstmt.executeQuery()); query.checkCancelled(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java index fc982503d..f2cfb9a50 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java @@ -205,8 +205,8 @@ public class CQueryPlan implements SpiQueryPlan { return new DQueryPlanOutput(beanType(), name, hash, sql, profileLocation, bind, planString); } - public DataReader createDataReader(ResultSet rset) { - return new RsetDataReader(dataTimeZone, rset); + public DataReader createDataReader(boolean unmodifiable, ResultSet rset) { + return new RsetDataReader(unmodifiable, dataTimeZone, rset); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRawSql.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRawSql.java index f19e3fd37..4d8132de0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRawSql.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRawSql.java @@ -22,8 +22,8 @@ final class CQueryPlanRawSql extends CQueryPlan { } @Override - public DataReader createDataReader(ResultSet rset) { - return new RsetDataReaderIndexed(dataTimeZone, rset, rsetIndexPositions); + public DataReader createDataReader(boolean unmodifiable, ResultSet rset) { + return new RsetDataReaderIndexed(unmodifiable, dataTimeZone, rset, rsetIndexPositions); } private int[] createIndexPositions(OrmQueryRequest request, SqlTree sqlTree) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java index 1c10577c1..f716d76c0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java @@ -147,7 +147,7 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine request.setDefaultFetchBuffer(defaultFetchSizeFindList); } request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); - final DataReader dataReader = binder.createDataReader(request.resultSet()); + final DataReader dataReader = binder.createDataReader(false, request.resultSet()); T value = null; if (dataReader.next()) { value = scalarType.read(dataReader); @@ -172,7 +172,7 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine request.setDefaultFetchBuffer(defaultFetchSizeFindList); } request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); - final DataReader dataReader = binder.createDataReader(request.resultSet()); + final DataReader dataReader = binder.createDataReader(false, request.resultSet()); List rows = new ArrayList<>(); while (dataReader.next()) { rows.add(scalarType.read(dataReader)); @@ -197,7 +197,7 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine request.setDefaultFetchBuffer(defaultFetchSizeFindEach); } request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); - final DataReader dataReader = binder.createDataReader(request.resultSet()); + final DataReader dataReader = binder.createDataReader(false, request.resultSet()); while (dataReader.next()) { consumer.accept(scalarType.read(dataReader)); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java index 5bf529d77..8b9efca04 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReader.java @@ -23,16 +23,23 @@ public class RsetDataReader implements DataReader { static final int clobBufferSize = 512; static final int stringInitialSize = 512; + private final boolean unmodifiable; private final DataTimeZone dataTimeZone; private final ResultSet rset; protected int pos; private String json; - public RsetDataReader(DataTimeZone dataTimeZone, ResultSet rset) { + public RsetDataReader(boolean unmodifiable, DataTimeZone dataTimeZone, ResultSet rset) { + this.unmodifiable = unmodifiable; this.dataTimeZone = dataTimeZone; this.rset = rset; } + @Override + public boolean unmodifiable() { + return unmodifiable; + } + @Override public final void pushJson(String json) { this.json = json; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReaderIndexed.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReaderIndexed.java index 773a19bb8..0638b4c13 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReaderIndexed.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/RsetDataReaderIndexed.java @@ -11,8 +11,8 @@ public final class RsetDataReaderIndexed extends RsetDataReader { private final int[] rsetIndexPositions; - public RsetDataReaderIndexed(DataTimeZone dataTimeZone, ResultSet rset, int[] rsetIndexPositions) { - super(dataTimeZone, rset); + public RsetDataReaderIndexed(boolean unmodifiable, DataTimeZone dataTimeZone, ResultSet rset, int[] rsetIndexPositions) { + super(unmodifiable, dataTimeZone, rset); this.rsetIndexPositions = rsetIndexPositions; } diff --git a/ebean-test/src/test/java/org/tests/compositekeys/TestCKeyDelete.java b/ebean-test/src/test/java/org/tests/compositekeys/TestCKeyDelete.java index b6507998d..504bf29a8 100644 --- a/ebean-test/src/test/java/org/tests/compositekeys/TestCKeyDelete.java +++ b/ebean-test/src/test/java/org/tests/compositekeys/TestCKeyDelete.java @@ -1,5 +1,6 @@ package org.tests.compositekeys; +import io.ebean.UnmodifiableEntityException; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import org.junit.jupiter.api.Test; @@ -9,6 +10,7 @@ import org.tests.model.basic.CKeyParentId; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -51,6 +53,14 @@ public class TestCKeyDelete extends BaseTestCase { List ids = DB.find(CKeyParent.class).where().eq("id.oneKey", 101).findIds(); assertThat(ids).hasSize(1); + List idsUnmodifiable = DB.find(CKeyParent.class) + .setUnmodifiable(true) + .where().eq("id.oneKey", 101) + .findIds(); + assertThat(idsUnmodifiable).hasSize(1); + assertThatThrownBy(() -> idsUnmodifiable.get(0).setOneKey(7)) + .isInstanceOf(UnmodifiableEntityException.class); + CKeyParentId foundId = ids.get(0); assertThat(foundId.getOneKey()).isEqualTo(101); assertThat(foundId.getTwoKey()).isEqualTo("deleteMe2"); diff --git a/ebean-test/src/test/java/org/tests/model/composite/TestCompositeKeyUserClient.java b/ebean-test/src/test/java/org/tests/model/composite/TestCompositeKeyUserClient.java index d2e67f631..98aed0f96 100644 --- a/ebean-test/src/test/java/org/tests/model/composite/TestCompositeKeyUserClient.java +++ b/ebean-test/src/test/java/org/tests/model/composite/TestCompositeKeyUserClient.java @@ -1,5 +1,6 @@ package org.tests.model.composite; +import io.ebean.UnmodifiableEntityException; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.test.LoggedSql; @@ -8,11 +9,12 @@ import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class TestCompositeKeyUserClient extends BaseTestCase { +class TestCompositeKeyUserClient extends BaseTestCase { @Test - public void test() { + void test() { CkeUser user0 = new CkeUser(); user0.setUserPK(new CkeUserKey(20, "sally")); @@ -29,6 +31,19 @@ public class TestCompositeKeyUserClient extends BaseTestCase { client.setClientPK(new CkeClientKey(20, "susan")); client.setUser(user1); + List unmodifiable = DB.find(CkeUser.class) + .setUnmodifiable(true) + .where().eq("userPK.codCompany", 20) + .findList(); + + assertThat(unmodifiable).hasSize(2); + for (CkeUser ckeUser : unmodifiable) { + CkeUserKey userPK = ckeUser.getUserPK(); + assertThatThrownBy(() -> userPK.setCodCompany(7)) + .describedAs("EmbeddedId is unmodifiable") + .isInstanceOf(UnmodifiableEntityException.class); + } + LoggedSql.start(); DB.save(client); diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java b/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java index a2453bd4b..97dafe88d 100644 --- a/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java +++ b/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java @@ -138,6 +138,23 @@ class TestQueryOrderById extends BaseTestCase { assertThat(read.getName()).isEqualTo(customer.getName()); } + @Test + void unmodifiableWithReference() { + ResetBasicData.reset(); + + List result = DB.find(Contact.class) + .setUnmodifiable(true) + .findList(); + + assertThat(result).isNotEmpty(); + for (Contact contact : result) { + Customer customer = contact.getCustomer(); + assertThat(customer.getId()).isNotNull(); + assertThatThrownBy(() -> customer.setId(42)) + .isInstanceOf(UnsupportedOperationException.class); + } + } + @Test void immutableResult() { ResetBasicData.reset(); diff --git a/ebean-test/src/test/java/org/tests/query/embedded/TestMultipleEmbeddedLoading.java b/ebean-test/src/test/java/org/tests/query/embedded/TestMultipleEmbeddedLoading.java index 1bdb21ab3..c585112e6 100644 --- a/ebean-test/src/test/java/org/tests/query/embedded/TestMultipleEmbeddedLoading.java +++ b/ebean-test/src/test/java/org/tests/query/embedded/TestMultipleEmbeddedLoading.java @@ -1,5 +1,6 @@ package org.tests.query.embedded; +import io.ebean.CacheMode; import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Database; @@ -14,13 +15,14 @@ import org.tests.model.embedded.EInvoice.State; import java.util.Date; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -public class TestMultipleEmbeddedLoading extends BaseTestCase { +class TestMultipleEmbeddedLoading extends BaseTestCase { @Test - public void testSimpleCase() { + void testSimpleCase() { // prepare test EAddress ship = new EAddress(); @@ -51,6 +53,19 @@ public class TestMultipleEmbeddedLoading extends BaseTestCase { assertEquals("2 Apple St", invoice.getBillAddress().getStreet()); assertEquals("2 Apple St", invoice2.getBillAddress().getStreet()); + EInvoice readOnlyInvoice = DB.find(EInvoice.class) + .setId(invoice.getId()) + .setUnmodifiable(true) + .setBeanCacheMode(CacheMode.OFF) + .findOne(); + + assertThatThrownBy(() -> readOnlyInvoice.getBillAddress().setCity("junk")) + .describedAs("embedded bean is unmodifiable") + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> readOnlyInvoice.getShipAddress().setCity("junk")) + .describedAs("embedded bean is unmodifiable") + .isInstanceOf(UnsupportedOperationException.class); + // act: only update one of the embedded fields invoice2.getBillAddress().setStreet("3 Pineapple St"); // bean should be dirty