mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-10-23 16:47:18 +00:00
#1922 - Performance - Too pessimistic synchronized block for lazy loading on reference bean or l2 cache bean
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package io.ebean.bean;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Database;
|
||||
import io.ebean.ValuePair;
|
||||
|
||||
import javax.persistence.EntityNotFoundException;
|
||||
@@ -104,9 +105,6 @@ public final class EntityBeanIntercept implements Serializable {
|
||||
|
||||
/**
|
||||
* Create a intercept with a given entity.
|
||||
* <p>
|
||||
* Refer to agent ProxyConstructor.
|
||||
* </p>
|
||||
*/
|
||||
public EntityBeanIntercept(Object ownerBean) {
|
||||
this.owner = (EntityBean) ownerBean;
|
||||
@@ -821,14 +819,14 @@ public final class EntityBeanIntercept implements Serializable {
|
||||
|
||||
synchronized (this) {
|
||||
if (beanLoader == null) {
|
||||
BeanLoader serverLoader = (BeanLoader) DB.byName(ebeanServerName);
|
||||
if (serverLoader == null) {
|
||||
throw new PersistenceException("Server [" + ebeanServerName + "] was not found?");
|
||||
final Database database = DB.byName(ebeanServerName);
|
||||
if (database == null) {
|
||||
throw new PersistenceException("Database [" + ebeanServerName + "] was not found?");
|
||||
}
|
||||
|
||||
// For stand alone reference bean or after deserialisation lazy load
|
||||
// using the ebeanServer. Synchronise only on the bean.
|
||||
loadBeanInternal(loadProperty, serverLoader);
|
||||
loadBeanInternal(loadProperty, database.getPluginApi());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.ebean.bean;
|
||||
|
||||
import io.ebean.Database;
|
||||
|
||||
/**
|
||||
* BeanLoader used when single beans are loaded (which is usually not ideal / N+1).
|
||||
*/
|
||||
public abstract class SingleBeanLoader implements BeanLoader {
|
||||
|
||||
protected final Database database;
|
||||
|
||||
SingleBeanLoader(Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return database.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Single bean lazy loaded when bean from L2 cache.
|
||||
*/
|
||||
public static class L2 extends SingleBeanLoader {
|
||||
public L2(Database database) {
|
||||
super(database);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBean(EntityBeanIntercept ebi) {
|
||||
database.getPluginApi().loadBeanL2(ebi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Single bean lazy loaded when a reference bean.
|
||||
*/
|
||||
public static class Ref extends SingleBeanLoader {
|
||||
public Ref(Database database) {
|
||||
super(database);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBean(EntityBeanIntercept ebi) {
|
||||
database.getPluginApi().loadBeanRef(ebi);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.ebean.plugin;
|
||||
|
||||
import io.ebean.EbeanServer;
|
||||
import io.ebean.bean.BeanLoader;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
import io.ebean.config.ServerConfig;
|
||||
import io.ebean.config.dbplatform.DatabasePlatform;
|
||||
|
||||
@@ -10,7 +12,7 @@ import java.util.List;
|
||||
/**
|
||||
* Extensions to Database API made available to plugins.
|
||||
*/
|
||||
public interface SpiServer extends EbeanServer {
|
||||
public interface SpiServer extends EbeanServer, BeanLoader {
|
||||
|
||||
/**
|
||||
* Return the serverConfig.
|
||||
@@ -52,4 +54,19 @@ public interface SpiServer extends EbeanServer {
|
||||
*/
|
||||
DataSource getReadOnlyDataSource();
|
||||
|
||||
/**
|
||||
* Invoke lazy loading on this single bean (reference bean).
|
||||
*/
|
||||
void loadBeanRef(EntityBeanIntercept ebi);
|
||||
|
||||
/**
|
||||
* Invoke lazy loading on this single bean (L2 cache bean).
|
||||
*/
|
||||
void loadBeanL2(EntityBeanIntercept ebi);
|
||||
|
||||
/**
|
||||
* Invoke lazy loading on this single bean when no BeanLoader is set.
|
||||
* Typically due to serialisation or multiple stateless updates.
|
||||
*/
|
||||
void loadBean(EntityBeanIntercept ebi);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.ebeaninternal.api;
|
||||
|
||||
import io.ebean.meta.MetricType;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.metric.CountMetric;
|
||||
import io.ebean.metric.MetricFactory;
|
||||
import io.ebean.metric.TimedMetric;
|
||||
|
||||
@@ -12,6 +13,9 @@ public class ExtraMetrics {
|
||||
|
||||
private final TimedMetric bindCapture;
|
||||
private final TimedMetric planCollect;
|
||||
private final CountMetric loadOneL2;
|
||||
private final CountMetric loadOneRef;
|
||||
private final CountMetric loadOneNoLoader;
|
||||
|
||||
/**
|
||||
* Create the extra metrics.
|
||||
@@ -20,6 +24,9 @@ public class ExtraMetrics {
|
||||
final MetricFactory factory = MetricFactory.get();
|
||||
this.bindCapture = factory.createTimedMetric(MetricType.ORM, "ebean.queryplan.bindcapture");
|
||||
this.planCollect = factory.createTimedMetric(MetricType.ORM, "ebean.queryplan.collect");
|
||||
this.loadOneL2 = factory.createCountMetric(MetricType.ORM, "loadone.l2");
|
||||
this.loadOneRef = factory.createCountMetric(MetricType.ORM, "loadone.ref");
|
||||
this.loadOneNoLoader = factory.createCountMetric(MetricType.ORM, "loadone.noloader");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,11 +43,38 @@ public class ExtraMetrics {
|
||||
return planCollect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment counter for lazy loading one bean from L2 cache.
|
||||
* All good when lazy loading also hits L2 cache.
|
||||
*/
|
||||
public void incrementLoadOneL2() {
|
||||
loadOneL2.increment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment counter for lazy loading on reference bean.
|
||||
* We ought to be able to avoid this by changing to a tuned query.
|
||||
*/
|
||||
public void incrementLoadOneRef() {
|
||||
loadOneRef.increment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment counter for lazy loading one bean due to no loader.
|
||||
* Likely due to multiple stateless updates or serialisation.
|
||||
*/
|
||||
public void incrementLoadOneNoLoader() {
|
||||
loadOneNoLoader.increment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the metrics.
|
||||
*/
|
||||
public void visitMetrics(MetricVisitor visitor) {
|
||||
bindCapture.visit(visitor);
|
||||
planCollect.visit(visitor);
|
||||
loadOneL2.visit(visitor);
|
||||
loadOneRef.visit(visitor);
|
||||
loadOneNoLoader.visit(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.function.Predicate;
|
||||
/**
|
||||
* Service Provider extension to EbeanServer.
|
||||
*/
|
||||
public interface SpiEbeanServer extends ExtendedServer, EbeanServer, BeanLoader, BeanCollectionLoader {
|
||||
public interface SpiEbeanServer extends ExtendedServer, EbeanServer, BeanCollectionLoader {
|
||||
|
||||
/**
|
||||
* Return the log manager.
|
||||
|
||||
@@ -580,6 +580,19 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
@Override
|
||||
public void loadBean(EntityBeanIntercept ebi) {
|
||||
beanLoader.loadBean(ebi);
|
||||
extraMetrics.incrementLoadOneNoLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBeanRef(EntityBeanIntercept ebi) {
|
||||
beanLoader.loadBean(ebi);
|
||||
extraMetrics.incrementLoadOneRef();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBeanL2(EntityBeanIntercept ebi) {
|
||||
beanLoader.loadBean(ebi);
|
||||
extraMetrics.incrementLoadOneL2();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
import io.ebean.bean.PersistenceContext;
|
||||
import io.ebean.bean.SingleBeanLoader;
|
||||
import io.ebean.cache.QueryCacheEntry;
|
||||
import io.ebean.config.EncryptKey;
|
||||
import io.ebean.config.ServerConfig;
|
||||
@@ -2040,7 +2041,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
|
||||
if (disableLazyLoad) {
|
||||
ebi.setDisableLazyLoad(true);
|
||||
} else {
|
||||
ebi.setBeanLoader(ebeanServer);
|
||||
ebi.setBeanLoader(refBeanLoader());
|
||||
}
|
||||
ebi.setReference(idPropertyIndex);
|
||||
if (Boolean.TRUE == readOnly) {
|
||||
@@ -2058,6 +2059,14 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
|
||||
}
|
||||
}
|
||||
|
||||
SingleBeanLoader refBeanLoader() {
|
||||
return new SingleBeanLoader.Ref(ebeanServer);
|
||||
}
|
||||
|
||||
SingleBeanLoader l2BeanLoader() {
|
||||
return new SingleBeanLoader.L2(ebeanServer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a non read only reference bean without checking cacheSharableBeans.
|
||||
*/
|
||||
@@ -2072,7 +2081,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
|
||||
EntityBean eb = createEntityBean();
|
||||
id = convertSetId(id, eb);
|
||||
EntityBeanIntercept ebi = eb._ebean_getIntercept();
|
||||
ebi.setBeanLoader(ebeanServer);
|
||||
ebi.setBeanLoader(refBeanLoader());
|
||||
ebi.setReference(idPropertyIndex);
|
||||
if (pc != null) {
|
||||
contextPut(pc, id, eb);
|
||||
|
||||
@@ -690,7 +690,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
|
||||
EntityBeanIntercept ebi = bean._ebean_getIntercept();
|
||||
// Not using loadContext here so no batch lazy loading for these beans
|
||||
ebi.setBeanLoader(desc.getEbeanServer());
|
||||
ebi.setBeanLoader(desc.l2BeanLoader());
|
||||
if (Boolean.TRUE.equals(readOnly)) {
|
||||
ebi.setReadOnly(true);
|
||||
}
|
||||
|
||||
@@ -323,10 +323,6 @@ public class TDSpiEbeanServer implements SpiEbeanServer {
|
||||
public void loadMany(BeanCollection<?> collection, boolean onlyIds) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBean(EntityBeanIntercept ebi) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown(boolean shutdownDataSource, boolean deregisterDriver) {
|
||||
}
|
||||
|
||||
+10
-6
@@ -3,13 +3,17 @@ package io.ebeaninternal.server.deploy;
|
||||
import io.ebean.BaseTestCase;
|
||||
import io.ebean.BeanState;
|
||||
import io.ebean.Ebean;
|
||||
import org.junit.Test;
|
||||
import org.tests.model.basic.Order;
|
||||
import org.tests.model.basic.ResetBasicData;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TestReferenceWithConstructorProperties extends BaseTestCase {
|
||||
|
||||
/**
|
||||
@@ -24,14 +28,14 @@ public class TestReferenceWithConstructorProperties extends BaseTestCase {
|
||||
BeanState beanState = Ebean.getBeanState(order);
|
||||
Set<String> loadedProps = beanState.getLoadedProps();
|
||||
|
||||
Assert.assertEquals(1, loadedProps.size());
|
||||
Assert.assertTrue(beanState.isReference());
|
||||
assertEquals(1, loadedProps.size());
|
||||
assertTrue(beanState.isReference());
|
||||
|
||||
// read the status invokes lazy loading
|
||||
order.getStatus();
|
||||
|
||||
Assert.assertFalse(beanState.isReference());
|
||||
|
||||
assertFalse(beanState.isReference());
|
||||
assertNotNull(order.getCustomer());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-4
@@ -9,16 +9,24 @@ import io.ebean.Update;
|
||||
import io.ebean.cache.ServerCache;
|
||||
import io.ebean.cache.ServerCacheManager;
|
||||
import io.ebeaninternal.server.cache.CachedManyIds;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.tests.model.basic.*;
|
||||
import org.tests.model.basic.Contact;
|
||||
import org.tests.model.basic.Country;
|
||||
import org.tests.model.basic.Customer;
|
||||
import org.tests.model.basic.OBeanChild;
|
||||
import org.tests.model.basic.OCachedBean;
|
||||
import org.tests.model.basic.OCachedBeanChild;
|
||||
import org.tests.model.basic.Order;
|
||||
import org.tests.model.basic.OrderDetail;
|
||||
import org.tests.model.basic.ResetBasicData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TestCacheCollectionIds extends BaseTestCase {
|
||||
@@ -88,7 +96,7 @@ public class TestCacheCollectionIds extends BaseTestCase {
|
||||
List<Contact> contacts2 = customer2.getContacts();
|
||||
|
||||
for (Contact contact : contacts2) {
|
||||
contact.getFirstName();
|
||||
assertNotNull(contact.getFirstName());
|
||||
contact.getEmail();
|
||||
}
|
||||
return contacts2.size();
|
||||
@@ -297,7 +305,7 @@ public class TestCacheCollectionIds extends BaseTestCase {
|
||||
// assert that the cache contains the expected entry
|
||||
assertEquals("countries cache now loaded with 1 entry", 1, cachedBeanCountriesCache.size());
|
||||
CachedManyIds dummyEntry = (CachedManyIds) cachedBeanCountriesCache.get(dummyLoad.getId());
|
||||
Assert.assertNotNull(dummyEntry);
|
||||
assertNotNull(dummyEntry);
|
||||
assertEquals("2 ids in the entry", 2, dummyEntry.getIdList().size());
|
||||
assertTrue(dummyEntry.getIdList().contains("NZ"));
|
||||
assertTrue(dummyEntry.getIdList().contains("AU"));
|
||||
|
||||
@@ -12,6 +12,8 @@ import org.tests.model.basic.FeatureDescription;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TestL2CacheWithSharedBean extends BaseTestCase {
|
||||
|
||||
private TunedQueryInfo createTunedQueryInfo(OrmQueryDetail tunedDetail) {
|
||||
@@ -25,7 +27,7 @@ public class TestL2CacheWithSharedBean extends BaseTestCase {
|
||||
|
||||
FeatureDescription f1 = new FeatureDescription();
|
||||
f1.setName("one");
|
||||
f1.setDescription(null);
|
||||
f1.setDescription("helloOne");
|
||||
|
||||
Ebean.save(f1);
|
||||
|
||||
@@ -44,19 +46,20 @@ public class TestL2CacheWithSharedBean extends BaseTestCase {
|
||||
|
||||
FeatureDescription fd2 = query.findOne(); // LOAD cache
|
||||
|
||||
fd2.getDescription(); // invoke lazy load (this fails)
|
||||
String description0 = fd2.getDescription(); // invoke lazy load
|
||||
assertEquals("helloOne", description0);
|
||||
|
||||
// load the cache
|
||||
FeatureDescription fetchOne = Ebean.find(FeatureDescription.class, f1.getId());
|
||||
Assert.assertNotNull(fetchOne);
|
||||
Assert.assertEquals(1, beanCache.getStatistics(false).getSize());
|
||||
assertEquals(1, beanCache.getStatistics(false).getSize());
|
||||
|
||||
FeatureDescription fetchTwo = Ebean.find(FeatureDescription.class, f1.getId());
|
||||
FeatureDescription fetchThree = Ebean.find(FeatureDescription.class, f1.getId());
|
||||
Assert.assertSame(fetchTwo, fetchThree);
|
||||
|
||||
String description = fetchThree.getDescription();
|
||||
Assert.assertNull(description);
|
||||
String description1 = fetchThree.getDescription();
|
||||
assertEquals("helloOne", description1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user