, SpiExpression, Expression
this.exprList = exprList;
}
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache
+ return false;
+ }
+
/**
* Simplify nested expressions where possible.
*
diff --git a/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java b/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java
index 2b02c4e21..670abb0ce 100644
--- a/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java
+++ b/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java
@@ -8,6 +8,7 @@ import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.api.SpiExpressionValidation;
import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.api.NaturalKeyQueryData;
import java.io.IOException;
@@ -56,6 +57,12 @@ abstract class LogicExpression implements SpiExpression {
this.expTwo = (SpiExpression) expTwo;
}
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache
+ return false;
+ }
+
@Override
public void simplify() {
// do nothing
diff --git a/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java b/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java
index 2a5a942ce..61e0d82c8 100644
--- a/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java
+++ b/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java
@@ -6,6 +6,7 @@ import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.api.SpiExpressionValidation;
import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.api.NaturalKeyQueryData;
import java.io.IOException;
@@ -23,6 +24,12 @@ class NestedPathWrapperExpression implements SpiExpression {
this.delegate = delegate;
}
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache
+ return false;
+ }
+
@Override
public void simplify() {
// do nothing
@@ -59,7 +66,7 @@ class NestedPathWrapperExpression implements SpiExpression {
@Override
public void queryPlanHash(StringBuilder builder) {
builder.append("NestedPath[");
- if (nestedPath != null){
+ if (nestedPath != null) {
builder.append("path:").append(nestedPath).append(" ");
}
delegate.queryPlanHash(builder);
diff --git a/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java b/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java
index 21507b4d3..f27ab9f97 100644
--- a/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java
+++ b/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java
@@ -2,12 +2,19 @@ package io.ebeaninternal.server.expression;
import io.ebean.event.BeanQueryRequest;
import io.ebeaninternal.api.SpiExpression;
+import io.ebeaninternal.api.NaturalKeyQueryData;
/**
* Base abstract expression that does nothing for prepareExpression().
*/
abstract class NonPrepareExpression implements SpiExpression {
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache
+ return false;
+ }
+
@Override
public void simplify() {
// do nothing
diff --git a/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java b/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java
index adc609fad..28ff54916 100644
--- a/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java
+++ b/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java
@@ -6,6 +6,7 @@ import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.api.SpiExpressionValidation;
import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.api.NaturalKeyQueryData;
import java.io.IOException;
@@ -16,6 +17,12 @@ class NoopExpression implements SpiExpression {
protected static final NoopExpression INSTANCE = new NoopExpression();
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache
+ return false;
+ }
+
@Override
public void simplify() {
// do nothing
diff --git a/src/main/java/io/ebeaninternal/server/expression/NotExpression.java b/src/main/java/io/ebeaninternal/server/expression/NotExpression.java
index 8ff526ee6..264d0d3db 100644
--- a/src/main/java/io/ebeaninternal/server/expression/NotExpression.java
+++ b/src/main/java/io/ebeaninternal/server/expression/NotExpression.java
@@ -7,6 +7,7 @@ import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.api.SpiExpressionValidation;
import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.api.NaturalKeyQueryData;
import java.io.IOException;
@@ -21,6 +22,12 @@ final class NotExpression implements SpiExpression {
this.exp = (SpiExpression) exp;
}
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache
+ return false;
+ }
+
@Override
public void simplify() {
// do nothing
diff --git a/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java b/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java
index ee88e8a89..e653873eb 100644
--- a/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java
+++ b/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java
@@ -5,6 +5,7 @@ import io.ebean.plugin.ExpressionPath;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.server.el.ElPropertyValue;
+import io.ebeaninternal.api.NaturalKeyQueryData;
import java.io.IOException;
import java.util.Arrays;
@@ -26,6 +27,15 @@ public class SimpleExpression extends AbstractValueExpression {
return null;
}
+ @Override
+ public boolean naturalKey(NaturalKeyQueryData data) {
+ // can't use naturalKey cache for NOT IN
+ if (type != Op.EQ) {
+ return false;
+ }
+ return data.matchEq(propName, bindValue);
+ }
+
@Override
public void writeDocQuery(DocQueryContext context) throws IOException {
if (type == Op.BETWEEN) {
diff --git a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java
index 0922af599..21e6c35d3 100644
--- a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java
+++ b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java
@@ -39,9 +39,7 @@ public abstract class DLoadBaseContext {
this.desc = desc;
this.queryProps = queryProps;
this.fullPath = parent.getFullPath(path);
-
- this.hitCache = !parent.isExcludeBeanCache() && desc.isBeanCaching();
-
+ this.hitCache = parent.isBeanCacheGet() && desc.isBeanCaching();
this.objectGraphNode = parent.getObjectGraphNode(path);
this.queryFetch = queryProps != null && queryProps.isQueryFetch();
diff --git a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java
index a04848703..5300979ac 100644
--- a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java
+++ b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.loadcontext;
+import io.ebean.CacheMode;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.CallStack;
import io.ebean.bean.EntityBeanIntercept;
@@ -41,7 +42,7 @@ public class DLoadContext implements LoadContext {
private final boolean asDraft;
private final Timestamp asOf;
private final Boolean readOnly;
- private final boolean excludeBeanCache;
+ private final CacheMode useBeanCache;
private final int defaultBatchSize;
private final boolean disableLazyLoading;
private final boolean disableReadAudit;
@@ -74,7 +75,7 @@ public class DLoadContext implements LoadContext {
this.persistenceContext = persistenceContext;
this.origin = initOrigin();
this.defaultBatchSize = 100;
- this.excludeBeanCache = false;
+ this.useBeanCache = CacheMode.OFF;
this.asDraft = false;
this.asOf = null;
this.readOnly = false;
@@ -107,7 +108,7 @@ public class DLoadContext implements LoadContext {
this.readOnly = query.isReadOnly();
this.disableReadAudit = query.isDisableReadAudit();
this.disableLazyLoading = query.isDisableLazyLoading();
- this.excludeBeanCache = query.isExcludeBeanCache();
+ this.useBeanCache = query.getUseBeanCache();
this.useProfiling = query.getProfilingListener() != null;
ObjectGraphNode parentNode = query.getParentNode();
@@ -158,8 +159,8 @@ public class DLoadContext implements LoadContext {
registerSecondaryNode(many, props);
}
- protected boolean isExcludeBeanCache() {
- return excludeBeanCache;
+ protected boolean isBeanCacheGet() {
+ return useBeanCache.isGet();
}
/**
diff --git a/src/main/java/io/ebeaninternal/server/persist/Binder.java b/src/main/java/io/ebeaninternal/server/persist/Binder.java
index 34a3f8f2c..9c673a5f3 100644
--- a/src/main/java/io/ebeaninternal/server/persist/Binder.java
+++ b/src/main/java/io/ebeaninternal/server/persist/Binder.java
@@ -229,9 +229,9 @@ public class Binder {
/**
* Return the SQL in clause taking into account Multi-value support.
*/
- public String getInExpression(boolean not, Object[] bindValues) {
- ScalarType> type = getScalarType(bindValues[0].getClass());
- return multiValueBind.getInExpression(not, type, bindValues.length);
+ public String getInExpression(boolean not, List bindValues) {
+ ScalarType> type = getScalarType(bindValues.get(0).getClass());
+ return multiValueBind.getInExpression(not, type, bindValues.size());
}
/**
diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java
index a327f3882..5b3758eb9 100644
--- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java
+++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java
@@ -151,7 +151,7 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine {
SpiQuery query = request.getQuery();
- if (query.isLoadBeanCache()) {
+ if (query.isBeanCachePut()) {
// load the individual beans into the bean cache
BeanDescriptor descriptor = request.getBeanDescriptor();
Collection c = result.getActualDetails();
@@ -160,6 +160,8 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine {
}
}
+ request.mergeCacheHits(result);
+
if (!result.isEmpty() && query.getUseQueryCache().isPut()) {
// load the query result into the query cache
result.setReadOnly(true);
@@ -189,7 +191,7 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine {
result = queryEngine.find(request);
}
- if (result != null && request.isUseBeanCache()) {
+ if (result != null && request.isBeanCachePut()) {
request.getBeanDescriptor().cacheBeanPut((EntityBean) result);
}
diff --git a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java
index 2f0b14715..ec52298f2 100644
--- a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java
+++ b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java
@@ -29,6 +29,7 @@ import io.ebeaninternal.api.BindParams;
import io.ebeaninternal.api.CQueryPlanKey;
import io.ebeaninternal.api.HashQuery;
import io.ebeaninternal.api.ManyWhereJoins;
+import io.ebeaninternal.api.NaturalKeyQueryData;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionList;
import io.ebeaninternal.api.SpiExpressionValidation;
@@ -193,9 +194,7 @@ public class DefaultOrmQuery implements SpiQuery {
private boolean usageProfiling = true;
- private boolean loadBeanCache;
-
- private boolean excludeBeanCache;
+ private CacheMode useBeanCache = CacheMode.AUTO;
private CacheMode useQueryCache = CacheMode.OFF;
@@ -375,6 +374,7 @@ public class DefaultOrmQuery implements SpiQuery {
@Override
public DefaultOrmQuery asDraft() {
this.temporalMode = TemporalMode.DRAFT;
+ this.useBeanCache = CacheMode.OFF;
return this;
}
@@ -641,6 +641,28 @@ public class DefaultOrmQuery implements SpiQuery {
return !distinct && !singleAttribute;
}
+ @Override
+ public NaturalKeyQueryData naturalKey() {
+
+ if (whereExpressions == null) {
+ return null;
+ }
+ String[] naturalKey = beanDescriptor.getNaturalKey();
+ if (naturalKey == null || naturalKey.length == 0) {
+ return null;
+ }
+
+ NaturalKeyQueryData data = new NaturalKeyQueryData<>(naturalKey);
+ for (SpiExpression expression : whereExpressions.getUnderlyingList()) {
+ // must be eq or in
+ if (!expression.naturalKey(data)) {
+ return null;
+ }
+ }
+
+ return data;
+ }
+
@Override
public NaturalKeyBindParam getNaturalKeyBindParam() {
NaturalKeyBindParam namedBind = null;
@@ -685,15 +707,13 @@ public class DefaultOrmQuery implements SpiQuery {
copy.m2mIncludeJoin = m2mIncludeJoin;
copy.profilingListener = profilingListener;
-// copy.query = query;
copy.rootTableAlias = rootTableAlias;
copy.distinct = distinct;
copy.sqlDistinct = sqlDistinct;
copy.timeout = timeout;
copy.mapKey = mapKey;
copy.id = id;
- copy.loadBeanCache = loadBeanCache;
- copy.excludeBeanCache = excludeBeanCache;
+ copy.useBeanCache = useBeanCache;
copy.useQueryCache = useQueryCache;
copy.readOnly = readOnly;
if (detail != null) {
@@ -833,7 +853,7 @@ public class DefaultOrmQuery implements SpiQuery {
@Override
public DefaultOrmQuery setForUpdate(boolean forUpdate) {
this.forUpdate = (forUpdate) ? ForUpdate.BASE : null;
- this.excludeBeanCache = true;
+ this.useBeanCache = CacheMode.OFF;
return this;
}
@@ -854,7 +874,7 @@ public class DefaultOrmQuery implements SpiQuery {
private DefaultOrmQuery setForUpdateWithMode(ForUpdate mode) {
this.forUpdate = mode;
- this.excludeBeanCache = true;
+ this.useBeanCache = CacheMode.OFF;
return this;
}
@@ -1083,29 +1103,40 @@ public class DefaultOrmQuery implements SpiQuery {
}
@Override
- public boolean isExcludeBeanCache() {
- // not using L2 cache for asDraft() query
- return excludeBeanCache || isAsDraft();
+ public boolean isBeanCachePut() {
+ return beanDescriptor.isBeanCaching() && useBeanCache.isPut();
}
@Override
- public boolean isUseBeanCache() {
- return !isExcludeBeanCache() && beanDescriptor.isBeanCaching();
+ public boolean isBeanCacheGet() {
+ return beanDescriptor.isBeanCaching() && useBeanCache.isGet();
}
@Override
- public CacheMode getUseQueryCache() {
- // not using L2 cache for asDraft() query
- if (isAsDraft()) {
- return CacheMode.OFF;
- } else {
- return useQueryCache;
+ public boolean isBeanCacheReload() {
+ return CacheMode.RECACHE == useBeanCache;
+ }
+
+ @Override
+ public void resetBeanCacheAutoMode() {
+ if (useBeanCache == CacheMode.AUTO) {
+ useBeanCache = CacheMode.OFF;
}
}
+ @Override
+ public CacheMode getUseBeanCache() {
+ return useBeanCache;
+ }
+
+ @Override
+ public CacheMode getUseQueryCache() {
+ return useQueryCache;
+ }
+
@Override
public DefaultOrmQuery setUseCache(boolean useCache) {
- this.excludeBeanCache = !useCache;
+ this.useBeanCache = (useCache) ? CacheMode.ON: CacheMode.OFF;
return this;
}
@@ -1115,15 +1146,9 @@ public class DefaultOrmQuery implements SpiQuery {
return this;
}
- @Override
- public boolean isLoadBeanCache() {
- // not using L2 cache for asDraft() query
- return !isAsDraft() && loadBeanCache;
- }
-
@Override
public DefaultOrmQuery setLoadBeanCache(boolean loadBeanCache) {
- this.loadBeanCache = loadBeanCache;
+ this.useBeanCache = CacheMode.RECACHE;
return this;
}
diff --git a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java
index e60bb4e73..47ee67204 100644
--- a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java
+++ b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java
@@ -85,7 +85,7 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest {
}
@Override
- public void appendInExpression(boolean not, Object[] bindValues) {
+ public void appendInExpression(boolean not, List bindValues) {
}
}
diff --git a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java
index a92ccf92a..28047d00b 100644
--- a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java
+++ b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.querydefn;
import io.ebean.BaseTestCase;
+import io.ebean.CacheMode;
import io.ebean.Ebean;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.core.OrmQueryRequest;
@@ -19,7 +20,7 @@ public class DefaultOrmQueryTest extends BaseTestCase {
DefaultOrmQuery q1 = (DefaultOrmQuery) Ebean.find(Customer.class)
.setForUpdate(true).where().eq("id", 42).query();
- assertThat(q1.isExcludeBeanCache()).isTrue();
+ assertThat(q1.getUseBeanCache()).isSameAs(CacheMode.OFF);
}
@Test
diff --git a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java
index e2d98199c..d440cc884 100644
--- a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java
+++ b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java
@@ -3,7 +3,6 @@ package org.tests.batchload;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.cache.ServerCache;
-import io.ebean.cache.ServerCacheManager;
import io.ebean.cache.ServerCacheStatistics;
import org.tests.model.basic.UUOne;
import org.ebeantest.LoggedSqlCollector;
@@ -18,6 +17,8 @@ import static org.junit.Assert.assertNotNull;
public class TestBatchLazyWithCacheHits extends BaseTestCase {
+ private ServerCache beanCache = server().getServerCacheManager().getBeanCache(UUOne.class);
+
private UUOne insert(String name) {
UUOne one = new UUOne();
one.setName("testBLWCH" + name);
@@ -28,24 +29,17 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase {
@Test
public void testOnCacheHit() {
- ArrayList inserted = new ArrayList<>();
- String[] names = "A,B,C,D,E,F,G,H,I,J".split(",");
- for (String name : names) {
- inserted.add(insert(name));
- }
+ ArrayList inserted = insertData();
- ServerCacheManager serverCacheManager = Ebean.getDefaultServer().getServerCacheManager();
- ServerCache beanCache = serverCacheManager.getBeanCache(UUOne.class);
- beanCache.clear();
- beanCache.getStatistics(true); // Reset statistics - otherwise other tests will interfere
+ clearCacheAndStatistics();
UUOne b = Ebean.find(UUOne.class, inserted.get(1).getId());
assertNotNull(b);
UUOne b2 = Ebean.find(UUOne.class, inserted.get(1).getId());
assertNotNull(b2);
- ServerCacheStatistics statistics = beanCache.getStatistics(true);
- assertEquals(statistics.getHitCount(), 1);
+
+ assertBeanCacheHits(1);
UUOne c = Ebean.find(UUOne.class)
.where().idEq(inserted.get(2).getId())
@@ -55,9 +49,9 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase {
UUOne c2 = Ebean.find(UUOne.class)
.where().idEq(inserted.get(2).getId())
.findOne();
+
assertNotNull(c2);
- statistics = beanCache.getStatistics(true);
- assertEquals(statistics.getHitCount(), 1);
+ assertBeanCacheHits(1);
LoggedSqlCollector.start();
@@ -76,13 +70,33 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase {
List sql = LoggedSqlCollector.stop();
System.out.println("sql:" + sql);
- // batch lazy loading into cache
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("from uuone t0 where t0.name like ");
platformAssertIn(sql.get(1), "from uuone t0 where t0.id");
- statistics = beanCache.getStatistics(true);
- assertThat(statistics.getSize()).isGreaterThan(3);
+ // not lazy loading into bean cache
+ int size = beanCache.getStatistics(true).getSize();
+ assertThat(size).isEqualTo(2);
+ }
+
+ private void assertBeanCacheHits(int hits) {
+ ServerCacheStatistics statistics = beanCache.getStatistics(true);
+ assertEquals(statistics.getHitCount(), hits);
+ }
+
+ private void clearCacheAndStatistics() {
+
+ beanCache.clear();
+ beanCache.getStatistics(true);
+ }
+
+ private ArrayList insertData() {
+ ArrayList inserted = new ArrayList<>();
+ String[] names = "A,B,C,D,E,F,G,H,I,J".split(",");
+ for (String name : names) {
+ inserted.add(insert(name));
+ }
+ return inserted;
}
}
diff --git a/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean.java b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean.java
new file mode 100644
index 000000000..592818113
--- /dev/null
+++ b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean.java
@@ -0,0 +1,62 @@
+package org.tests.model.basic.cache;
+
+import io.ebean.annotation.Cache;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * Cached bean for testing caching implementation.
+ */
+@Cache(naturalKey = {"store","sku"})
+@Entity
+@Table(name = "o_cached_natkey")
+public class OCachedNatKeyBean {
+
+ @Id
+ Long id;
+
+ String store;
+
+ String sku;
+
+ String description;
+
+ OCachedNatKeyBean(String store, String sku) {
+ this.store = store;
+ this.sku = sku;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getStore() {
+ return store;
+ }
+
+ public void setStore(String store) {
+ this.store = store;
+ }
+
+ public String getSku() {
+ return sku;
+ }
+
+ public void setSku(String sku) {
+ this.sku = sku;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean3.java b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean3.java
new file mode 100644
index 000000000..92cbde2ec
--- /dev/null
+++ b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean3.java
@@ -0,0 +1,65 @@
+package org.tests.model.basic.cache;
+
+import io.ebean.annotation.Cache;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * Cached bean for compound natural key.
+ */
+@Cache(naturalKey = {"store","code","sku"})
+@Entity
+@Table(name = "o_cached_natkey3")
+public class OCachedNatKeyBean3 {
+
+ @Id
+ Long id;
+
+ String store;
+
+ int code;
+
+ String sku;
+
+ String description;
+
+ OCachedNatKeyBean3(String store, int code, String sku) {
+ this.store = store;
+ this.code = code;
+ this.sku = sku;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getStore() {
+ return store;
+ }
+
+ public void setStore(String store) {
+ this.store = store;
+ }
+
+ public String getSku() {
+ return sku;
+ }
+
+ public void setSku(String sku) {
+ this.sku = sku;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java
new file mode 100644
index 000000000..45dab5f70
--- /dev/null
+++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java
@@ -0,0 +1,238 @@
+package org.tests.model.basic.cache;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebean.cache.ServerCache;
+import io.ebean.cache.ServerCacheManager;
+import io.ebean.cache.ServerCacheStatistics;
+import org.ebeantest.LoggedSqlCollector;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestCacheViaComplexNaturalKey extends BaseTestCase {
+
+ private ServerCache beanCache = cacheManager().getBeanCache(OCachedNatKeyBean.class);
+ private ServerCache natKeyCache = cacheManager().getNaturalKeyCache(OCachedNatKeyBean.class);
+
+ private static boolean loadOnce;
+
+ private ServerCacheManager cacheManager() {
+ return server().getServerCacheManager();
+ }
+
+ private static synchronized void insertSome() {
+ if (!loadOnce) {
+ Ebean.find(OCachedNatKeyBean.class).delete();
+
+ List stores = new ArrayList<>(Arrays.asList("abc", "def"));
+ for (String store : stores) {
+ List skus = new ArrayList<>(Arrays.asList("1", "2", "3", "4"));
+ for (String sku : skus) {
+ saveBean(store, sku);
+ }
+ }
+ loadOnce = true;
+ }
+ }
+
+ private static void saveBean(String abc, String sku) {
+ Ebean.save(new OCachedNatKeyBean(abc, sku));
+ }
+
+ private void clearCacheAndStatistics() {
+ Ebean.getServerCacheManager().clearAll();
+ clearStatistics();
+ }
+
+ private void clearStatistics() {
+ beanCache.getStatistics(true);
+ natKeyCache.getStatistics(true);
+ }
+
+ private void assertNaturalKeyHitMiss(int expectedHit, int expectedMiss) {
+ ServerCacheStatistics stats = natKeyCache.getStatistics(true);
+ assertHitMiss(expectedHit, expectedMiss, stats);
+ }
+
+ private void assertBeanCacheHitMiss(int expectedHit, int expectedMiss) {
+ ServerCacheStatistics stats = beanCache.getStatistics(true);
+ assertHitMiss(expectedHit, expectedMiss, stats);
+ }
+
+ private void assertHitMiss(int expectedHit, int expectedMiss, ServerCacheStatistics stats) {
+ assertThat(stats.getHitCount()).isEqualTo(expectedHit);
+ assertThat(stats.getMissCount()).isEqualTo(expectedMiss);
+ }
+
+ private void setup() {
+ insertSome();
+ clearCacheAndStatistics();
+ }
+
+ private void loadSomeIntoCache() {
+
+ Ebean.find(OCachedNatKeyBean.class)
+ .setLoadBeanCache(true)
+ .where().le("sku", "2")
+ .findList();
+
+ clearStatistics();
+ }
+
+ @Test
+ public void findList_inClause_someHits() {
+
+ setup();
+ loadSomeIntoCache();
+
+ String storeId = "abc";
+ List skus = new ArrayList<>(Arrays.asList("1", "2", "3"));
+
+ LoggedSqlCollector.start();
+
+ List list = Ebean.find(OCachedNatKeyBean.class)
+ .where()
+ .eq("store", storeId)
+ .in("sku", skus)
+ .setUseCache(true)
+ .orderBy("sku desc")
+ .findList();
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(1);
+ if (isH2()) {
+ // in clause with only 1 bind param - (sku=3 ... we got hits on sku 1 and 2)
+ assertThat(sql.get(0)).contains("from o_cached_natkey t0 where t0.store = ? and t0.sku in (? ) order by t0.sku desc; --bind(abc,Array[1]={3})");
+ }
+
+ assertThat(list).hasSize(3);
+
+ assertNaturalKeyHitMiss(2, 1);
+ assertBeanCacheHitMiss(2, 0);
+ }
+
+ @Test
+ public void findList_inClause_allHits() {
+
+ setup();
+ loadSomeIntoCache();
+
+ String storeId = "abc";
+ List skus = new ArrayList<>(Arrays.asList("1", "2"));
+
+ LoggedSqlCollector.start();
+
+ List list = Ebean.find(OCachedNatKeyBean.class)
+ .where()
+ .eq("store", storeId)
+ .in("sku", skus)
+ .setUseCache(true)
+ .orderBy("sku desc")
+ .findList();
+
+ List sql = LoggedSqlCollector.stop();
+
+ // no SQL - all beans from cache
+ assertThat(sql).isEmpty();
+
+ assertThat(list).hasSize(2);
+ assertNaturalKeyHitMiss(2, 0);
+ assertBeanCacheHitMiss(2, 0);
+ }
+
+ @Test
+ public void findList_inClause_noHits() {
+
+ setup();
+ loadSomeIntoCache();
+
+ String storeId = "abc";
+ List skus = new ArrayList<>(Arrays.asList("3", "4"));
+
+ LoggedSqlCollector.start();
+
+ List list = Ebean.find(OCachedNatKeyBean.class)
+ .where()
+ .eq("store", storeId)
+ .in("sku", skus)
+ .setUseCache(true)
+ .orderBy("sku desc")
+ .findList();
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(1);
+ if (isH2()) {
+ // in clause with 2 bind params as we got not hits on the cache
+ assertThat(sql.get(0)).contains("from o_cached_natkey t0 where t0.store = ? and t0.sku in (?, ? ) order by t0.sku desc; --bind(abc,Array[2]={3,4})");
+ }
+
+ assertThat(list).hasSize(2);
+ assertNaturalKeyHitMiss(0, 2);
+ assertBeanCacheHitMiss(0, 0);
+ }
+
+ @Test
+ public void hit() {
+
+ setup();
+ loadSomeIntoCache();
+
+ // simple case - no IN clause
+ OCachedNatKeyBean hit = Ebean.find(OCachedNatKeyBean.class)
+ .where()
+ .eq("store", "abc")
+ .eq("sku", "2")
+ .findOne();
+
+ assertThat(hit).isNotNull();
+
+ assertNaturalKeyHitMiss(1, 0);
+ assertBeanCacheHitMiss(1, 0);
+ }
+
+ @Test
+ public void miss() {
+
+ setup();
+
+ OCachedNatKeyBean miss = Ebean.find(OCachedNatKeyBean.class)
+ .where()
+ .eq("store", "def")
+ .eq("sku", "4")
+ .findOne();
+
+ assertThat(miss).isNotNull();
+
+ // miss on natural key
+ assertNaturalKeyHitMiss(0, 1);
+
+ // no activity against bean cache
+ assertBeanCacheHitMiss(0, 0);
+ }
+
+
+ @Test
+ public void explicitUseCacheFalse_expect_noUseOfCache() {
+
+ setup();
+
+ OCachedNatKeyBean miss = Ebean.find(OCachedNatKeyBean.class)
+ .where()
+ .eq("store", "def")
+ .eq("sku", "4")
+ .setUseCache(false) // explicitly not use cache
+ .findOne();
+
+ assertThat(miss).isNotNull();
+
+ // no activity against either cache
+ assertNaturalKeyHitMiss(0, 0);
+ assertBeanCacheHitMiss(0, 0);
+ }
+
+}
diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java
new file mode 100644
index 000000000..26919f861
--- /dev/null
+++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java
@@ -0,0 +1,250 @@
+package org.tests.model.basic.cache;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebean.cache.ServerCache;
+import io.ebean.cache.ServerCacheManager;
+import io.ebean.cache.ServerCacheStatistics;
+import org.ebeantest.LoggedSqlCollector;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestCacheViaComplexNaturalKey3 extends BaseTestCase {
+
+ private ServerCache beanCache = cacheManager().getBeanCache(OCachedNatKeyBean3.class);
+ private ServerCache natKeyCache = cacheManager().getNaturalKeyCache(OCachedNatKeyBean3.class);
+
+ private static boolean loadOnce;
+
+ private ServerCacheManager cacheManager() {
+ return server().getServerCacheManager();
+ }
+
+ private static synchronized void insertSome() {
+ if (!loadOnce) {
+ Ebean.find(OCachedNatKeyBean3.class).delete();
+
+ List stores = new ArrayList<>(Arrays.asList("abc", "def"));
+ for (String store : stores) {
+ List skus = new ArrayList<>(Arrays.asList("1", "2", "3"));
+ for (String sku : skus) {
+ int[] codes = {1000,1001,1002,1003,1004};
+ for (int code : codes) {
+ saveBean(store, code, sku);
+ }
+ }
+ }
+ loadOnce = true;
+ }
+ }
+
+ private static void saveBean(String store, int code, String sku) {
+ Ebean.save(new OCachedNatKeyBean3(store, code, sku));
+ }
+
+ private void clearCacheAndStatistics() {
+ Ebean.getServerCacheManager().clearAll();
+ clearStatistics();
+ }
+
+ private void clearStatistics() {
+ beanCache.getStatistics(true);
+ natKeyCache.getStatistics(true);
+ }
+
+ private void assertNaturalKeyHitMiss(int expectedHit, int expectedMiss) {
+ ServerCacheStatistics stats = natKeyCache.getStatistics(true);
+ assertHitMiss(expectedHit, expectedMiss, stats);
+ }
+
+ private void assertBeanCacheHitMiss(int expectedHit, int expectedMiss) {
+ ServerCacheStatistics stats = beanCache.getStatistics(true);
+ assertHitMiss(expectedHit, expectedMiss, stats);
+ }
+
+ private void assertHitMiss(int expectedHit, int expectedMiss, ServerCacheStatistics stats) {
+ assertThat(stats.getHitCount()).isEqualTo(expectedHit);
+ assertThat(stats.getMissCount()).isEqualTo(expectedMiss);
+ }
+
+ private void setup() {
+ insertSome();
+ clearCacheAndStatistics();
+ }
+
+ private void loadSomeIntoCache() {
+
+ Ebean.find(OCachedNatKeyBean3.class)
+ .setLoadBeanCache(true)
+ .where()
+ .ge("sku", "2")
+ .eq("store", "def")
+ .ge("code", 1001)
+ .findList();
+
+ clearStatistics();
+ }
+
+ @Test
+ public void findList_inClause_someHits() {
+
+ setup();
+ loadSomeIntoCache();
+
+
+ List codes = new ArrayList<>(Arrays.asList(1001, 1000, 1002, 1003));
+
+ LoggedSqlCollector.start();
+
+ List list = Ebean.find(OCachedNatKeyBean3.class)
+ .where()
+ .eq("store", "def")
+ .in("code", codes)
+ .eq("sku", "2")
+ .setUseCache(true)
+ .order().desc("sku")
+ .order().asc("code")
+ .findList();
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(1);
+ if (isH2()) {
+ // in clause with only 1 bind param - (sku=3 ... we got hits on sku 1 and 2)
+ assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and t0.code in (? ) and t0.sku = ? order by t0.sku desc, t0.code; --bind(def,Array[1]={1000},2)");
+ }
+
+ assertThat(list).hasSize(4);
+
+ assertNaturalKeyHitMiss(3, 1);
+ assertBeanCacheHitMiss(3, 0);
+ }
+
+ @Test
+ public void findList_inClause_allHits() {
+
+ setup();
+ loadSomeIntoCache();
+
+ List skus = new ArrayList<>(Arrays.asList("2", "3"));
+
+ LoggedSqlCollector.start();
+
+ List list = Ebean.find(OCachedNatKeyBean3.class)
+ .where()
+ .eq("store", "def")
+ .in("sku", skus)
+ .eq("code", 1001)
+ .setUseCache(true)
+ .orderBy("sku desc")
+ .findList();
+
+ List sql = LoggedSqlCollector.stop();
+
+ // no SQL - all beans from cache
+ assertThat(sql).isEmpty();
+
+ assertThat(list).hasSize(2);
+ assertNaturalKeyHitMiss(2, 0);
+ assertBeanCacheHitMiss(2, 0);
+ }
+
+ @Test
+ public void findList_inClause_noHits() {
+
+ setup();
+ loadSomeIntoCache();
+
+ String storeId = "abc";
+ List skus = new ArrayList<>(Arrays.asList("3", "2", "4"));
+
+ LoggedSqlCollector.start();
+
+ List list = Ebean.find(OCachedNatKeyBean3.class)
+ .where()
+ .eq("store", storeId)
+ .in("sku", skus)
+ .eq("code", 1001)
+ .setUseCache(true)
+ .orderBy("sku desc")
+ .findList();
+
+ List sql = LoggedSqlCollector.stop();
+ assertThat(sql).hasSize(1);
+ if (isH2()) {
+ // in clause with 2 bind params as we got not hits on the cache
+ assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and t0.sku in (?, ?, ? ) and t0.code = ? order by t0.sku desc; --bind(abc,Array[3]={3,2,4},1001)");
+ }
+
+ assertThat(list).hasSize(2);
+ assertNaturalKeyHitMiss(0, 3);
+ assertBeanCacheHitMiss(0, 0);
+ }
+
+
+ @Test
+ public void hit() {
+
+ setup();
+ loadSomeIntoCache();
+
+ // simple case - no IN clause
+ OCachedNatKeyBean3 hit = Ebean.find(OCachedNatKeyBean3.class)
+ .where()
+ .eq("store", "def")
+ .eq("sku", "2")
+ .eq("code", 1002)
+ .findOne();
+
+ assertThat(hit).isNotNull();
+
+ assertNaturalKeyHitMiss(1, 0);
+ assertBeanCacheHitMiss(1, 0);
+ }
+
+ @Test
+ public void miss() {
+
+ setup();
+
+ OCachedNatKeyBean3 miss = Ebean.find(OCachedNatKeyBean3.class)
+ .where()
+ .eq("store", "abc")
+ .eq("sku", "2")
+ .eq("code", 1000)
+ .findOne();
+
+ assertThat(miss).isNotNull();
+
+ // miss on natural key
+ assertNaturalKeyHitMiss(0, 1);
+
+ // no activity against bean cache
+ assertBeanCacheHitMiss(0, 0);
+ }
+
+ @Test
+ public void explicitUseCacheFalse_expect_noUseOfCache() {
+
+ setup();
+
+ OCachedNatKeyBean3 miss = Ebean.find(OCachedNatKeyBean3.class)
+ .where()
+ .eq("store", "abc")
+ .eq("sku", "2")
+ .eq("code", 1000)
+ .setUseCache(false) // explicitly not use cache
+ .findOne();
+
+ assertThat(miss).isNotNull();
+
+ // no activity against either cache
+ assertNaturalKeyHitMiss(0, 0);
+ assertBeanCacheHitMiss(0, 0);
+ }
+
+}