diff --git a/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java b/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java index 9d67499d1..ed03080cd 100644 --- a/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java +++ b/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java @@ -60,21 +60,15 @@ public interface PersistenceContext { int size(Class rootType); /** - * Return a copy of the Persistence context to use for large query iteration. + * Signalizes the PersistenceContext, the begin for large query iteration. */ - PersistenceContext forIterate(); + void beginIterate(); /** - * Return a new Persistence context during iteration of large query result. + * Signalizes the PersistenceContext, the end for large query iteration. */ - PersistenceContext forIterateReset(); - - /** - * Return true if the persistence context has grown and hit the 'reset limit' - * during large query iteration. - */ - boolean resetLimit(); - + void endIterate(); + /** * Wrapper on a bean to also indicate if a bean has been deleted. *

diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java index fc948016f..7acdb976f 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java @@ -20,7 +20,7 @@ public interface JsonBeanReader { /** * Create a new reader taking the context from the existing one but using a new JsonParser. */ - JsonBeanReader forJson(JsonParser moreJson, boolean resetContext); + JsonBeanReader forJson(JsonParser moreJson); /** * Add a bean explicitly to the persistence context. diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/LoadContext.java b/ebean-core/src/main/java/io/ebeaninternal/api/LoadContext.java index 51f02826c..b23a183bb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/LoadContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/LoadContext.java @@ -36,14 +36,6 @@ public interface LoadContext { */ PersistenceContext getPersistenceContext(); - /** - * Set the persistence context used by this query and future lazy loading. - *

- * Used by query iterator when processing large result sets. - *

- */ - void resetPersistenceContext(PersistenceContext persistenceContext); - /** * Register a Bean for lazy loading. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java index aaf4e2ce8..4594cbe25 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java @@ -14,7 +14,7 @@ public interface SpiJsonReader { PersistenceContext getPersistenceContext(); - SpiJsonReader forJson(JsonParser moreJson, boolean resetContext); + SpiJsonReader forJson(JsonParser moreJson); void persistenceContextPut(Object beanId, T currentBean); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index 85d89ffe2..6ef1e352b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -225,6 +225,9 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery createdTransaction = true; } persistenceContext = persistenceContext(query, transaction); + if (Type.ITERATE == query.getType()) { + persistenceContext.beginIterate(); + } loadContext = new DLoadContext(this, secondaryQueries); } @@ -233,6 +236,9 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery */ @Override public void rollbackTransIfRequired() { + if (Type.ITERATE == query.getType()) { + persistenceContext.endIterate(); + } if (createdTransaction) { try { transaction.end(); @@ -263,20 +269,6 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery return jsonRead; } - /** - * For iterate queries reset the persistenceContext and loadContext. - */ - public void flushPersistenceContextOnIterate() { - if (persistenceContext.resetLimit()) { - persistenceContext = persistenceContext.forIterateReset(); - loadContext.resetPersistenceContext(persistenceContext); - if (jsonRead != null) { - jsonRead.setPersistenceContext(persistenceContext); - jsonRead.setLoadContext(loadContext); - } - } - } - /** * Get the TransactionContext either explicitly set on the query or * transaction scoped. @@ -292,11 +284,7 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery if (scope == PersistenceContextScope.QUERY || t == null) { return new DefaultPersistenceContext(); } - if (Type.ITERATE == query.getType()) { - return t.getPersistenceContext().forIterate(); - } else { - return t.getPersistenceContext(); - } + return t.getPersistenceContext(); } /** @@ -306,6 +294,9 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery */ @Override public void endTransIfRequired() { + if (Type.ITERATE == query.getType()) { + persistenceContext.endIterate(); + } if (createdTransaction && transaction.isActive()) { transaction.commit(); if (query.getType().isUpdate()) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java index 5abb988c7..b11c987bb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java @@ -90,7 +90,7 @@ final class BeanDescriptorJsonHelp { return null; } JsonParser newParser = node.traverse(); - SpiJsonReader newReader = jsonRead.forJson(newParser, false); + SpiJsonReader newReader = jsonRead.forJson(newParser); // check for the discriminator value to determine the correct sub type String discColumn = inheritInfo.getRoot().getDiscriminatorColumn(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java index 2dd6c1378..427d59de2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -46,7 +46,7 @@ final class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext /** * Reset the buffers after a query iterator reset. */ - public void clear() { + private void clear() { if (bufferList != null) { bufferList.clear(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java index 018a9f647..bfef45804 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java @@ -57,7 +57,7 @@ public final class DLoadContext implements LoadContext { private final ProfileLocation profileLocation; private final ProfilingListener profilingListener; private final Map nodePathMap = new HashMap<>(); - private PersistenceContext persistenceContext; + private final PersistenceContext persistenceContext; private List secQuery; private Object tenantId; @@ -251,19 +251,6 @@ public final class DLoadContext implements LoadContext { return persistenceContext; } - @Override - public void resetPersistenceContext(PersistenceContext persistenceContext) { - this.persistenceContext = persistenceContext; - // clear the load contexts for beans and beanCollections - for (DLoadBeanContext beanContext : beanMap.values()) { - beanContext.clear(); - } - for (DLoadManyContext manyContext : manyMap.values()) { - manyContext.clear(); - } - this.rootBeanContext.clear(); - } - @Override public void register(String path, EntityBeanIntercept ebi) { getBeanContext(path).register(ebi); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java index 8473e148b..68bd61d81 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -48,7 +48,7 @@ final class DLoadManyContext extends DLoadBaseContext implements LoadManyContext /** * Reset the buffers for a query iterator reset. */ - public void clear() { + private void clear() { if (bufferList != null) { bufferList.clear(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java index d438ec747..6d183b936 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorSimple.java @@ -26,7 +26,6 @@ final class CQueryIteratorSimple implements QueryIterator { public boolean hasNext() { boolean ret = false; try { - request.flushPersistenceContextOnIterate(); ret = cquery.hasNext(); return ret; } catch (SQLException e) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java index ad1a924b5..3df436716 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryIteratorWithBuffer.java @@ -35,8 +35,6 @@ final class CQueryIteratorWithBuffer implements QueryIterator { try { if (buffer.isEmpty() && moreToLoad) { // load buffer - request.flushPersistenceContextOnIterate(); - int i = -1; while (moreToLoad && ++i < bufferSize) { if (cquery.hasNext()) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/text/json/DJsonBeanReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/text/json/DJsonBeanReader.java index 8193d397c..79465e037 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/text/json/DJsonBeanReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/text/json/DJsonBeanReader.java @@ -45,7 +45,7 @@ public final class DJsonBeanReader implements JsonBeanReader { } @Override - public JsonBeanReader forJson(JsonParser moreJson, boolean resetContext) { - return new DJsonBeanReader<>(desc, readJson.forJson(moreJson, resetContext)); + public JsonBeanReader forJson(JsonParser moreJson) { + return new DJsonBeanReader<>(desc, readJson.forJson(moreJson)); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/text/json/ReadJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/text/json/ReadJson.java index f685188b6..7acf4e531 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/text/json/ReadJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/text/json/ReadJson.java @@ -50,22 +50,14 @@ public final class ReadJson implements SpiJsonReader { /** * Construct when transferring load context, persistence context, object mapper etc to a new ReadJson instance. */ - private ReadJson(JsonParser moreJson, ReadJson source, boolean resetContext) { + private ReadJson(JsonParser moreJson, ReadJson source) { this.parser = moreJson; this.rootDesc = source.rootDesc; this.pathStack = source.pathStack; this.visitorMap = source.visitorMap; this.objectMapper = source.objectMapper; - if (resetContext) { - this.persistenceContext = new DefaultPersistenceContext(); - this.loadContext = source.loadContext; - if (loadContext != null) { - loadContext.resetPersistenceContext(persistenceContext); - } - } else { - this.persistenceContext = source.persistenceContext; - this.loadContext = source.loadContext; - } + this.persistenceContext = source.persistenceContext; + this.loadContext = source.loadContext; } private LoadContext initLoadContext(BeanDescriptor desc, JsonReadOptions readOptions) { @@ -96,8 +88,8 @@ public final class ReadJson implements SpiJsonReader { * Return a new instance of ReadJson using the existing context but with a new JsonParser. */ @Override - public SpiJsonReader forJson(JsonParser moreJson, boolean resetContext) { - return new ReadJson(moreJson, this, resetContext); + public SpiJsonReader forJson(JsonParser moreJson) { + return new ReadJson(moreJson, this); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java index 169ca59a3..1ff3d44fa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java @@ -6,6 +6,9 @@ import io.ebeaninternal.api.SpiBeanType; import io.ebeaninternal.api.SpiBeanTypeManager; import io.ebeaninternal.api.SpiPersistenceContext; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; import java.util.*; import java.util.concurrent.locks.ReentrantLock; @@ -25,14 +28,16 @@ import java.util.concurrent.locks.ReentrantLock; */ public final class DefaultPersistenceContext implements SpiPersistenceContext { - /** - * Map used hold caches. One cache per bean type. - */ private final HashMap, ClassContext> typeCache = new HashMap<>(); - private final ReentrantLock lock = new ReentrantLock(); + private final ReferenceQueue queue = new ReferenceQueue(); - private int putCount; + /** + * When we are inside an iterate loop, we will add only WeakReferences. This + * allows the JVM GC to collect beans, which are not referenced elsewhere. In + * normal operation, we will use hard references, to avoid performance impact + */ + private int iterateDepth; /** * Create a new PersistenceContext. @@ -40,46 +45,22 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public DefaultPersistenceContext() { } - /** - * Create as a shallow copy with initial or types that have not been added to. - */ - private DefaultPersistenceContext(DefaultPersistenceContext parent, boolean initial) { - for (Map.Entry, ClassContext> entry : parent.typeCache.entrySet()) { - typeCache.put(entry.getKey(), entry.getValue().copy(initial)); + @Override + public void beginIterate() { + lock.lock(); + try { + iterateDepth++; + } finally { + lock.unlock(); } } - /** - * Return the initial shallow copy with each ClassContext noting it's initialSize (to detect additions). - */ @Override - public PersistenceContext forIterate() { - return new DefaultPersistenceContext(this, true); - } - - /** - * Return a shallow copy including each ClassContext that has had no additions (still at initialSize). - */ - @Override - public PersistenceContext forIterateReset() { - return new DefaultPersistenceContext(this, false); - } - - @Override - public boolean resetLimit() { + public void endIterate() { lock.lock(); try { - if (putCount < 100) { - return false; - } - putCount = 0; - for (ClassContext value : typeCache.values()) { - if (value.resetLimit()) { - return true; - } - } - // checking after another 100 puts - return false; + iterateDepth--; + expungeStaleEntries(); // when leaving the iterator, cleanup. } finally { lock.unlock(); } @@ -92,8 +73,8 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public void put(Class rootType, Object id, Object bean) { lock.lock(); try { - putCount++; - classContext(rootType).put(id, bean); + expungeStaleEntries(); + classContext(rootType).useReferences(iterateDepth > 0).put(id, bean); } finally { lock.unlock(); } @@ -103,8 +84,8 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public Object putIfAbsent(Class rootType, Object id, Object bean) { lock.lock(); try { - putCount++; - return classContext(rootType).putIfAbsent(id, bean); + expungeStaleEntries(); + return classContext(rootType).useReferences(iterateDepth > 0).putIfAbsent(id, bean); } finally { lock.unlock(); } @@ -117,6 +98,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public Object get(Class rootType, Object id) { lock.lock(); try { + expungeStaleEntries(); return classContext(rootType).get(id); } finally { lock.unlock(); @@ -127,6 +109,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public WithOption getWithOption(Class rootType, Object id) { lock.lock(); try { + expungeStaleEntries(); return classContext(rootType).getWithOption(id); } finally { lock.unlock(); @@ -140,6 +123,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public int size(Class rootType) { lock.lock(); try { + expungeStaleEntries(); ClassContext classMap = typeCache.get(rootType); return classMap == null ? 0 : classMap.size(); } finally { @@ -155,6 +139,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { lock.lock(); try { typeCache.clear(); + expungeStaleEntries(); } finally { lock.unlock(); } @@ -168,6 +153,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { if (classMap != null) { classMap.clear(); } + expungeStaleEntries(); } finally { lock.unlock(); } @@ -181,6 +167,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { if (classMap != null && id != null) { classMap.deleted(id); } + expungeStaleEntries(); } finally { lock.unlock(); } @@ -194,6 +181,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { if (classMap != null && id != null) { classMap.remove(id); } + expungeStaleEntries(); } finally { lock.unlock(); } @@ -203,6 +191,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public List dirtyBeans(SpiBeanTypeManager manager) { lock.lock(); try { + expungeStaleEntries(); List list = new ArrayList<>(); for (ClassContext classContext : typeCache.values()) { classContext.dirtyBeans(manager, list); @@ -213,10 +202,23 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { } } + /** + * When there is a queue, poll it to remove stale entries from the map. Note: + * This is always done AFTER useReferences was called with + * true. Polling an empty queue has no performance impact. + */ + private void expungeStaleEntries() { + Reference ref; + while ((ref = queue.poll()) != null) { + ((BeanRef)ref).expunge(); + } + } + @Override public String toString() { lock.lock(); try { + expungeStaleEntries(); return typeCache.toString(); } finally { lock.unlock(); @@ -224,69 +226,47 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { } private ClassContext classContext(Class rootType) { - return typeCache.computeIfAbsent(rootType, k -> new ClassContext(rootType)); + return typeCache.computeIfAbsent(rootType, k -> new ClassContext(k, queue)); } - + private static class ClassContext { private final Map map = new HashMap<>(); private final Class rootType; + private final ReferenceQueue queue; private Set deleteSet; - private int initialSize; - private ClassContext parent; + + private boolean useReferences; + private int weakCount; - private ClassContext(Class rootType) { + + private ClassContext(Class rootType, ReferenceQueue queue) { this.rootType = rootType; + this.queue = queue; } /** - * Create as a shallow copy. + * When called with "true", initialize referenceQueue and store BeanRefs instead + * of real object references. */ - private ClassContext(ClassContext source, boolean initial) { - this.rootType = source.rootType; - if (initial || source.isTransfer()) { - parent = source.transferParent(); - initialSize = parent.size(); - if (source.deleteSet != null) { - deleteSet = new HashSet<>(source.deleteSet); - } - } + public ClassContext useReferences(boolean useReferences) { + this.useReferences = useReferences; + return this; } - /** - * True if this should be transferred to a new iterator persistence context. - */ - private boolean isTransfer() { - // map not added to and has some original/parent beans - return map.isEmpty() && initialSize > 0; - } - - private ClassContext transferParent() { - return (parent != null) ? parent : this; - } - - /** - * Return a shallow copy if initial copy or it has not grown (still at initialSize). - */ - private ClassContext copy(boolean initial) { - return new ClassContext(this, initial); - } - - /** - * Return true if grown above the reset limit size of 1000. - */ - private boolean resetLimit() { - return map.size() > 1000; - } @Override public String toString() { - return "size:" + map.size(); + return "size:" + map.size() + " (" + weakCount + " weak)"; } private Object get(Object id) { - Object bean = (parent == null) ? null : parent.get(id); - return bean != null ? bean : map.get(id); + Object ret = map.get(id); + if (ret instanceof BeanRef) { + return ((BeanRef)ret).get(); + } else { + return ret; + } } private WithOption getWithOption(Object id) { @@ -304,24 +284,33 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { return existingValue; } // put the new value and return null indicating the put was successful - map.put(id, bean); + put(id, bean); return null; } private void put(Object id, Object b) { - map.put(id, b); + if (useReferences) { + weakCount++; + map.put(id, new BeanRef(this, id, b, queue)); + } else { + map.put(id, b); + } } private int size() { - return map.size() + initialSize; + return map.size(); } private void clear() { map.clear(); + weakCount = 0; } private void remove(Object id) { - map.remove(id); + Object ret = map.remove(id); + if (ret instanceof BeanRef) { + weakCount--; + } } private void deleted(Object id) { @@ -329,7 +318,7 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { deleteSet = new HashSet<>(); } deleteSet.add(id); - map.remove(id); + remove(id); } /** @@ -338,6 +327,10 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { void dirtyBeans(SpiBeanTypeManager manager, List list) { final SpiBeanType beanType = manager.beanType(rootType); for (Object value : map.values()) { + if (queue != null && value instanceof BeanRef) { + value = ((BeanRef) value).get(); + if (value == null) continue; + } EntityBean bean = (EntityBean) value; if (bean._ebean_getIntercept().isDirty() || beanType.isToManyDirty(bean)) { list.add(value); @@ -346,4 +339,20 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { } } + private static class BeanRef extends WeakReference { + + private final ClassContext classContext; + private final Object key; + + private BeanRef(ClassContext classContext, Object key, Object referent, ReferenceQueue q) { + super(referent, q); + this.classContext = classContext; + this.key = key; + } + + private void expunge() { + classContext.remove(key); + } + + } } diff --git a/ebean-test/src/test/java/io/ebean/text/json/JsonBeanReaderTest.java b/ebean-test/src/test/java/io/ebean/text/json/JsonBeanReaderTest.java index 6cf8f52db..a60b18d3f 100644 --- a/ebean-test/src/test/java/io/ebean/text/json/JsonBeanReaderTest.java +++ b/ebean-test/src/test/java/io/ebean/text/json/JsonBeanReaderTest.java @@ -47,7 +47,7 @@ public class JsonBeanReaderTest extends BaseTestCase { beanReader.read(); JsonParser more = getParser(); - JsonBeanReader moreReader = beanReader.forJson(more, true); + JsonBeanReader moreReader = beanReader.forJson(more); Customer customer = moreReader.read(); assertThat(customer.getId()).isEqualTo(42); diff --git a/ebean-test/src/test/java/io/ebeaninternal/server/transaction/DefaultPersistenceContextTest.java b/ebean-test/src/test/java/io/ebeaninternal/server/transaction/DefaultPersistenceContextTest.java index 4983c31e3..236d3a2fc 100644 --- a/ebean-test/src/test/java/io/ebeaninternal/server/transaction/DefaultPersistenceContextTest.java +++ b/ebean-test/src/test/java/io/ebeaninternal/server/transaction/DefaultPersistenceContextTest.java @@ -6,16 +6,13 @@ import org.junit.jupiter.api.Test; import org.tests.model.basic.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -public class DefaultPersistenceContextTest { +class DefaultPersistenceContextTest { private final Customer customer42; - private final Car car1; - public DefaultPersistenceContextTest() { + DefaultPersistenceContextTest() { customer42 = new Customer(); customer42.setId(42); car1 = new Car(); @@ -37,8 +34,7 @@ public class DefaultPersistenceContextTest { } @Test - public void put_get_withInheritance() { - + void put_get_withInheritance() { PersistenceContext pc = pc(); pc.put(root(Vehicle.class), 1, car1); @@ -47,8 +43,7 @@ public class DefaultPersistenceContextTest { } @Test - public void put_get() { - + void put_get() { PersistenceContext pc = pc(); pc.put(Customer.class, customer42.getId(), customer42); @@ -57,8 +52,7 @@ public class DefaultPersistenceContextTest { } @Test - public void putIfAbsent_when_absent() { - + void putIfAbsent_when_absent() { PersistenceContext pc = pc(); Object existing = pc.putIfAbsent(Customer.class, customer42.getId(), customer42); @@ -66,8 +60,7 @@ public class DefaultPersistenceContextTest { } @Test - public void putIfAbsent_when_notAbsent() { - + void putIfAbsent_when_notAbsent() { PersistenceContext pc = pcWith42(); Object existing = pc.putIfAbsent(Customer.class, customer42.getId(), new Customer()); @@ -75,30 +68,28 @@ public class DefaultPersistenceContextTest { } @Test - public void get_when_empty() { + void get_when_empty() { PersistenceContext pc = pc(); Object found = pc.get(Customer.class, 42); assertThat(found).isNull(); } @Test - public void get_when_there() { + void get_when_there() { PersistenceContext pc = pcWith42(); Object found = pc.get(Customer.class, 42); assertThat(found).isSameAs(customer42); } @Test - public void getWithOption_when_empty() { - + void getWithOption_when_empty() { PersistenceContext pc = pc(); PersistenceContext.WithOption withOption = pc.getWithOption(Customer.class, 42); assertThat(withOption).isNull(); } @Test - public void getWithOption_when_there() { - + void getWithOption_when_there() { PersistenceContext pc = pcWith42(); PersistenceContext.WithOption withOption = pc.getWithOption(Customer.class, 42); @@ -106,8 +97,7 @@ public class DefaultPersistenceContextTest { } @Test - public void getWithOption_when_deleted() { - + void getWithOption_when_deleted() { PersistenceContext pc = pcWith42(); pc.deleted(Customer.class, 42); @@ -117,38 +107,33 @@ public class DefaultPersistenceContextTest { } @Test - public void size_when_empty() { - + void size_when_empty() { PersistenceContext pc = pc(); assertThat(pc.size(Customer.class)).isEqualTo(0); } @Test - public void size_when_some() { - + void size_when_some() { PersistenceContext pc = pcWith42(); assertThat(pc.size(Customer.class)).isEqualTo(1); } @Test - public void clear() { - + void clear() { PersistenceContext pc = pcWith42(); pc.clear(); assertThat(pc.size(Customer.class)).isEqualTo(0); } @Test - public void clearClass() { - + void clearClass() { PersistenceContext pc = pcWith42(); pc.clear(Customer.class); assertThat(pc.size(Customer.class)).isEqualTo(0); } @Test - public void clearClassAndId() { - + void clearClassAndId() { PersistenceContext pc = pcWith42(); pc.put(Customer.class, 43, new Customer()); @@ -160,24 +145,24 @@ public class DefaultPersistenceContextTest { } @Test - public void forIterate() { + void beginIterate() { final DefaultPersistenceContext pc = pcWith42(); final Object origCustomer42 = pc.get(Customer.class, 42); // act - final PersistenceContext pcIterate = pc.forIterate(); - assertThat(pc).isNotSameAs(pcIterate); - assertThat(pcIterate.size(Customer.class)).isEqualTo(1); + pc.beginIterate(); + assertThat(pc.size(Customer.class)).isEqualTo(1); // assert same instance (bean effectively transferred to iterator persistence context - final Object customer42 = pcIterate.get(Customer.class, 42); + final Object customer42 = pc.get(Customer.class, 42); assertThat(customer42).isSameAs(origCustomer42); - final PersistenceContext.WithOption option = pcIterate.getWithOption(Customer.class, 42); + final PersistenceContext.WithOption option = pc.getWithOption(Customer.class, 42); assertThat(option.getBean()).isSameAs(origCustomer42); + pc.endIterate(); } @Test - public void forIterate_many() { + void beginIterate_many() throws InterruptedException { DefaultPersistenceContext pc = new DefaultPersistenceContext(); addCustomers(pc, 1, 100); addContacts(pc, 1, 1010); @@ -185,50 +170,26 @@ public class DefaultPersistenceContextTest { assertThat(pc.size(Contact.class)).isEqualTo(1010); // act - final PersistenceContext pcIterate = pc.forIterate(); - assertThat(pcIterate.size(Customer.class)).isEqualTo(100); - assertThat(pcIterate.size(Contact.class)).isEqualTo(1010); + pc.beginIterate(); + assertThat(pc.size(Customer.class)).isEqualTo(100); + assertThat(pc.size(Contact.class)).isEqualTo(1010); + + addCustomers(pc, 200, 100); + addContacts(pc, 2000, 1010); + + assertThat(pc.size(Customer.class)).isEqualTo(200); + assertThat(pc.size(Contact.class)).isEqualTo(2020); + pc.endIterate(); + + System.gc(); + Thread.sleep(50); // give the GC some time + + assertThat(pc.size(Customer.class)).isEqualTo(100); + assertThat(pc.size(Contact.class)).isEqualTo(1010); } @Test - public void forIterate_resetLimit_forIterateReset() { - DefaultPersistenceContext initialPc = new DefaultPersistenceContext(); - addCustomers(initialPc, 1, 100); - addContacts(initialPc, 1, 1010); - - final PersistenceContext pcIterate = initialPc.forIterate(); - assertFalse(pcIterate.resetLimit()); - - // added 900 NEW contact beans - addContacts(pcIterate, 2000, 900); - assertThat(pcIterate.size(Contact.class)).isEqualTo(1910); - assertFalse(pcIterate.resetLimit()); - - // boundary, added 1000 NEW contact beans (still false) - addContacts(pcIterate, 3000, 100); - assertFalse(pcIterate.resetLimit()); - - addContacts(pcIterate, 4000, 1); - addProducts(pcIterate, 1, 100); - // ACT - over 1000 added beans boundary for contacts so returns true - assertTrue(pcIterate.resetLimit()); - - assertThat(pcIterate.size(Contact.class)).isEqualTo(2011); - assertThat(pcIterate.size(Customer.class)).isEqualTo(100); - assertThat(pcIterate.size(Product.class)).isEqualTo(100); - - // ACT - obtain new PC forIterateReset - PersistenceContext pcReset = pcIterate.forIterateReset(); - - // keeps original customer beans as no new added beans there - assertThat(pcReset.size(Customer.class)).isEqualTo(100); // customers didn't change - // added beans to contacts and products so those where reset - assertThat(pcReset.size(Contact.class)).isEqualTo(0); - assertThat(pcReset.size(Product.class)).isEqualTo(0); - } - - @Test - public void toString_sillyTest() { + void toString_sillyTest() { DefaultPersistenceContext pc = pcWith42(); assertThat(pc.toString()).contains("org.tests.model.basic.Customer"); } @@ -248,12 +209,4 @@ public class DefaultPersistenceContextTest { pc.put(Contact.class, i, bean); } } - - private void addProducts(PersistenceContext pc, int start, int loop) { - for (int i = start; i < start + loop; i++) { - Product bean = new Product(); - bean.setId(i); - pc.put(Product.class, i, bean); - } - } } diff --git a/ebean-test/src/test/java/org/tests/basic/TestPersistenceContext.java b/ebean-test/src/test/java/org/tests/basic/TestPersistenceContext.java index ea894091a..4d79e5b89 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestPersistenceContext.java +++ b/ebean-test/src/test/java/org/tests/basic/TestPersistenceContext.java @@ -2,16 +2,23 @@ package org.tests.basic; import io.ebean.BaseTestCase; import io.ebean.DB; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebeaninternal.api.SpiPersistenceContext; +import io.ebeaninternal.api.SpiTransaction; + import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; import org.tests.model.basic.ResetBasicData; +import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestPersistenceContext extends BaseTestCase { @@ -112,6 +119,65 @@ public class TestPersistenceContext extends BaseTestCase { } }); } + } + + @Disabled // run manually + @Test + void testPcScopes() throws InterruptedException { + for (int i = 0; i < 5000; i++) { + Customer c = new Customer(); + c.setName("Customer #" + i); + DB.save(c); + Order o = new Order(); + o.setCustomer(c); + DB.save(o); + } + + try (Transaction txn = DB.beginTransaction()) { + List first100 = DB.find(Customer.class).where().le("id", 100).findList(); + assertEquals(100, first100.size()); + for (Customer c : first100) { + c.setSmallnote("one of the first 100"); + } + + Customer[] lastBean = new Customer[1]; + DB.find(Customer.class).setLazyLoadBatchSize(1).findEach(customer -> { + if (customer.getId() <= 100) { + assertEquals("one of the first 100", customer.getSmallnote()); + // nested finds + DB.find(Order.class).where().eq("customer", customer).findEach(20, consumer->{}); + } else { + assertNotEquals("one of the first 100", customer.getSmallnote()); + } + lastBean[0] = customer; + }); + + SpiPersistenceContext pc = ((SpiTransaction) txn).getPersistenceContext(); + System.out.println(pc.toString()); // Customer=size:5000 (4900 weak), Order=size:100 (100 weak) + + System.gc(); + Thread.sleep(100); + pc.get(Customer.class, 1); // trigger expungeStaleEntries + pc.get(Order.class, 1); // Checkme: Would it be a problem, that every classContext has its own queue + System.out.println(pc.toString()); // Customer=size:101 (1 weak), no orders + + first100 = DB.find(Customer.class).where().le("id", 100).findList(); + for (Customer c : first100) { + assertEquals("one of the first 100", c.getSmallnote()); + } + Customer lastBeanFromDb = DB.find(Customer.class).setId(lastBean[0].getId()).findOne(); + assertTrue(lastBeanFromDb == lastBean[0]); + + // read 200 + DB.find(Customer.class).where().le("id", 200).findList(); + System.out.println(pc.toString()); // Customer=size:201 (1 weak) + + lastBean[0] = null; + lastBeanFromDb = null; + System.gc(); + Thread.sleep(100); + System.out.println(pc.toString()); // Customer=size:200 (0 weak) + } } }