diff --git a/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java b/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java index 9ab3bface..8b28cf679 100644 --- a/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java +++ b/src/main/java/com/avaje/ebean/bean/EntityBeanIntercept.java @@ -451,6 +451,9 @@ public final class EntityBeanIntercept implements Serializable { return -1; } + /** + * Return the property name for the given property. + */ public String getProperty(int propertyIndex) { if (propertyIndex == -1) { return null; @@ -458,18 +461,38 @@ public final class EntityBeanIntercept implements Serializable { return owner._ebean_getPropertyName(propertyIndex); } + /** + * Return the number of properties.s + */ public int getPropertyLength() { return owner._ebean_getPropertyNames().length; } + /** + * Set the property to be treated as unloaded. Used for properties initialised in default + * constructor. + */ + public void setPropertyUnloaded(int propertyIndex) { + loadedProps[propertyIndex] = false; + } + + /** + * Set the property to be loaded. + */ public void setLoadedProperty(int propertyIndex) { loadedProps[propertyIndex] = true; } + /** + * Return true if the property is loaded. + */ public boolean isLoadedProperty(int propertyIndex) { return loadedProps[propertyIndex]; } + /** + * Return true if the property is considered changed. + */ public boolean isChangedProperty(int propertyIndex) { return (changedProps != null && changedProps[propertyIndex]); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 1ad02cab9..3c95d5f16 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -210,6 +210,11 @@ public class BeanDescriptor implements MetaBeanInfo { private final int versionPropertyIndex; + /** + * Properties that are initialised in the constructor need to be 'unloaded' to support partial object queries. + */ + private final int[] unloadProperties; + /** * Properties local to this type (not from a super type). */ @@ -430,14 +435,46 @@ public class BeanDescriptor implements MetaBeanInfo { if (Modifier.isAbstract(beanType.getModifiers())) { this.idPropertyIndex = -1; this.versionPropertyIndex = -1; + this.unloadProperties = new int[0]; } else { EntityBeanIntercept ebi = prototypeEntityBean._ebean_getIntercept(); this.idPropertyIndex = (idProperty == null) ? -1 : ebi.findProperty(idProperty.getName()); this.versionPropertyIndex = (versionProperty == null) ? -1 : ebi.findProperty(versionProperty.getName()); + this.unloadProperties = derivePropertiesToUnload(prototypeEntityBean); } } + /** + * Derive an array of property positions for properties that are initialised in the constructor. + * These properties need to be unloaded when populating beans for queries. + */ + private int[] derivePropertiesToUnload(EntityBean prototypeEntityBean) { + + boolean[] loaded = prototypeEntityBean._ebean_getIntercept().getLoaded(); + int[] props = new int[loaded.length]; + int pos = 0; + + // collect the positions of the properties initialised in the default constructor. + for (int i = 0; i < loaded.length; i++) { + if (loaded[i]) { + props[pos++] = i; + } + } + + if (pos == 0) { + // nothing set in the constructor + return new int[0]; + } + + // populate a smaller/minimal array + int[] unload = new int[pos]; + for (int i = 0; i < pos; i++) { + unload[i] = props[i]; + } + return unload; + } + /** * Create an entity bean that is used as a prototype/factory to create new instances. */ @@ -1142,7 +1179,17 @@ public class BeanDescriptor implements MetaBeanInfo { */ public EntityBean createEntityBean() { try { - return (EntityBean)prototypeEntityBean._ebean_newInstance(); + EntityBean bean = (EntityBean)prototypeEntityBean._ebean_newInstance(); + + if (unloadProperties.length > 0) { + // 'unload' any properties initialised in the default constructor + EntityBeanIntercept ebi = bean._ebean_getIntercept(); + for (int i = 0; i < unloadProperties.length; i++) { + ebi.setPropertyUnloaded(unloadProperties[i]); + } + } + return bean; + } catch (Exception ex) { throw new PersistenceException(ex); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java index 998e25551..1f8cd1d3b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeAlias.java @@ -65,8 +65,8 @@ public class SqlTreeAlias { for (String propJoin : propJoins) { ElPropertyDeploy elProp = desc.getElPropertyDeploy(propJoin); if (elProp != null && elProp.getBeanProperty().isEmbedded()) { - String[] split = SplitName.split(propJoin); - addPropertyJoin(split[0], joinProps); + //String[] split = SplitName.split(propJoin); + //addPropertyJoin(split[0], joinProps); addEmbeddedPropertyJoin(propJoin); } else { diff --git a/src/test/java/com/avaje/tests/autofetch/AutofetchEmbeddedTest.java b/src/test/java/com/avaje/tests/autofetch/AutofetchEmbeddedTest.java index 62345b59b..2a201f256 100644 --- a/src/test/java/com/avaje/tests/autofetch/AutofetchEmbeddedTest.java +++ b/src/test/java/com/avaje/tests/autofetch/AutofetchEmbeddedTest.java @@ -1,14 +1,40 @@ package com.avaje.tests.autofetch; +import java.util.List; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.tests.model.embedded.EMain; -import org.junit.Test; -import java.util.List; +import com.avaje.tests.model.embedded.Eembeddable; public class AutofetchEmbeddedTest extends BaseTestCase { + private final Logger logger = LoggerFactory.getLogger(AutofetchEmbeddedTest.class); + + @Test + public void testEmbeddedBeanLazyLoadAndUpdate() { + + EMain testBean = new EMain(); + testBean.setName("test"); + testBean.getEmbeddable().setDescription("test description"); + Ebean.save(testBean); + + EMain partialBean = Ebean.find(EMain.class).select("version").setId(testBean.getId()).findUnique(); + + logger.info(" -- invoke lazy loading of embedded bean"); + Eembeddable embeddable = partialBean.getEmbeddable(); + embeddable.setDescription("modified description"); + + logger.info(" -- update bean"); + Ebean.save(partialBean); + + } + @Test public void testEmbeddedBeanQueryTuning() { Ebean.getServer(null).getAdminAutofetch().setProfiling(true); diff --git a/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java b/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java index 8762b7e61..f6b586edb 100644 --- a/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java +++ b/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java @@ -7,6 +7,7 @@ import java.util.List; import com.avaje.ebean.Ebean; import com.avaje.ebean.EbeanServer; import com.avaje.ebean.TxRunnable; +import com.avaje.tests.model.basic.Order.Status; public class ResetBasicData { @@ -273,6 +274,7 @@ public class ResetBasicData { Product product1 = Ebean.getReference(Product.class, 1); Order order = new Order(); + order.setStatus(Status.SHIPPED); order.setCustomer(customer); List details = new ArrayList(); @@ -290,6 +292,7 @@ public class ResetBasicData { Product product3 = Ebean.getReference(Product.class, 3); Order order = new Order(); + order.setStatus(Status.COMPLETE); order.setCustomer(customer); List details = new ArrayList(); @@ -301,15 +304,14 @@ public class ResetBasicData { Ebean.save(order); } - - private void createOrder4(Customer customer) { + private void createOrder4(Customer customer) { - Order order = new Order(); - order.setCustomer(customer); + Order order = new Order(); + order.setCustomer(customer); - order.addShipment(new OrderShipment()); + order.addShipment(new OrderShipment()); - Ebean.save(order); - } + Ebean.save(order); + } } diff --git a/src/test/java/com/avaje/tests/model/embedded/EInvoice.java b/src/test/java/com/avaje/tests/model/embedded/EInvoice.java new file mode 100644 index 000000000..f3b9a538b --- /dev/null +++ b/src/test/java/com/avaje/tests/model/embedded/EInvoice.java @@ -0,0 +1,101 @@ +package com.avaje.tests.model.embedded; + +import java.util.Date; + +import javax.persistence.AttributeOverride; +import javax.persistence.AttributeOverrides; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Version; + +@Entity +public class EInvoice { + + public enum State { + New, Processing, Approved + } + + @Id + Long id; + + @Version + Long version; + + Date date; + + State state; + + @ManyToOne + EPerson person; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "street", column = @Column(name = "ship_street")), + @AttributeOverride(name = "suburb", column = @Column(name = "ship_suburb")), + @AttributeOverride(name = "city", column = @Column(name = "ship_city")) + }) + EAddress shipAddress; + + @Embedded + EAddress billAddress; + + 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 State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public EPerson getPerson() { + return person; + } + + public void setPerson(EPerson person) { + this.person = person; + } + + public EAddress getShipAddress() { + return shipAddress; + } + + public void setShipAddress(EAddress shipAddress) { + this.shipAddress = shipAddress; + } + + public EAddress getBillAddress() { + return billAddress; + } + + public void setBillAddress(EAddress billAddress) { + this.billAddress = billAddress; + } + +} diff --git a/src/test/java/com/avaje/tests/query/embedded/TestMultipleEmbeddedLoading.java b/src/test/java/com/avaje/tests/query/embedded/TestMultipleEmbeddedLoading.java new file mode 100644 index 000000000..6d880075f --- /dev/null +++ b/src/test/java/com/avaje/tests/query/embedded/TestMultipleEmbeddedLoading.java @@ -0,0 +1,77 @@ +package com.avaje.tests.query.embedded; + +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.embedded.EAddress; +import com.avaje.tests.model.embedded.EInvoice; +import com.avaje.tests.model.embedded.EInvoice.State; + +public class TestMultipleEmbeddedLoading extends BaseTestCase { + + @Test + public void testSimpleCase() { + + // prepare test + EAddress ship = new EAddress(); + ship.setStreet("1 Banana St"); + ship.setSuburb("Suburb"); + ship.setCity("Auckland"); + + EAddress bill = new EAddress(); + bill.setStreet("2 Apple St"); + bill.setSuburb("Suburb"); + bill.setCity("Auckland"); + + EInvoice invoice = new EInvoice(); + invoice.setDate(new Date(System.currentTimeMillis())); + invoice.setState(State.New); + invoice.setShipAddress(ship); + invoice.setBillAddress(bill); + + // act: save and fetch + Ebean.save(invoice); + + EInvoice invoice2 = Ebean.find(EInvoice.class) + .where().idEq(invoice.getId()) + .findUnique(); + + // assert fetched bean populated as expected + Assert.assertEquals(invoice.getId(), invoice2.getId()); + Assert.assertEquals(invoice.getState(), invoice2.getState()); + Assert.assertEquals(invoice.getDate(), invoice2.getDate()); + Assert.assertEquals("2 Apple St", invoice.getBillAddress().getStreet()); + Assert.assertEquals("2 Apple St", invoice2.getBillAddress().getStreet()); + + // act: only update one of the embedded fields + invoice2.getBillAddress().setStreet("3 Pineapple St"); + // bean should be dirty + Ebean.save(invoice2); + + EInvoice invoice3 = Ebean.find(EInvoice.class) + .where().idEq(invoice.getId()) + .findUnique(); + + // assert field has updated value + Assert.assertEquals("3 Pineapple St", invoice3.getBillAddress().getStreet()); + + + // fetch a partial + EInvoice invoicePartial = Ebean.find(EInvoice.class) + .select("state, date") + .where().idEq(invoice.getId()) + .findUnique(); + + // lazy load of embedded bean + EAddress billAddress = invoicePartial.getBillAddress(); + + Assert.assertNotNull(billAddress); + Assert.assertEquals("3 Pineapple St", billAddress.getStreet()); + + } + +} diff --git a/src/test/java/com/avaje/tests/query/other/TestFindPartialWithConstructorSetFields.java b/src/test/java/com/avaje/tests/query/other/TestFindPartialWithConstructorSetFields.java new file mode 100644 index 000000000..aebbd596c --- /dev/null +++ b/src/test/java/com/avaje/tests/query/other/TestFindPartialWithConstructorSetFields.java @@ -0,0 +1,43 @@ +package com.avaje.tests.query.other; + +import java.sql.Date; +import java.util.List; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.BeanState; +import com.avaje.ebean.Ebean; +import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.basic.Order.Status; +import com.avaje.tests.model.basic.ResetBasicData; + +public class TestFindPartialWithConstructorSetFields extends BaseTestCase { + + @Test + public void test() { + + ResetBasicData.reset(); + + + List list = Ebean.find(Order.class) + .select("shipDate") + .findList(); + + for (Order order : list) { + BeanState beanState = Ebean.getBeanState(order); + Set loadedProps = beanState.getLoadedProps(); + Assert.assertTrue(loadedProps.contains("shipDate")); + Assert.assertTrue(!loadedProps.contains("status")); + Assert.assertTrue(!loadedProps.contains("orderDate")); + + Status status = order.getStatus(); + Date orderDate = order.getOrderDate(); + System.out.println("-- order - "+order.getId()+" status:"+status+" date:"+orderDate); + } + + } + +}