diff --git a/src/main/java/com/avaje/ebean/Query.java b/src/main/java/com/avaje/ebean/Query.java index 8bab2c0cc..65cf369de 100644 --- a/src/main/java/com/avaje/ebean/Query.java +++ b/src/main/java/com/avaje/ebean/Query.java @@ -1257,4 +1257,11 @@ public interface Query extends Serializable { */ Class getBeanType(); + /** + * Set true if you want to disable lazy loading. + *

+ * That is, once the object graph is returned further lazy loading is disabled. + *

+ */ + Query setDisableLazyLoading(boolean disableLazyLoading); } diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index c604174be..dd7eba38c 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -576,9 +576,14 @@ public interface SpiQuery extends Query { */ int getFirstRow(); + /** + * Return true if lazy loading has been disabled on the query. + */ + boolean isDisableLazyLoading(); + /** * Internally set by Ebean when this query must use the DISTINCT keyword. - *

+ *

* This does not exclude/remove the use of the id property. */ Query setSqlDistinct(boolean sqlDistinct); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/DRawSqlSelect.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/DRawSqlSelect.java index 6465edade..1ff9eea90 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/DRawSqlSelect.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/DRawSqlSelect.java @@ -152,7 +152,7 @@ public class DRawSqlSelect { } } - SqlTreeNode sqlRoot = new SqlTreeNodeRoot(desc, selectProps, null, withId); + SqlTreeNode sqlRoot = new SqlTreeNodeRoot(desc, selectProps, withId, false); return new SqlTree(desc.getName(), sqlRoot); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java index dbcdd61ec..b79bacfe7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -49,6 +49,8 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex if (parent.isReadOnly() != null) { query.setReadOnly(parent.isReadOnly()); } + // propagate the asOf and lazy loading mode + query.setDisableLazyLoading(parent.isDisableLazyLoading()); query.asOf(parent.getAsOf()); query.setParentNode(objectGraphNode); query.setLazyLoadProperty(lazyLoadProperty); diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java index 1b6d6ea14..5d60a7862 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadContext.java @@ -36,8 +36,9 @@ public class DLoadContext implements LoadContext { private final Boolean readOnly; private final boolean excludeBeanCache; private final int defaultBatchSize; - - /** + private final boolean disableLazyLoading; + + /** * The path relative to the root of the object graph. */ private final String relativePath; @@ -59,7 +60,8 @@ public class DLoadContext implements LoadContext { SpiQuery query = request.getQuery(); this.asOf = query.getAsOf(); - this.readOnly = query.isReadOnly(); + this.readOnly = query.isReadOnly(); + this.disableLazyLoading = query.isDisableLazyLoading(); this.excludeBeanCache = Boolean.FALSE.equals(query.isUseBeanCache()); this.useAutofetchManager = query.getAutoFetchManager() != null; @@ -219,6 +221,10 @@ public class DLoadContext implements LoadContext { return asOf; } + protected boolean isDisableLazyLoading() { + return disableLazyLoading; + } + public PersistenceContext getPersistenceContext() { return persistenceContext; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java index 7f4739e33..45510f373 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -61,6 +61,8 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex query.setReadOnly(parent.isReadOnly()); } + // propagate the asOf and lazy loading mode + query.setDisableLazyLoading(parent.isDisableLazyLoading()); query.asOf(parent.getAsOf()); query.setParentNode(objectGraphNode); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java index 1944db81b..ac6740f12 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java @@ -71,6 +71,8 @@ public class SqlTreeBuilder { */ private final boolean rawNoId; + private final boolean disableLazyLoad; + private SqlTreeNode rootNode; /** @@ -81,6 +83,7 @@ public class SqlTreeBuilder { this.rawSql = true; this.desc = request.getBeanDescriptor(); this.rawNoId = rawNoId; + this.disableLazyLoad = request.getQuery().isDisableLazyLoading(); this.query = null; this.subQuery = false; this.queryDetail = queryDetail; @@ -104,6 +107,7 @@ public class SqlTreeBuilder { this.rawNoId = false; this.desc = request.getBeanDescriptor(); this.query = request.getQuery(); + this.disableLazyLoad = (query == null) ? false : query.isDisableLazyLoading(); this.subQuery = Type.SUBQUERY.equals(query.getType()) || Type.ID_LIST.equals(query.getType()); this.includeJoin = query.getIncludeTableJoin(); @@ -252,8 +256,7 @@ public class SqlTreeBuilder { } } - private SqlTreeNode buildNode(String prefix, BeanPropertyAssoc prop, BeanDescriptor desc, - List myList) { + private SqlTreeNode buildNode(String prefix, BeanPropertyAssoc prop, BeanDescriptor desc, List myList) { OrmQueryProperties queryProps = queryDetail.getChunk(prefix, false); @@ -265,14 +268,13 @@ public class SqlTreeBuilder { // Optional many property for lazy loading query BeanPropertyAssocMany lazyLoadMany = (query == null) ? null : query.getLazyLoadForParentsProperty(); boolean withId = !rawNoId && !subQuery && (query == null || !query.isDistinct()); - - return new SqlTreeNodeRoot(desc, props, myList, withId, includeJoin, lazyLoadMany, SpiQuery.TemporalMode.of(query)); + return new SqlTreeNodeRoot(desc, props, myList, withId, includeJoin, lazyLoadMany, SpiQuery.TemporalMode.of(query), disableLazyLoad); } else if (prop instanceof BeanPropertyAssocMany) { - return new SqlTreeNodeManyRoot(prefix, (BeanPropertyAssocMany) prop, props, myList); + return new SqlTreeNodeManyRoot(prefix, (BeanPropertyAssocMany) prop, props, myList, disableLazyLoad); } else { - return new SqlTreeNodeBean(prefix, prop, props, myList, true); + return new SqlTreeNodeBean(prefix, prop, props, myList, true, disableLazyLoad); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java index 848a0969a..f1e1d7df4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -87,49 +87,58 @@ public class SqlTreeNodeBean implements SqlTreeNode { */ protected String intersectionAsOfTableAlias; - public SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, SqlTreeProperties props, - List myChildren, boolean withId) { + /** + * Construct for Raw SQL. + */ + public SqlTreeNodeBean(BeanDescriptor desc, SqlTreeProperties props, boolean withId, boolean disableLazyLoad) { + this(null, null, desc, props, null, withId, null, null, disableLazyLoad); + } - this(prefix, beanProp, beanProp.getTargetDescriptor(), props, myChildren, withId, null, SpiQuery.TemporalMode.CURRENT); + /** + * Construct for leaf node. + */ + public SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, SqlTreeProperties props, + List myChildren, boolean withId, boolean disableLazyLoad) { + + this(prefix, beanProp, beanProp.getTargetDescriptor(), props, myChildren, withId, null, SpiQuery.TemporalMode.CURRENT, disableLazyLoad); + } + + /** + * Construct for root node. + */ + public SqlTreeNodeBean(BeanDescriptor desc, SqlTreeProperties props, List myList, boolean withId, + BeanPropertyAssocMany many, SpiQuery.TemporalMode temporalMode, boolean disableLazyLoad) { + this(null, null, desc, props, myList, withId, many, temporalMode, disableLazyLoad); } /** * Create with the appropriate node. */ - public SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, BeanDescriptor desc, - SqlTreeProperties props, List myChildren, boolean withId, BeanPropertyAssocMany lazyLoadParent, SpiQuery.TemporalMode temporalMode) { + private SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, BeanDescriptor desc, SqlTreeProperties props, + List myChildren, boolean withId, BeanPropertyAssocMany lazyLoadParent, + SpiQuery.TemporalMode temporalMode, boolean disableLazyLoad) { this.lazyLoadParent = lazyLoadParent; this.lazyLoadParentIdBinder = (lazyLoadParent == null) ? null : lazyLoadParent.getBeanDescriptor().getIdBinder(); this.prefix = prefix; - this.nodeBeanProp = beanProp; this.desc = desc; + this.inheritInfo = desc.getInheritInfo(); + this.idBinder = desc.getIdBinder(); this.temporalMode = temporalMode; this.temporalVersions = temporalMode == SpiQuery.TemporalMode.VERSIONS; - this.inheritInfo = desc.getInheritInfo(); + this.nodeBeanProp = beanProp; this.extraWhere = (beanProp == null) ? null : beanProp.getExtraWhere(); - this.idBinder = desc.getIdBinder(); - // the bean has an Id property and we want to use it this.readId = withId && (desc.getIdProperty() != null); - this.disableLazyLoad = !readId || desc.isSqlSelectBased() || temporalVersions; + this.disableLazyLoad = disableLazyLoad || !readId || desc.isSqlSelectBased() || temporalVersions; this.tableJoins = props.getTableJoins(); - this.partialObject = props.isPartialObject(); - this.readOnlyLeaf = props.isReadOnly(); - this.properties = props.getProps(); - - - if (myChildren == null) { - children = NO_CHILDREN; - } else { - children = myChildren.toArray(new SqlTreeNode[myChildren.size()]); - } + this.children = myChildren == null ? NO_CHILDREN : myChildren.toArray(new SqlTreeNode[myChildren.size()]); pathMap = createPathMap(prefix, desc); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java index b2962ca52..6086c2a8f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java @@ -10,10 +10,10 @@ import com.avaje.ebeaninternal.server.deploy.DbSqlContext; public final class SqlTreeNodeManyRoot extends SqlTreeNodeBean { - final BeanPropertyAssocMany manyProp; + private final BeanPropertyAssocMany manyProp; - public SqlTreeNodeManyRoot(String prefix, BeanPropertyAssocMany prop, SqlTreeProperties props, List myList) { - super(prefix, prop, prop.getTargetDescriptor(), props, myList, true, null, null); + public SqlTreeNodeManyRoot(String prefix, BeanPropertyAssocMany prop, SqlTreeProperties props, List myList, boolean disableLazyLoad) { + super(prefix, prop, props, myList, true, disableLazyLoad); this.manyProp = prop; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java index c9e10ca18..047d52c95 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java @@ -19,13 +19,17 @@ public final class SqlTreeNodeRoot extends SqlTreeNodeBean { * Specify for SqlSelect to include an Id property or not. */ public SqlTreeNodeRoot(BeanDescriptor desc, SqlTreeProperties props, List myList, boolean withId, - TableJoin includeJoin, BeanPropertyAssocMany many, SpiQuery.TemporalMode temporalMode) { - super(null, null, desc, props, myList, withId, many, temporalMode); + TableJoin includeJoin, BeanPropertyAssocMany many, SpiQuery.TemporalMode temporalMode, boolean disableLazyLoad) { + + super(desc, props, myList, withId, many, temporalMode, disableLazyLoad); this.includeJoin = includeJoin; } - public SqlTreeNodeRoot(BeanDescriptor desc, SqlTreeProperties props, List myList, boolean withId) { - super(null, null, desc, props, myList, withId, null, null); + /** + * Construct for raw sql. + */ + public SqlTreeNodeRoot(BeanDescriptor desc, SqlTreeProperties props, boolean withId, boolean disableLazyLoad) { + super(desc, props, withId, disableLazyLoad); this.includeJoin = null; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index bd8f7c09b..ee41cba03 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -83,6 +83,11 @@ public class DefaultOrmQuery implements SpiQuery { private int firstRow; + /** + * Set to true to disable lazy loading on the object graph returned. + */ + private boolean disableLazyLoading; + /** * Lazy loading batch size (can override server wide default). */ @@ -710,6 +715,7 @@ public class DefaultOrmQuery implements SpiQuery { builder.add(firstRow).add(maxRows).add(orderBy).add(forUpdate); builder.add(rawWhereClause).add(additionalWhere).add(additionalHaving); builder.add(mapKey); + builder.add(disableLazyLoading); builder.add(id != null); builder.add(asOf != null); builder.add(rawSql == null ? 0 : rawSql.queryHash()); @@ -1138,6 +1144,16 @@ public class DefaultOrmQuery implements SpiQuery { this.includeTableJoin = includeTableJoin; } + @Override + public Query setDisableLazyLoading(boolean disableLazyLoading) { + this.disableLazyLoading = disableLazyLoading; + return this; + } + + public boolean isDisableLazyLoading() { + return disableLazyLoading; + } + public int getFirstRow() { return firstRow; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java index d1b37e028..21509ae86 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java @@ -341,6 +341,9 @@ public class OrmQueryProperties implements Serializable { if (filterMany != null) { filterMany.queryPlanHash(request, builder); } + builder.add(lazyFetchBatch); + builder.add(queryFetchBatch); + builder.add(queryFetchAll); } public String getProperties() { diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java new file mode 100644 index 000000000..ede7a01d3 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java @@ -0,0 +1,40 @@ +package com.avaje.ebeaninternal.server.querydefn; + + +import com.avaje.ebean.Ebean; +import com.avaje.ebean.FetchConfig; +import com.avaje.ebeaninternal.api.HashQueryPlan; +import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; +import com.avaje.ebeaninternal.api.SpiQuery; +import com.avaje.tests.model.basic.Order; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultOrmQueryTest { + + @Test + public void testQueryAutofetchHash() throws Exception { + + SpiQuery query1 = (SpiQuery)Ebean.find(Order.class) + .select("status, shipDate") + .fetch("details", "orderQty, unitPrice", new FetchConfig().query()) + .fetch("details.product", "sku, name"); + + HashQueryPlanBuilder b1 = new HashQueryPlanBuilder(); + query1.queryAutofetchHash(b1); + HashQueryPlan hash1 = b1.build(); + + SpiQuery query2 = (SpiQuery)Ebean.find(Order.class) + .select("status, shipDate") + .fetch("details", "orderQty, unitPrice") + .fetch("details.product", "sku, name"); + + HashQueryPlanBuilder b2 = new HashQueryPlanBuilder(); + query2.queryAutofetchHash(b2); + HashQueryPlan hash2 = b2.build(); + + assertThat(hash1).isNotEqualTo(hash2); + + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/tests/batchload/TestQueryJoinToAssocOne.java b/src/test/java/com/avaje/tests/batchload/TestQueryJoinToAssocOne.java index e1603cdde..ac4996c82 100644 --- a/src/test/java/com/avaje/tests/batchload/TestQueryJoinToAssocOne.java +++ b/src/test/java/com/avaje/tests/batchload/TestQueryJoinToAssocOne.java @@ -4,6 +4,7 @@ import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.ebean.FetchConfig; import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.basic.OrderDetail; import com.avaje.tests.model.basic.ResetBasicData; import org.avaje.ebeantest.LoggedSqlCollector; import org.junit.Test; @@ -15,7 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class TestQueryJoinToAssocOne extends BaseTestCase { @Test - public void testLazyOnNonLoaded() { + public void testQueryJoinOnFullyPopulatedParent() { ResetBasicData.reset(); @@ -23,6 +24,33 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { // This will use 2 SQL queries to build this object graph List l0 = Ebean.find(Order.class) + .fetch("details", "orderQty, unitPrice", new FetchConfig().query()) + .fetch("details.product", "sku, name") + .findList(); + + assertThat(l0).isNotEmpty(); + + List loggedSql = LoggedSqlCollector.stop(); + assertThat(loggedSql).hasSize(2); + + String secondaryQuery = loggedSql.get(1); + assertThat(secondaryQuery).contains("select t0.order_id c0, t0.id c1,"); + assertThat(secondaryQuery).contains(" from o_order_detail t0 left outer join o_product t1"); + assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); + } + + + @Test + public void testQueryJoinOnPartiallyPopulatedParent() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + + // This will use 2 SQL queries to build this object graph + List l0 = Ebean.find(Order.class) + .setUseCache(false) .select("status, shipDate") .fetch("details", "orderQty, unitPrice", new FetchConfig().query()) .fetch("details.product", "sku, name") @@ -33,5 +61,81 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(2); + String secondaryQuery = loggedSql.get(1); + assertThat(secondaryQuery).contains("select t0.order_id c0, t0.id c1,"); + assertThat(secondaryQuery).contains(" from o_order_detail t0 left outer join o_product t1"); + assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); + } + + @Test + public void testQueryJoinOnPartiallyPopulatedParent_withLazyLoadingDisabled() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + + // This will use 2 SQL queries to build this object graph + List l0 = Ebean.find(Order.class) + .setUseCache(false) + .setDisableLazyLoading(true) + .select("status, shipDate") + .fetch("details", "orderQty, unitPrice", new FetchConfig().query()) + .fetch("details.product", "sku, name") + .findList(); + + assertThat(l0).isNotEmpty(); + + Order order = l0.get(0); + // normally invokes lazy loading + order.getOrderDate(); + + List details = order.getDetails(); + OrderDetail orderDetail = details.get(0); + // normally invokes lazy loading + orderDetail.getShipQty(); + + List loggedSql = LoggedSqlCollector.stop(); + assertThat(loggedSql).hasSize(2); + + String secondaryQuery = loggedSql.get(1); + assertThat(secondaryQuery).contains("select t0.order_id c0, t0.id c1,"); + assertThat(secondaryQuery).contains(" from o_order_detail t0 left outer join o_product t1"); + assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); + } + + @Test + public void testJoinOnPartiallyPopulatedParent_withLazyLoadingDisabled() { + + ResetBasicData.reset(); + + LoggedSqlCollector.start(); + + // This will use 2 SQL queries to build this object graph + List l0 = Ebean.find(Order.class) + .setDisableLazyLoading(true) + .select("status, shipDate") + .fetch("details", "orderQty, unitPrice")//, new FetchConfig().query()) + .fetch("details.product", "sku, name") + .findList(); + + assertThat(l0).isNotEmpty(); + + Order order = l0.get(0); + // normally invokes lazy loading + order.getOrderDate(); + + List details = order.getDetails(); + OrderDetail orderDetail = details.get(0); + // normally invokes lazy loading + orderDetail.getShipQty(); + + List loggedSql = LoggedSqlCollector.stop(); + assertThat(loggedSql).hasSize(1); + + String originQuery = loggedSql.get(0); + assertThat(originQuery).contains("select t0.id c0, t0.status c1, t0.ship_date c2, t1.id c3, t1.order_qty c4, t1.unit_price c5"); + assertThat(originQuery).contains(" from o_order t0 left outer join o_order_detail t1 "); } }