diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookup.java b/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookup.java index 0ae791747..cf29cf6d5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookup.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookup.java @@ -1,52 +1,25 @@ package io.ebeaninternal.api; -import io.ebeaninternal.server.expression.IdInExpression; - -import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** - * Used for bean cache lookup with where ids in expression. + * Process Cache lookup by Id(s). */ -public class CacheIdLookup { - - private final IdInExpression idInExpression; - - private int remaining; - - public CacheIdLookup(IdInExpression idInExpression) { - this.idInExpression = idInExpression; - } +public interface CacheIdLookup { /** - * Return the Id values for the in expression. + * Return the Id values to lookup against the L2 cache. */ - public Collection idValues() { - return idInExpression.idValues(); - } + Collection idValues(); /** - * Process the hits returning the beans fetched from cache and - * adjusting the in expression (to not fetch the hits). + * Remove the hits returning the beans fetched from L2 cache. */ - public List removeHits(BeanCacheResult cacheResult) { + List removeHits(BeanCacheResult cacheResult); - Set hitIds = new HashSet<>(); - List beans = new ArrayList<>(hitIds.size()); - - for (BeanCacheResult.Entry hit : cacheResult.hits()) { - hitIds.add(hit.getKey()); - beans.add(hit.getBean()); - } - - this.remaining = idInExpression.removeIds(hitIds); - return beans; - } - - public boolean allHits() { - return remaining == 0; - } + /** + * Return true if all beans where found in L2 cache. + */ + boolean allHits(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookupMany.java b/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookupMany.java new file mode 100644 index 000000000..bfe9d5b84 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookupMany.java @@ -0,0 +1,55 @@ +package io.ebeaninternal.api; + +import io.ebeaninternal.server.expression.IdInExpression; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Used for bean cache lookup with where ids in expression. + */ +public class CacheIdLookupMany implements CacheIdLookup { + + private final IdInExpression idInExpression; + + private int remaining; + + public CacheIdLookupMany(IdInExpression idInExpression) { + this.idInExpression = idInExpression; + } + + /** + * Return the Id values for the in expression. + */ + @Override + public Collection idValues() { + return idInExpression.idValues(); + } + + /** + * Process the hits returning the beans fetched from cache and + * adjusting the in expression (to not fetch the hits). + */ + @Override + public List removeHits(BeanCacheResult cacheResult) { + + Set hitIds = new HashSet<>(); + List beans = new ArrayList<>(hitIds.size()); + + for (BeanCacheResult.Entry hit : cacheResult.hits()) { + hitIds.add(hit.getKey()); + beans.add(hit.getBean()); + } + + this.remaining = idInExpression.removeIds(hitIds); + return beans; + } + + @Override + public boolean allHits() { + return remaining == 0; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookupSingle.java b/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookupSingle.java new file mode 100644 index 000000000..47ee89c4b --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/api/CacheIdLookupSingle.java @@ -0,0 +1,38 @@ +package io.ebeaninternal.api; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Used for bean cache lookup with a single id value. + */ +public class CacheIdLookupSingle implements CacheIdLookup { + + private final Object idValue; + private boolean found; + + public CacheIdLookupSingle(Object idValue) { + this.idValue = idValue; + } + + @Override + public Collection idValues() { + return Collections.singleton(idValue); + } + + @Override + public List removeHits(BeanCacheResult cacheResult) { + final List> hits = cacheResult.hits(); + if (hits.size() == 1) { + found = true; + return Collections.singletonList(hits.get(0).getBean()); + } + return Collections.emptyList(); + } + + @Override + public boolean allHits() { + return found; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryFetch.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryFetch.java index 396827978..716dbc076 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryFetch.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryFetch.java @@ -1,6 +1,7 @@ package io.ebeaninternal.api; import io.ebean.FetchConfig; +import io.ebeaninternal.server.querydefn.OrmQueryDetail; import java.util.Set; @@ -19,4 +20,8 @@ public interface SpiQueryFetch { */ void fetchProperties(String name, Set properties, FetchConfig config); + /** + * Add a nested fetch graph. + */ + void addNested(String name, OrmQueryDetail nestedDetail, FetchConfig config); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java index 5b2ed841a..9f7cb9b7e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java @@ -644,4 +644,9 @@ class DefaultFetchGroupQuery implements SpiFetchGroupQuery, SpiQueryFetch public void fetchProperties(String property, Set columns, FetchConfig config) { detail.fetchProperties(property, columns, config); } + + @Override + public void addNested(String name, OrmQueryDetail nestedDetail, FetchConfig config) { + detail.addNested(name, nestedDetail, config); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 074f364bc..867e16dfa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -35,6 +35,8 @@ import io.ebean.plugin.BeanType; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CQueryPlanKey; import io.ebeaninternal.api.CacheIdLookup; +import io.ebeaninternal.api.CacheIdLookupMany; +import io.ebeaninternal.api.CacheIdLookupSingle; import io.ebeaninternal.api.HashQuery; import io.ebeaninternal.api.ManyWhereJoins; import io.ebeaninternal.api.NaturalKeyQueryData; @@ -740,10 +742,14 @@ public class DefaultOrmQuery implements SpiQuery { return null; } List underlyingList = whereExpressions.getUnderlyingList(); - if (underlyingList.size() == 1) { + if (underlyingList.isEmpty()) { + if (id != null) { + return new CacheIdLookupSingle<>(id); + } + } else if (underlyingList.size() == 1) { SpiExpression singleExpression = underlyingList.get(0); if (singleExpression instanceof IdInExpression) { - return new CacheIdLookup<>((IdInExpression) singleExpression); + return new CacheIdLookupMany<>((IdInExpression) singleExpression); } } return null; @@ -1412,6 +1418,11 @@ public class DefaultOrmQuery implements SpiQuery { detail.fetchProperties(path, other); } + @Override + public void addNested(String name, OrmQueryDetail nestedDetail, FetchConfig config) { + detail.addNested(name, nestedDetail, config); + } + @Override public Query select(String columns) { detail.select(columns); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java index d1a3aeafc..466bb3eb9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java @@ -120,8 +120,8 @@ public class OrmQueryProperties implements Serializable { this.parentPath = SplitName.parent(path); this.allProperties = other.allProperties; this.included = other.included; - this.cache = other.cache; this.fetchConfig = fetchConfig; + this.cache = fetchConfig.isCache(); } /** diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java index c54862151..d5843a612 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java @@ -2,7 +2,10 @@ package io.ebean.typequery; import io.ebean.ExpressionList; import io.ebean.FetchConfig; +import io.ebean.FetchGroup; import io.ebeaninternal.api.SpiQueryFetch; +import io.ebeaninternal.server.querydefn.OrmQueryDetail; +import io.ebeaninternal.server.querydefn.SpiFetchGroup; import java.util.LinkedHashSet; import java.util.Set; @@ -142,7 +145,34 @@ public abstract class TQAssocBean extends TQProperty { return _root; } - private final SpiQueryFetch spiQuery() { + /** + * Fetch using the nested FetchGroup. + */ + public R fetch(FetchGroup nestedGroup) { + return fetchNested(nestedGroup, FETCH_DEFAULT); + } + + /** + * Fetch query using the nested FetchGroup. + */ + public R fetchQuery(FetchGroup nestedGroup) { + return fetchNested(nestedGroup, FETCH_QUERY); + } + + /** + * Fetch cache using the nested FetchGroup. + */ + public R fetchCache(FetchGroup nestedGroup) { + return fetchNested(nestedGroup, FETCH_CACHE); + } + + private R fetchNested(FetchGroup nestedGroup, FetchConfig fetchConfig) { + OrmQueryDetail nestedDetail = ((SpiFetchGroup) nestedGroup).underlying(); + spiQuery().addNested(_name, nestedDetail, fetchConfig); + return _root; + } + + private SpiQueryFetch spiQuery() { return (SpiQueryFetch)((TQRootBean) _root).query(); } diff --git a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java index 2f470fdb5..db899549c 100644 --- a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java @@ -3,9 +3,13 @@ package org.querytest; import io.ebean.DB; import io.ebean.FetchGroup; import io.ebean.test.LoggedSql; +import org.example.domain.Customer; import org.example.domain.Order; +import org.example.domain.otherpackage.PhoneNumber; import org.example.domain.query.QCustomer; import org.example.domain.query.QOrder; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import java.util.List; @@ -18,6 +22,26 @@ public class QOrderTest { private static final QOrder or = QOrder.alias(); + private static final FetchGroup fgC = QCustomer.forFetchGroup() + .select(cu.name, cu.phoneNumber) + .buildFetchGroup(); + + private static final FetchGroup fgNested1 = QOrder.forFetchGroup() + .select(or.status, or.shipDate) + .customer.fetch(fgC) + .buildFetchGroup(); + + private static final FetchGroup fgNested_fetchQuery = QOrder.forFetchGroup() + .select(or.status) + .customer.fetchQuery(fgC) + .buildFetchGroup(); + + private static final FetchGroup fgNested_fetchCache = QOrder.forFetchGroup() + .select(or.status) + .customer.fetchCache(fgC) + .buildFetchGroup(); + + private static final FetchGroup fg = QOrder.forFetchGroup() .select(or.status, or.shipDate) .customer.fetchCache(cu.name, cu.status, cu.registered, cu.comments) @@ -28,6 +52,20 @@ public class QOrderTest { .customer.fetch(cu.name) .buildFetchGroup(); + private static Order order; + private static Customer customer; + + @BeforeClass + public static void before() { + setupData(); + } + + @AfterClass + public static void after() { + DB.delete(order); + DB.delete(customer); + } + @Test public void fetchCache() { @@ -76,6 +114,69 @@ public class QOrderTest { assertThat(sql.get(0)).contains("select t0.id, t0.status, t1.id, t1.name from o_order t0 join be_customer t1 on t1.id = t0.customer_id where"); } + @Test + public void viaFetchGraph_withNested() { + + DB.getDefault(); + LoggedSql.start(); + + new QOrder() + .status.eq(Order.Status.NEW) + .select(fgNested1) + .findList(); + + final List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.ship_date, t1.id, t1.name, t1.phone_number from o_order t0 join be_customer t1 on t1.id = t0.customer_id where"); + } + + @Test + public void viaFetchGraph_withNested_fetchQuery() { + + DB.getDefault(); + LoggedSql.start(); + + final Order found = new QOrder() + .id.eq(order.getId()) + .select(fgNested_fetchQuery) + .findOne(); + + final List sql = LoggedSql.stop(); + + // assert fetching customer via fetchQuery + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.customer_id from o_order t0 where t0.id = ?"); + assertThat(sql.get(1)).contains("select t0.id, t0.name, t0.phone_number from be_customer t0 where t0.id = ?"); + + assertThat(found.getCustomer().getPhoneNumber().getMsisdn()).isEqualTo("Ph1"); + } + + + @Test + public void viaFetchGraph_withNested_fetchCache() { + + DB.getDefault(); + + // ensure the customer is loaded in the L2 cache + new QCustomer().id.eq(customer.getId()).findOne(); + + LoggedSql.start(); + + final Order found = new QOrder() + .id.eq(order.getId()) + .select(fgNested_fetchCache) // cache hit for customer + .findOne(); + + final String msisdn = found.getCustomer().getPhoneNumber().getMsisdn(); + assertThat(msisdn).isEqualTo("Ph1"); + + final List sql = LoggedSql.stop(); + + // assert we only hit DB for order + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.customer_id from o_order t0 where t0.id = ?"); + } + @Test public void select_partial() { @@ -113,4 +214,15 @@ public class QOrderTest { } + private static void setupData() { + + customer = new Customer(); + customer.setName("Fred"); + customer.setPhoneNumber(new PhoneNumber("Ph1")); + customer.save(); + + order = new Order(); + order.setCustomer(customer); + order.save(); + } }