diff --git a/src/main/java/io/ebean/ExpressionList.java b/src/main/java/io/ebean/ExpressionList.java index 280a6356b..2a2a6938b 100644 --- a/src/main/java/io/ebean/ExpressionList.java +++ b/src/main/java/io/ebean/ExpressionList.java @@ -412,6 +412,11 @@ public interface ExpressionList { */ Query select(String properties); + /** + * Apply the fetchGroup which defines what part of the object graph to load. + */ + Query select(FetchGroup fetchGroup); + /** * Set whether this query uses DISTINCT. *

diff --git a/src/main/java/io/ebean/FetchGroup.java b/src/main/java/io/ebean/FetchGroup.java new file mode 100644 index 000000000..54552a227 --- /dev/null +++ b/src/main/java/io/ebean/FetchGroup.java @@ -0,0 +1,114 @@ +package io.ebean; + +import javax.annotation.Nonnull; + +/** + * Defines what part of the object graph to load (select and fetch clauses). + *

+ * Using a FetchGroup effectively sets the select() and fetch() clauses for a query. It is alternative + * to specifying the select() and fetch() clauses on the query allowing for more re-use of "what to load" + * that can be defined separately from the query and combined with other FetchGroups. + *

+ * + *

Select example

* + *
{@code
+ *
+ * FetchGroup fetchGroup = FetchGroup.of("name, status");
+ *
+ * Customer.query()
+ *   .select(fetchGroup)
+ *   .findList();
+ *
+ * }
+ * + *

Select and fetch example

+ *
{@code
+ *
+ * FetchGroup fetchGroup = FetchGroup
+ *   .select("name, status")
+ *   .fetch("contacts", "firstName, lastName, email")
+ *   .build();
+ *
+ * Customer.query()
+ *   .select(fetchGroup)
+ *   .findList();
+ *
+ * }
+ * + *

Combining FetchGroups

+ *

+ * FetchGroups can be combined together to form another FetchGroup. + *

+ *
{@code
+ *
+ *  FetchGroup FG_ADDRESS = FetchGroup
+ *    .select("line1, line2, city")
+ *    .fetch("country", "name")
+ *    .build();
+ *
+ *  FetchGroup FG_CUSTOMER = FetchGroup
+ *    .select("name, version")
+ *    .fetch("billingAddress", FG_ADDRESS)
+ *    .build();
+ *
+ *
+ *  Customer.query()
+ *    .select(FG_CUSTOMER)
+ *    .findList();
+ *
+ * }
+ */ +public interface FetchGroup { + + /** + * Return the FetchGroup with the given select clause. + *

+ * We use this for simple FetchGroup that only select() properties and do not have additional fetch() clause. + *

+ *
{@code
+   *
+   * FetchGroup fetchGroup = FetchGroup.of("name, status");
+   *
+   * Customer.query()
+   *   .select(fetchGroup)
+   *   .findList();
+   *
+   * }
+ * + * @param select The select clause of the FetchGroup + * + * @return The FetchGroup with the given select clause + */ + @Nonnull + static FetchGroup of(String select) { + return XServiceProvider.fetchGroupOf(select); + } + + /** + * Return the FetchGroupBuilder with the given select clause that we can add fetch clauses to. + *

+ * We chain select() with one or more fetch() clauses to define the object graph to load. + *

+ *
{@code
+   *
+   * FetchGroup fetchGroup = FetchGroup
+   *   .select("name, status")
+   *   .fetch("contacts", "firstName, lastName, email")
+   *   .build();
+   *
+   * Customer.query()
+   *   .select(fetchGroup)
+   *   .findList();
+   *
+   * }
+ * + * @param select The select clause for the FetchGroup + * + * @return The FetchGroupBuilder with the given select clause which we will add fetch clauses to + */ + @Nonnull + static FetchGroupBuilder select(String select) { + return XServiceProvider.fetchGroupSelect(select); + } + +} diff --git a/src/main/java/io/ebean/FetchGroupBuilder.java b/src/main/java/io/ebean/FetchGroupBuilder.java new file mode 100644 index 000000000..65ad49bbf --- /dev/null +++ b/src/main/java/io/ebean/FetchGroupBuilder.java @@ -0,0 +1,87 @@ +package io.ebean; + +import javax.annotation.Nonnull; + +/** + * Builds a FetchGroup by adding fetch clauses. + *

+ * We add select() and fetch() clauses to define the object graph we want to load. + *

+ * + *
{@code
+ *
+ * FetchGroup fetchGroup = FetchGroup
+ *   .select("name, status")
+ *   .fetch("contacts", "firstName, lastName, email")
+ *   .build();
+ *
+ * Customer.query()
+ *   .select(fetchGroup)
+ *   .where()
+ *   ...
+ *   .findList();
+ *
+ * }
+ */ +public interface FetchGroupBuilder { + + /** + * Fetch the path including all its properties. + */ + @Nonnull + FetchGroupBuilder fetch(String path); + + /** + * Fetch the path with the nested fetch group. + */ + @Nonnull + FetchGroupBuilder fetch(String path, FetchGroup nestedGroup); + + /** + * Fetch the path using a query join with the nested fetch group. + */ + @Nonnull + FetchGroupBuilder fetchQuery(String path, FetchGroup nestedGroup); + + /** + * Fetch the path lazily with the nested fetch group. + */ + @Nonnull + FetchGroupBuilder fetchLazy(String path, FetchGroup nestedGroup); + + /** + * Fetch the path including specified properties. + */ + @Nonnull + FetchGroupBuilder fetch(String path, String properties); + + /** + * Fetch the path including all its properties using a query join. + */ + @Nonnull + FetchGroupBuilder fetchQuery(String path); + + /** + * Fetch the path including specified properties using a query join. + */ + @Nonnull + FetchGroupBuilder fetchQuery(String path, String properties); + + /** + * Fetch the path including all its properties lazily. + */ + @Nonnull + FetchGroupBuilder fetchLazy(String path); + + /** + * Fetch the path including specified properties lazily. + */ + @Nonnull + FetchGroupBuilder fetchLazy(String path, String properties); + + /** + * Build and return the FetchGroup. + */ + @Nonnull + FetchGroup build(); +} diff --git a/src/main/java/io/ebean/Query.java b/src/main/java/io/ebean/Query.java index 3fb48c1eb..350393776 100644 --- a/src/main/java/io/ebean/Query.java +++ b/src/main/java/io/ebean/Query.java @@ -397,6 +397,11 @@ public interface Query { */ Query select(String fetchProperties); + /** + * Apply the fetchGroup which defines what part of the object graph to load. + */ + Query select(FetchGroup fetchGroup); + /** * Specify a path to fetch eagerly including specific properties. *

diff --git a/src/main/java/io/ebean/XServiceProvider.java b/src/main/java/io/ebean/XServiceProvider.java index 518e1b3af..53a81993a 100644 --- a/src/main/java/io/ebean/XServiceProvider.java +++ b/src/main/java/io/ebean/XServiceProvider.java @@ -1,5 +1,6 @@ package io.ebean; +import io.ebean.service.SpiFetchGroupService; import io.ebean.service.SpiProfileLocationFactory; import io.ebean.service.SpiRawSqlService; @@ -15,6 +16,16 @@ class XServiceProvider { private static SpiProfileLocationFactory profileLocationFactory = initProfileLocation(); + private static SpiFetchGroupService fetchGroupService = initSpiFetchGroupService(); + + private static SpiFetchGroupService initSpiFetchGroupService() { + Iterator loader = ServiceLoader.load(SpiFetchGroupService.class).iterator(); + if (loader.hasNext()) { + return loader.next(); + } + throw new IllegalStateException("No service implementation found for SpiFetchGroupService?"); + } + private static SpiRawSqlService initRawSql() { Iterator loader = ServiceLoader.load(SpiRawSqlService.class).iterator(); @@ -47,4 +58,17 @@ class XServiceProvider { return profileLocationFactory; } + /** + * Return the FetchGroup with the given select clause. + */ + static FetchGroup fetchGroupOf(String select) { + return fetchGroupService.of(select); + } + + /** + * Return the FetchGroupBuilder with the given select clause. + */ + static FetchGroupBuilder fetchGroupSelect(String select) { + return fetchGroupService.select(select); + } } diff --git a/src/main/java/io/ebean/service/SpiFetchGroupService.java b/src/main/java/io/ebean/service/SpiFetchGroupService.java new file mode 100644 index 000000000..813f5244d --- /dev/null +++ b/src/main/java/io/ebean/service/SpiFetchGroupService.java @@ -0,0 +1,23 @@ +package io.ebean.service; + +import io.ebean.FetchGroup; +import io.ebean.FetchGroupBuilder; + +/** + * Service that parses FetchGroup expressions. + */ +public interface SpiFetchGroupService { + + /** + * Return the FetchGroup with the given select clause. + */ + FetchGroup of(String select); + + /** + * Create and return a FetchGroupBuilder starting with a select() clause. + * + * @param select The properties to select (top level properties) + * @return The FetchGroupBuilder to add additional fetch clauses + */ + FetchGroupBuilder select(String select); +} diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index 45324d954..578e2d9b1 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -6,6 +6,7 @@ import io.ebean.DtoQuery; import io.ebean.Expression; import io.ebean.ExpressionFactory; import io.ebean.ExpressionList; +import io.ebean.FetchGroup; import io.ebean.FetchPath; import io.ebean.FutureIds; import io.ebean.FutureList; @@ -460,6 +461,11 @@ public class DefaultExpressionList implements SpiExpressionList { return query.select(fetchProperties); } + @Override + public Query select(FetchGroup fetchGroup) { + return query.select(fetchGroup); + } + @Override public Query setDistinct(boolean distinct) { return query.setDistinct(distinct); diff --git a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index d927bb22f..b0329a1f0 100644 --- a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -5,6 +5,7 @@ import io.ebean.CountDistinctOrder; import io.ebean.DtoQuery; import io.ebean.Expression; import io.ebean.ExpressionList; +import io.ebean.FetchGroup; import io.ebean.FetchPath; import io.ebean.FutureIds; import io.ebean.FutureList; @@ -759,6 +760,11 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression return exprList.select(properties); } + @Override + public Query select(FetchGroup fetchGroup) { + return exprList.select(fetchGroup); + } + @Override public Query setDistinct(boolean distinct) { return exprList.setDistinct(distinct); diff --git a/src/main/java/io/ebeaninternal/server/query/DFetchGroup.java b/src/main/java/io/ebeaninternal/server/query/DFetchGroup.java new file mode 100644 index 000000000..6bd9cbc8e --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/query/DFetchGroup.java @@ -0,0 +1,26 @@ +package io.ebeaninternal.server.query; + +import io.ebeaninternal.server.querydefn.OrmQueryDetail; +import io.ebeaninternal.server.querydefn.SpiFetchGroup; + +/** + * Default FetchGroup implementation. + */ +class DFetchGroup implements SpiFetchGroup { + + private final OrmQueryDetail detail; + + DFetchGroup(OrmQueryDetail detail) { + this.detail = detail; + } + + @Override + public OrmQueryDetail detail() { + return detail.copy(); + } + + @Override + public OrmQueryDetail underlying() { + return detail; + } +} diff --git a/src/main/java/io/ebeaninternal/server/query/DFetchGroupBuilder.java b/src/main/java/io/ebeaninternal/server/query/DFetchGroupBuilder.java new file mode 100644 index 000000000..76778ee41 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/query/DFetchGroupBuilder.java @@ -0,0 +1,85 @@ +package io.ebeaninternal.server.query; + +import io.ebean.FetchConfig; +import io.ebean.FetchGroup; +import io.ebean.FetchGroupBuilder; +import io.ebeaninternal.server.querydefn.OrmQueryDetail; +import io.ebeaninternal.server.querydefn.SpiFetchGroup; + +/** + * Default implementation of the FetchGroupBuilder. + */ +class DFetchGroupBuilder implements FetchGroupBuilder { + + private static final FetchConfig FETCH_QUERY = new FetchConfig().query(); + + private static final FetchConfig FETCH_LAZY = new FetchConfig().lazy(); + + private OrmQueryDetail detail; + + DFetchGroupBuilder(OrmQueryDetail detail) { + this.detail = detail; + } + + @Override + public FetchGroupBuilder fetch(String path) { + detail.fetch(path, null, null); + return this; + } + + @Override + public FetchGroupBuilder fetch(String path, FetchGroup nestedGroup) { + return fetchNested(path, nestedGroup, null); + } + @Override + public FetchGroupBuilder fetchQuery(String path, FetchGroup nestedGroup) { + return fetchNested(path, nestedGroup, FETCH_QUERY); + } + + @Override + public FetchGroupBuilder fetchLazy(String path, FetchGroup nestedGroup) { + return fetchNested(path, nestedGroup, FETCH_LAZY); + } + + private FetchGroupBuilder fetchNested(String path, FetchGroup nestedGroup, FetchConfig fetchConfig) { + + OrmQueryDetail nestedDetail = ((SpiFetchGroup) nestedGroup).underlying(); + detail.addNested(path, nestedDetail, fetchConfig); + return this; + } + + @Override + public FetchGroupBuilder fetchQuery(String path) { + detail.fetch(path, null, FETCH_QUERY); + return this; + } + + @Override + public FetchGroupBuilder fetchLazy(String path) { + detail.fetch(path, null, FETCH_LAZY); + return this; + } + + @Override + public FetchGroupBuilder fetch(String path, String properties) { + detail.fetch(path, properties, null); + return this; + } + + @Override + public FetchGroupBuilder fetchQuery(String path, String properties) { + detail.fetch(path, properties, FETCH_QUERY); + return this; + } + + @Override + public FetchGroupBuilder fetchLazy(String path, String properties) { + detail.fetch(path, properties, FETCH_LAZY); + return this; + } + + @Override + public FetchGroup build() { + return new DFetchGroup(detail); + } +} diff --git a/src/main/java/io/ebeaninternal/server/query/DFetchGroupService.java b/src/main/java/io/ebeaninternal/server/query/DFetchGroupService.java new file mode 100644 index 000000000..62555997d --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/query/DFetchGroupService.java @@ -0,0 +1,28 @@ +package io.ebeaninternal.server.query; + +import io.ebean.FetchGroup; +import io.ebean.FetchGroupBuilder; +import io.ebean.service.SpiFetchGroupService; +import io.ebeaninternal.server.querydefn.OrmQueryDetail; + +/** + * Default implementation of SpiFetchGroupService. + */ +public final class DFetchGroupService implements SpiFetchGroupService { + + @Override + public FetchGroup of(String select) { + return new DFetchGroup(detail(select)); + } + + @Override + public FetchGroupBuilder select(String select) { + return new DFetchGroupBuilder(detail(select)); + } + + private OrmQueryDetail detail(String select) { + OrmQueryDetail detail = new OrmQueryDetail(); + detail.select(select); + return detail; + } +} diff --git a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index be5b769c5..da8a39f51 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -7,6 +7,7 @@ import io.ebean.Expression; import io.ebean.ExpressionFactory; import io.ebean.ExpressionList; import io.ebean.FetchConfig; +import io.ebean.FetchGroup; import io.ebean.FetchPath; import io.ebean.FutureIds; import io.ebean.FutureList; @@ -1329,6 +1330,12 @@ public class DefaultOrmQuery implements SpiQuery { return this; } + @Override + public DefaultOrmQuery select(FetchGroup fetchGroup) { + this.detail = ((SpiFetchGroup)fetchGroup).detail(); + return this; + } + @Override public DefaultOrmQuery fetch(String property) { return fetch(property, null, null); diff --git a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java index e7fa731b3..3b61ff949 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java @@ -55,6 +55,16 @@ public class OrmQueryDetail implements Serializable { return copy; } + /** + * Add a nested OrmQueryDetail to this detail. + */ + public void addNested(String path, OrmQueryDetail other, FetchConfig config) { + fetch(path, other.baseProps.getProperties(), config); + for (Map.Entry entry : other.fetchPaths.entrySet()) { + fetch(path + "." + entry.getKey(), entry.getValue().getProperties(), entry.getValue().getFetchConfig()); + } + } + /** * Calculate the hash for the query plan. */ diff --git a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java index 82c1f1630..966ca0543 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java @@ -148,27 +148,30 @@ public class OrmQueryProperties implements Serializable { /** * Copy constructor. */ - private OrmQueryProperties(OrmQueryProperties source) { - + private OrmQueryProperties(OrmQueryProperties source, FetchConfig sourceFetchConfig) { + this.fetchConfig = sourceFetchConfig; this.parentPath = source.parentPath; this.path = source.path; this.rawProperties = source.rawProperties; this.trimmedProperties = source.trimmedProperties; this.cache = source.cache; this.readOnly = source.readOnly; - this.fetchConfig = source.fetchConfig; this.filterMany = source.filterMany; this.included = (source.included == null) ? null : new LinkedHashSet<>(source.included); - if (includedBeanJoin != null) { - this.includedBeanJoin = new HashSet<>(source.includedBeanJoin); - } } /** * Creates a copy of the OrmQueryProperties. */ public OrmQueryProperties copy() { - return new OrmQueryProperties(this); + return new OrmQueryProperties(this, this.fetchConfig); + } + + /** + * Create a copy with the given fetch config. + */ + public OrmQueryProperties copy(FetchConfig fetchConfig) { + return new OrmQueryProperties(this, fetchConfig); } /** diff --git a/src/main/java/io/ebeaninternal/server/querydefn/SpiFetchGroup.java b/src/main/java/io/ebeaninternal/server/querydefn/SpiFetchGroup.java new file mode 100644 index 000000000..05935e25c --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/querydefn/SpiFetchGroup.java @@ -0,0 +1,19 @@ +package io.ebeaninternal.server.querydefn; + +import io.ebean.FetchGroup; + +/** + * Service API of FetchGroup. + */ +public interface SpiFetchGroup extends FetchGroup { + + /** + * Return the detail to use for query execution. + */ + OrmQueryDetail detail(); + + /** + * Return the underlying detail for copy purposes. + */ + OrmQueryDetail underlying(); +} diff --git a/src/main/resources/META-INF/services/io.ebean.service.SpiFetchGroupService b/src/main/resources/META-INF/services/io.ebean.service.SpiFetchGroupService new file mode 100644 index 000000000..56c2058bd --- /dev/null +++ b/src/main/resources/META-INF/services/io.ebean.service.SpiFetchGroupService @@ -0,0 +1 @@ +io.ebeaninternal.server.query.DFetchGroupService diff --git a/src/test/java/io/ebean/FetchGroupTest.java b/src/test/java/io/ebean/FetchGroupTest.java new file mode 100644 index 000000000..91bf229b8 --- /dev/null +++ b/src/test/java/io/ebean/FetchGroupTest.java @@ -0,0 +1,113 @@ +package io.ebean; + +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; +import org.tests.model.basic.Customer; +import org.tests.model.basic.ResetBasicData; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FetchGroupTest extends BaseTestCase { + + @Test + public void simple() { + + FetchGroup fetch = FetchGroup.of("name, status"); + + Query query = Customer.find + .query() + .where() + .ilike("name", "rob") + .select(fetch); + + query.findList(); + + assertThat(sqlOf(query)).contains("select t0.id, t0.name, t0.status from"); + } + + + @Test + public void nestedWithQueryJoin() { + + ResetBasicData.reset(); + + FetchGroup fetch = FetchGroup + .select("name, status") + .fetchQuery("contacts", "firstName, lastName, email") + .build(); + + Query query = Customer.find + .query() + .where() + .ilike("name", "rob") + .select(fetch); + + LoggedSqlCollector.start(); + query.findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id, t0.name, t0.status from o_customer"); + assertThat(sql.get(1)).contains("select t0.customer_id, t0.id, t0.first_name, t0.last_name, t0.email from contact"); + } + + + @Test + public void nestedWithQueryJoin_asNestedFetchGroup() { + + ResetBasicData.reset(); + + FetchGroup CT_NAME = FetchGroup.of("firstName, lastName, email"); + + FetchGroup fetch = FetchGroup + .select("name") + .fetchQuery("contacts", CT_NAME) + .build(); + + Query query = Customer.find + .query() + .where() + .ilike("name", "rob") + .select(fetch); + + LoggedSqlCollector.start(); + query.findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id, t0.name from o_customer"); + assertThat(sql.get(1)).contains(" from contact"); + } + + @Test + public void nested_withNestedFetchGroup() { + + ResetBasicData.reset(); + + FetchGroup FGAddress = FetchGroup + .select("line1, line2, city") + .fetch("country", "name") + .build(); + + FetchGroup FBCustomer = FetchGroup + .select("name, version") + .fetch("billingAddress", FGAddress) + .build(); + + Query query = Customer.find + .query() + .where() + .ilike("name", "rob") + .select(FBCustomer); + + LoggedSqlCollector.start(); + query.findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.name, t0.version, t1.id, t1.line_1, t1.line_2, t1.city, t2.code, t2.name from o_customer t0 left join o_address t1 on t1.id = t0.billing_address_id left join o_country t2 on t2.code = t1.country_code "); + } + +}