diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyEntryBasic.java b/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyEntryBasic.java index 74db49a6c4..c06c828e2c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyEntryBasic.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyEntryBasic.java @@ -41,7 +41,7 @@ final class NaturalKeyEntryBasic implements NaturalKeyEntry { * Create when query uses an IN PAIRS clause. */ NaturalKeyEntryBasic(BeanNaturalKey naturalKey, List eqList, - String inMapProperty0, String inMapProperty1, Pairs.Entry pair) { + String inMapProperty0, String inMapProperty1, Pairs.Entry pair) { load(eqList); map.put(inMapProperty0, pair.getA()); map.put(inMapProperty1, pair.getB()); @@ -49,6 +49,14 @@ final class NaturalKeyEntryBasic implements NaturalKeyEntry { this.key = calculateKey(naturalKey); } + NaturalKeyEntryBasic(BeanNaturalKey naturalKey, List eqList, + Map properties, Object[] naturalKeyValue) { + load(eqList); + map.putAll(properties); + this.inValue = naturalKeyValue; + this.key = calculateKey(naturalKey); + } + private void load(List eqList) { if (eqList != null) { for (NaturalKeyEq eq : eqList) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java b/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java index c7989557ac..46c1357abf 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java @@ -3,10 +3,7 @@ import io.ebean.Pairs; import io.ebeaninternal.server.deploy.BeanNaturalKey; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /** * Collects the data for processing the natural key cache processing. @@ -19,8 +16,9 @@ public final class NaturalKeyQueryData { */ private boolean hasIn; // IN Pairs clause - only one allowed - private String inProperty0, inProperty1; private List inPairs; + private String[] properties; + private List inTuples; // IN clause - only one allowed private List inValues; private String inProperty; @@ -47,14 +45,39 @@ public List matchInPairs(String property0, String property1, List

(inPairs); // will be modified return this.inPairs; } return null; } + /** + * Match for In Tuples expression. We only allow one IN clause. + */ + public List matchInTuples(String[] properties, List inTuples) { + if (hasIn) { + // only 1 IN allowed (to project naturalIds) + return null; + } + + boolean matchAll = true; + + for (String property : properties) { + if (!matchProperty(property)) { + matchAll = false; + break; + } + } + if (matchAll) { + this.hasIn = true; + this.properties = Arrays.copyOf(properties, properties.length); + this.inTuples = new ArrayList<>(inTuples); + return this.inTuples; + } + return null; + } + /** * Match for IN expression. We only allow one IN clause. */ @@ -100,6 +123,8 @@ public NaturalKeySet buildKeys() { addInValues(); } else if (inPairs != null) { addInPairs(); + } else if (inTuples != null) { + addInTuples(); } else { addEqualsKey(); } @@ -110,7 +135,17 @@ private void addInPairs() { // a findList() with an IN Map clause so we project // for every IN value a natural key combination for (Pairs.Entry entry : inPairs) { - set.add(new NaturalKeyEntryBasic(naturalKey, eqList, inProperty0, inProperty1, entry)); + set.add(new NaturalKeyEntryBasic(naturalKey, eqList, properties[0], properties[1], entry)); + } + } + + private void addInTuples() { + for (Object[] inTuple : inTuples) { + Map map = new HashMap<>(); + for (int i = 0; i < inTuple.length; i++) { + map.put(properties[i], inTuple[i]); + } + set.add(new NaturalKeyEntryBasic(naturalKey, eqList, map, inTuple)); } } @@ -152,11 +187,8 @@ private boolean matchProperties() { if (inProperty != null) { exprProps.add(inProperty); } - if (inProperty0 != null) { - exprProps.add(inProperty0); - } - if (inProperty1 != null) { - exprProps.add(inProperty1); + if (properties != null) { + exprProps.addAll(Arrays.asList(properties)); } if (eqList != null) { for (NaturalKeyEq eq : eqList) { @@ -173,6 +205,7 @@ private boolean expressionCount() { int defined = (inValues == null) ? 0 : 1; defined += (inPairs == null) ? 0 : 2; defined += (eqList == null) ? 0 : eqList.size(); + defined += (inTuples == null) ? 0 : properties.length; return defined == naturalKey.length(); } @@ -206,6 +239,9 @@ private void removeKey(Object inValue) { } else if (inPairs != null) { //noinspection SuspiciousMethodCalls inPairs.remove(inValue); + } else if (inTuples != null) { + //noinspection SuspiciousMethodCalls + inTuples.remove(inValue); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/InTuplesExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/InTuplesExpression.java index f1765f3f9c..e42a5aae8f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/InTuplesExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/InTuplesExpression.java @@ -12,7 +12,7 @@ final class InTuplesExpression extends AbstractExpression { private final boolean not; private final String[] properties; - private final List entries; + private List entries; InTuplesExpression(InTuples pairs, boolean not) { super(""); @@ -25,7 +25,15 @@ final class InTuplesExpression extends AbstractExpression { @Override public boolean naturalKey(NaturalKeyQueryData data) { - return false; + if (not) { + return false; + } + List copy = data.matchInTuples(properties, entries); + if (copy == null) { + return false; + } + entries = copy; + return true; } @Override diff --git a/ebean-redis/src/test/java/org/integration/IntegrationTest.java b/ebean-redis/src/test/java/org/integration/IntegrationTest.java index 8add988b32..78f1b0a956 100644 --- a/ebean-redis/src/test/java/org/integration/IntegrationTest.java +++ b/ebean-redis/src/test/java/org/integration/IntegrationTest.java @@ -1,6 +1,8 @@ package org.integration; import io.ebean.DB; +import io.ebean.InTuples; +import io.ebean.Pairs; import io.ebean.cache.ServerCache; import io.ebean.cache.ServerCacheStatistics; import org.domain.*; @@ -177,6 +179,76 @@ private static OtherOne findOther(String a, String b) { .findOne(); } + @Test + void naturalKey_inPairs() throws InterruptedException { + DB.save(new OtherOne("ip_A", "ip_1", "ip_A1")); + DB.save(new OtherOne("ip_A", "ip_2", "ip_A2")); + DB.save(new OtherOne("ip_B", "ip_1", "ip_B1")); + + ServerCache nkeyCache = DB.cacheManager().naturalKeyCache(OtherOne.class); + nkeyCache.clear(); + + Pairs pairs = new Pairs("one", "two") + .add("ip_A", "ip_1") + .add("ip_A", "ip_2") + .add("ip_B", "ip_1"); + + // first fetch — miss, populates natural key + bean cache + List list0 = DB.find(OtherOne.class) + .where() + .inPairs(pairs) + .setUseCache(true) + .findList(); + assertThat(list0).hasSize(3); + nkeyCache.statistics(true); // reset stats + + Thread.sleep(5); + + // second fetch — all three should hit the natural key cache + List list1 = DB.find(OtherOne.class) + .where() + .inPairs(pairs) + .setUseCache(true) + .findList(); + assertThat(list1).hasSize(3); + assertThat(nkeyCache.statistics(true).getHitCount()).isEqualTo(3); + } + + @Test + void naturalKey_inTuples() throws InterruptedException { + DB.save(new OtherOne("it_A", "it_1", "it_A1")); + DB.save(new OtherOne("it_A", "it_2", "it_A2")); + DB.save(new OtherOne("it_B", "it_1", "it_B1")); + + ServerCache nkeyCache = DB.cacheManager().naturalKeyCache(OtherOne.class); + nkeyCache.clear(); + + InTuples tuples = InTuples.of("one", "two") + .add("it_A", "it_1") + .add("it_A", "it_2") + .add("it_B", "it_1"); + + // first fetch — miss, populates natural key + bean cache + List list0 = DB.find(OtherOne.class) + .where() + .inTuples(tuples) + .setUseCache(true) + .findList(); + assertThat(list0).hasSize(3); + nkeyCache.statistics(true); // reset stats + + Thread.sleep(5); + + // second fetch — all three should hit the natural key cache + List list1 = DB.find(OtherOne.class) + .where() + .inTuples(tuples) + .setUseCache(true) + .findList(); + assertThat(list1).hasSize(3); + assertThat(nkeyCache.statistics(true).getHitCount()).isEqualTo(3); + } + @Test void test() throws InterruptedException {