Support unmodifiable Embedded and EmbeddedId

This commit is contained in:
Rob Bygrave
2025-03-13 22:52:45 +13:00
parent fa25d7cc8a
commit 5136a5a708
17 changed files with 89 additions and 24 deletions
@@ -7,6 +7,8 @@ import java.sql.SQLException;
public interface DataReader {
boolean unmodifiable();
void close() throws SQLException;
boolean next() throws SQLException;
@@ -67,7 +67,7 @@ public final class DtoQueryRequest<T> 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);
}
@@ -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) {
@@ -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);
@@ -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);
@@ -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);
}
}
@@ -309,7 +309,7 @@ public final class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfi
if (resultSet == null) {
return false;
}
dataReader = queryPlan.createDataReader(resultSet);
dataReader = queryPlan.createDataReader(query.isUnmodifiable(), resultSet);
return true;
}
@@ -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();
}
@@ -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);
}
/**
@@ -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) {
@@ -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<T> 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));
}
@@ -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;
@@ -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;
}
@@ -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<CKeyParentId> ids = DB.find(CKeyParent.class).where().eq("id.oneKey", 101).findIds();
assertThat(ids).hasSize(1);
List<CKeyParentId> 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");
@@ -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<CkeUser> 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);
@@ -138,6 +138,23 @@ class TestQueryOrderById extends BaseTestCase {
assertThat(read.getName()).isEqualTo(customer.getName());
}
@Test
void unmodifiableWithReference() {
ResetBasicData.reset();
List<Contact> 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();
@@ -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