#1605 - RuntimeException: Nothing in batch? ... when lazy loading on partially populated L2 bean cache

This commit is contained in:
rob bygrave
2019-01-08 21:44:18 +13:00
parent 5bcad17ae1
commit 7aef4ff1bd
6 changed files with 240 additions and 18 deletions
@@ -1502,19 +1502,18 @@ public class BeanDescriptor<T> implements BeanType<T>, 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<EntityBeanIntercept> cacheBeanLoadAll(List<EntityBeanIntercept> 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);
}
/**
@@ -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<T> {
}
/**
* 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<EntityBeanIntercept> beanCacheLoadAll(List<EntityBeanIntercept> list, PersistenceContext context, int lazyLoadProperty, String propertyName) {
Map<Object, EntityBeanIntercept> ebis = new HashMap<>();
for (EntityBeanIntercept ebi : list) {
ebis.put(desc.cacheKeyForBean(ebi.getOwner()), ebi);
}
Map<Object, Object> hits = getBeanCache().getAll(ebis.keySet());
if (beanLog.isTraceEnabled()) {
beanLog.trace(" LOAD ALL {}({}) - got hits ({})", cacheName, ebis.keySet(), hits.size());
}
Set<EntityBeanIntercept> loaded = new HashSet<>();
Iterator<Map.Entry<Object, Object>> iterator = hits.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Object, Object> 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) {
@@ -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<EntityBeanIntercept> 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);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<Object> ids = Arrays.asList(new String[]{"E001", "E002", "E003"});
List<PersonCacheEmail> 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<String> 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);
}
}