diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 48985d2ee..11b72808f 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -1502,19 +1502,18 @@ public class BeanDescriptor implements BeanType, STreeType { } /** - * Returns true if it managed to populate/load the bean from the cache. + * Hit the bean cache trying to load a list/batch of entities. + * Return the set of entities that were successfully loaded from L2 cache. */ - public boolean cacheBeanLoad(EntityBean bean, EntityBeanIntercept ebi, Object id, PersistenceContext context) { - return cacheHelp.beanCacheLoad(bean, ebi, cacheKey(id), context); + public Set cacheBeanLoadAll(List list, PersistenceContext persistenceContext, int lazyLoadProperty, String propertyName) { + return cacheHelp.beanCacheLoadAll(list, persistenceContext, lazyLoadProperty, propertyName); } /** * Returns true if it managed to populate/load the bean from the cache. */ - public boolean cacheBeanLoad(EntityBeanIntercept ebi, PersistenceContext context) { - EntityBean bean = ebi.getOwner(); - Object id = getId(bean); - return cacheBeanLoad(bean, ebi, id, context); + public boolean cacheBeanLoad(EntityBean bean, EntityBeanIntercept ebi, Object id, PersistenceContext context) { + return cacheHelp.beanCacheLoad(bean, ebi, cacheKey(id), context); } /** diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index ed5d7cdfb..88c816040 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -700,7 +701,56 @@ final class BeanDescriptorCacheHelp { } /** - * Returns true if it managed to populate/load the bean from the cache. + * Load a batch of entities from L2 bean cache checking the lazy loaded property is loaded. + */ + Set beanCacheLoadAll(List list, PersistenceContext context, int lazyLoadProperty, String propertyName) { + + Map ebis = new HashMap<>(); + for (EntityBeanIntercept ebi : list) { + ebis.put(desc.cacheKeyForBean(ebi.getOwner()), ebi); + } + + + Map hits = getBeanCache().getAll(ebis.keySet()); + + if (beanLog.isTraceEnabled()) { + beanLog.trace(" LOAD ALL {}({}) - got hits ({})", cacheName, ebis.keySet(), hits.size()); + } + + Set loaded = new HashSet<>(); + + Iterator> iterator = hits.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry hit = iterator.next(); + + Object key = hit.getKey(); + EntityBeanIntercept ebi = ebis.remove(key); + CachedBeanData cacheData = (CachedBeanData) hit.getValue(); + + if (lazyLoadProperty > -1 && !cacheData.isLoaded(propertyName)) { + if (beanLog.isTraceEnabled()) { + beanLog.trace(" LOAD {}({}) - cache miss on property({})", cacheName, key, propertyName); + } + iterator.remove(); + + } else { + CachedBeanDataToBean.load(desc, ebi.getOwner(), cacheData, context); + loaded.add(ebi); + if (beanLog.isDebugEnabled()) { + beanLog.debug(" LOAD {}({}) - hit", cacheName, key); + } + } + } + + if (!ebis.isEmpty() && beanLog.isTraceEnabled()) { + beanLog.trace(" LOAD {}({}) - cache miss", cacheName, ebis.keySet()); + } + + return loaded; + } + + /** + * Returns true if it managed to populate/load the single bean from the cache. */ boolean beanCacheLoad(EntityBean bean, EntityBeanIntercept ebi, String key, PersistenceContext context) { diff --git a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java index 574e6f72a..4c820c71c 100644 --- a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -13,6 +13,7 @@ import io.ebeaninternal.server.querydefn.OrmQueryProperties; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * Default implementation of LoadBeanContext. @@ -174,22 +175,19 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex return; } - if (context.hitCache && context.desc.cacheBeanLoad(ebi, persistenceContext)) { - // successfully hit the L2 cache so don't invoke DB lazy loading - list.remove(ebi); - return; - } - if (context.hitCache) { - // check each of the beans in the batch to see if they are in the L2 cache. - // bean successfully loaded from L2 cache so remove from batch load - list.removeIf(batchEbi -> batchEbi != ebi && context.desc.cacheBeanLoad(batchEbi, persistenceContext)); + Set hits = context.desc.cacheBeanLoadAll(list, persistenceContext, ebi.getLazyLoadPropertyIndex(), ebi.getLazyLoadProperty()); + + list.removeAll(hits); + if (list.isEmpty() || hits.contains(ebi)) { + // successfully hit the L2 cache so don't invoke DB lazy loading + return; + } } LoadBeanRequest req = new LoadBeanRequest(this, ebi.getLazyLoadProperty(), context.hitCache); context.desc.getEbeanServer().loadBean(req); } - } } diff --git a/src/test/java/org/tests/cache/personinfo/PersonCacheEmail.java b/src/test/java/org/tests/cache/personinfo/PersonCacheEmail.java new file mode 100644 index 000000000..fb6a7d147 --- /dev/null +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheEmail.java @@ -0,0 +1,49 @@ +package org.tests.cache.personinfo; + +import io.ebean.annotation.Cache; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +@Entity +@Cache +public class PersonCacheEmail { + + @Id + private String id; + + @ManyToOne + private PersonCacheInfo personInfo; + + private String email; + + public PersonCacheEmail(String id, String email) { + this.id = id; + this.email = email; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public PersonCacheInfo getPersonInfo() { + return personInfo; + } + + public void setPersonInfo(PersonCacheInfo personInfo) { + this.personInfo = personInfo; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java b/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java new file mode 100644 index 000000000..cd5325163 --- /dev/null +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java @@ -0,0 +1,37 @@ +package org.tests.cache.personinfo; + +import io.ebean.annotation.Cache; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +@Cache +public class PersonCacheInfo { + + @Id + private String personId; + + private String name; + + public PersonCacheInfo(String personId, String name) { + this.personId = personId; + this.name = name; + } + + public String getPersonId() { + return personId; + } + + public void setPersonId(String personId) { + this.personId = personId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java b/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java new file mode 100644 index 000000000..5cc192eca --- /dev/null +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java @@ -0,0 +1,89 @@ +package org.tests.cache.personinfo; + +import io.ebean.Ebean; +import io.ebean.cache.ServerCache; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PersonCacheTests { + + private void insert(int id, String email) { + + PersonCacheInfo person = new PersonCacheInfo("P00" + id, "P00" + id); + + PersonCacheEmail personEmail = new PersonCacheEmail("E00" + id, email); + personEmail.setPersonInfo(person); + + Ebean.save(person); + Ebean.save(personEmail); + } + + private void addTestData() { + + insert(1, "testA"); + insert(2, "testB"); + insert(3, "testC"); + } + + @Test + public void testInQuery() { + + addTestData(); + + LoggedSqlCollector.start(); + + Ebean.find(PersonCacheInfo.class) + .select("personId") // do not fetch name + .setUseCache(true) + .findList(); + + ServerCache beanCache = Ebean.getDefaultServer().getServerCacheManager().getBeanCache(PersonCacheInfo.class); + beanCache.getStatistics(true); + + List ids = Arrays.asList(new String[]{"E001", "E002", "E003"}); + + List emailList = + Ebean.find(PersonCacheEmail.class) + .where().idIn(ids) + .setUseCache(true) + .findList(); + + + for (PersonCacheEmail email : emailList) { + // get property that isn't in the cached data + assertThat(email.getPersonInfo().getName()).isNotNull(); + System.out.println(email.getPersonInfo().getName()); + } + + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(3); + + assertThat(beanCache.getStatistics(true).getHitCount()).isEqualTo(3); + + System.out.println("Fetch again ..."); + + emailList = + Ebean.find(PersonCacheEmail.class) + .where().idIn(ids) + .setUseCache(true) + .findList(); + + + for (PersonCacheEmail email : emailList) { + // get property but it is in the cache data now + System.out.println(email.getPersonInfo().getName()); + assertThat(email.getPersonInfo().getName()).isNotNull(); + } + + sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + + assertThat(beanCache.getStatistics(true).getHitCount()).isEqualTo(3); + + } +}