From ef91c643ed339b72816ecdfb971abbd9511eba46 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 19 Nov 2017 16:47:32 +1300 Subject: [PATCH] #1214 - ENH: Add inPairs() expression - support L2 cache hits for complex natural key with findList() and `in pairs` expression --- src/main/java/io/ebean/ExpressionFactory.java | 5 + src/main/java/io/ebean/ExpressionList.java | 5 + src/main/java/io/ebean/Pairs.java | 220 ++++++++++++++++++ .../config/dbplatform/DatabasePlatform.java | 15 +- .../io/ebeaninternal/api/NaturalKeyEntry.java | 28 ++- .../api/NaturalKeyQueryData.java | 63 ++++- .../server/core/BaseDbExpression.java | 19 ++ ...Expression.java => BasicDbExpression.java} | 6 +- .../server/core/DbExpressionHandler.java | 5 + .../server/core/InternalConfiguration.java | 19 +- .../server/core/OracleDbExpression.java | 6 +- ...ression.java => PostgresDbExpression.java} | 6 +- ...ession.java => SqlServerDbExpression.java} | 6 +- .../expression/DefaultExpressionFactory.java | 9 + .../expression/DefaultExpressionList.java | 9 +- .../server/expression/InPairsExpression.java | 142 +++++++++++ .../server/expression/JunctionExpression.java | 8 +- .../server/persist/MultiValueWrapper.java | 5 + .../server/expression/BaseExpressionTest.java | 57 +++++ .../server/expression/InExpressionTest.java | 55 ----- .../expression/InPairsExpressionTest.java | 92 ++++++++ .../cache/TestCacheViaComplexNaturalKey3.java | 72 ++++++ 22 files changed, 765 insertions(+), 87 deletions(-) create mode 100644 src/main/java/io/ebean/Pairs.java create mode 100644 src/main/java/io/ebeaninternal/server/core/BaseDbExpression.java rename src/main/java/io/ebeaninternal/server/core/{NotSupportedDbExpression.java => BasicDbExpression.java} (85%) rename src/main/java/io/ebeaninternal/server/core/{PostgresJsonExpression.java => PostgresDbExpression.java} (92%) rename src/main/java/io/ebeaninternal/server/core/{SqlServerJsonExpression.java => SqlServerDbExpression.java} (87%) create mode 100644 src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java create mode 100644 src/test/java/io/ebeaninternal/server/expression/InPairsExpressionTest.java diff --git a/src/main/java/io/ebean/ExpressionFactory.java b/src/main/java/io/ebean/ExpressionFactory.java index c0447f6ba..a2c44cffb 100644 --- a/src/main/java/io/ebean/ExpressionFactory.java +++ b/src/main/java/io/ebean/ExpressionFactory.java @@ -246,6 +246,11 @@ public interface ExpressionFactory { */ Expression icontains(String propertyName, String value); + /** + * In expression using pairs of value objects. + */ + Expression inPairs(Pairs pairs); + /** * In - property has a value in the array of values. */ diff --git a/src/main/java/io/ebean/ExpressionList.java b/src/main/java/io/ebean/ExpressionList.java index c84c9635e..50518df80 100644 --- a/src/main/java/io/ebean/ExpressionList.java +++ b/src/main/java/io/ebean/ExpressionList.java @@ -811,6 +811,11 @@ public interface ExpressionList { */ ExpressionList icontains(String propertyName, String value); + /** + * In expression using pairs of value objects. + */ + ExpressionList inPairs(Pairs pairs); + /** * In - using a subQuery. */ diff --git a/src/main/java/io/ebean/Pairs.java b/src/main/java/io/ebean/Pairs.java new file mode 100644 index 000000000..8929e3dd1 --- /dev/null +++ b/src/main/java/io/ebean/Pairs.java @@ -0,0 +1,220 @@ +package io.ebean; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds a list of value object pairs. + *

+ * This feature is to enable use of L2 cache with complex natural keys with findList() queries in cases where the + * IN clause is not a single property but instead a pair of properties. + *

+ *

+ * These queries can have predicates that can be translated into a list of complex natural keys such that the L2 + * cache can be hit with these keys to obtain some or all of the beans from L2 cache rather than the DB. + *

+ *
{@code
+ *
+ *   // where a bean is annotated with a complex
+ *   // natural key made of several properties
+ *   @Cache(naturalKey = {"store","code","sku"})
+ *
+ *
+ *   Pairs pairs = new Pairs("sku", "code");
+ *   pairs.add("sj2", 1000);
+ *   pairs.add("sj2", 1001);
+ *   pairs.add("pf3", 1000);
+ *
+ *   List list = Ebean.find(OCachedNatKeyBean3.class)
+ *   .where()
+ *   .eq("store", "def")
+ *   .inPairs(pairs)       // IN clause with 'pairs' of values
+ *   .orderBy("sku desc")
+ *
+ *   // query expressions cover the natural key properties
+ *   // so we can choose to hit the L2 bean cache if we want
+ *   .setUseCache(true)
+ *   .findList();
+ *
+ * }
+ *

Important implementation Note

+ *

+ * When binding many pairs of values we want to be able to utilise a DB index (as this type of query usually means the + * pairs are a unique key/index or part of a unique key/index and highly selective). Currently we know we can do this + * on any DB that supports expression/formula based indexes. + * using a DB string concatenation formula + *

+ *

+ * This means, the implementation converts the list of pairs into a list of strings via concatenation and we use a + * DB concatenation formula to match. We see SQL like: + *

+ *
{@code sql
+ *
+ *   ...
+ *   where t0.store = ?  and (t0.sku||'-'||t0.code) in (?, ? )
+ *
+ *   // bind values like: "sj2-1000", "pf3-1000"
+ *
+ * }
+ *

+ * We often create a DB expression index to match the DB concat formula like: + *

+ *
{@code sql
+ *
+ *   create index ix_name on table_name ((sku || '-' || code));
+ *
+ * }
+ */ +public class Pairs { + + private final String property0; + private final String property1; + + private final List entries = new ArrayList<>(); + + /** + * Character between the values when combined via DB varchar concatenation. + */ + private String concatSeparator = "-"; + + /** + * Optional suffix added to DB varchar concatenation formula. + */ + private String concatSuffix; + + /** + * Create with 2 property names. + * + * @param property0 The property of the first value + * @param property1 The property of the second value + */ + public Pairs(String property0, String property1) { + this.property0 = property0; + this.property1 = property1; + } + + /** + * Add a pair of value objects. + *

+ * Both values are expected to be immutable with equals and hashCode implementations. + *

+ * + * @param a Value of the first property + * @param b Value of the second property + */ + public Pairs add(Object a, Object b) { + entries.add(new Entry(a, b)); + return this; + } + + /** + * Return the first property name. + */ + public String getProperty0() { + return property0; + } + + /** + * Return the second property name. + */ + public String getProperty1() { + return property1; + } + + /** + * Return all the value pairs. + */ + public List getEntries() { + return entries; + } + + /** + * Return the separator character used with DB varchar concatenation to combine the 2 values. + */ + public String getConcatSeparator() { + return concatSeparator; + } + + /** + * Set the separator character used with DB varchar concatenation to combine the 2 values. + */ + public Pairs setConcatSeparator(String concatSeparator) { + this.concatSeparator = concatSeparator; + return this; + } + + /** + * Return a suffix used with DB varchar concatenation to combine the 2 values. + */ + public String getConcatSuffix() { + return concatSuffix; + } + + /** + * Add a suffix used with DB varchar concatenation to combine the 2 values. + */ + public Pairs setConcatSuffix(String concatSuffix) { + this.concatSuffix = concatSuffix; + return this; + } + + public String toString() { + return "p0:" + property0 + " p1:" + property1 + " entries:" + entries; + } + + /** + * A pair of 2 value objects. + *

+ * Used to support inPairs() expression. + */ + public static class Entry { + + private final Object a; + private final Object b; + + /** + * Create with values for property0 and property1 respectively. + * + * @param a Value of the first property + * @param b Value of the second property + */ + public Entry(Object a, Object b) { + this.a = a; + this.b = b; + } + + public String toString() { + return "{" + a + "," + b + "}"; + } + + /** + * Return the value for the first property. + */ + public Object getA() { + return a; + } + + /** + * Return the value for the second property. + */ + public Object getB() { + return b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Entry that = (Entry) o; + return a.equals(that.a) && b.equals(that.b); + } + + @Override + public int hashCode() { + int result = a.hashCode(); + result = 92821 * result + b.hashCode(); + return result; + } + } +} diff --git a/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java index 70d8652e1..e2e1bd5ed 100644 --- a/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java @@ -61,6 +61,8 @@ public class DatabasePlatform { */ protected String closeQuote = "\""; + protected String concatOperator = "||"; + /** * When set to true all db column names and table names use quoted identifiers. */ @@ -390,8 +392,6 @@ public class DatabasePlatform { /** * Return the close quote for quoted identifiers. - * - * @return the close quote */ public String getCloseQuote() { return closeQuote; @@ -399,17 +399,20 @@ public class DatabasePlatform { /** * Return the open quote for quoted identifiers. - * - * @return the open quote */ public String getOpenQuote() { return openQuote; } + /** + * Return the DB concat operator. + */ + public String getConcatOperator() { + return concatOperator; + } + /** * Return the JDBC type used to store booleans. - * - * @return the boolean db type */ public int getBooleanDbType() { return booleanDbType; diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java b/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java index a33fa2ead..50a096d7a 100644 --- a/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java +++ b/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java @@ -1,6 +1,8 @@ package io.ebeaninternal.api; +import io.ebean.Pairs; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -18,16 +20,15 @@ public class NaturalKeyEntry { * Used when query query just has a series of EQ expressions (no IN clause). */ public NaturalKeyEntry(String[] naturalKey, List eqList) { - this(naturalKey, eqList, null, null); + load(eqList); + this.key = calculateKey(naturalKey); } /** * Create when query uses an IN clause. */ public NaturalKeyEntry(String[] naturalKey, List eqList, String inProperty, Object inValue) { - for (NaturalKeyEq eq : eqList) { - map.put(eq.property, eq.value); - } + load(eqList); if (inProperty != null) { map.put(inProperty, inValue); this.inValue = inValue; @@ -35,6 +36,23 @@ public class NaturalKeyEntry { this.key = calculateKey(naturalKey); } + /** + * Create when query uses an IN PAIRS clause. + */ + public NaturalKeyEntry(String[] naturalKey, List eqList, + String inMapProperty0, String inMapProperty1, Pairs.Entry pair) { + load(eqList); + map.put(inMapProperty0, pair.getA()); + map.put(inMapProperty1, pair.getB()); + this.inValue = pair; + this.key = calculateKey(naturalKey); + } + + private void load(List eqList) { + for (NaturalKeyEq eq : eqList) { + map.put(eq.property, eq.value); + } + } private Object calculateKey(String[] naturalKey) { @@ -51,7 +69,7 @@ public class NaturalKeyEntry { } /** - * Return the natural cache key. + * Return the natural cache key (String concatenation of values). */ public Object key() { return key; diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java b/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java index aa6ad24fc..d542404f4 100644 --- a/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java +++ b/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java @@ -1,5 +1,7 @@ package io.ebeaninternal.api; +import io.ebean.Pairs; + import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -13,10 +15,20 @@ public class NaturalKeyQueryData { private final String[] naturalKey; - private Collection inValues; + /** + * Only one of IN or IN PAIRS is allowed. + */ + private boolean hasIn; + // IN Pairs clause - only one allowed + private String inProperty0, inProperty1; + private List inPairs; + + // IN clause - only one allowed + private Collection inValues; private String inProperty; + // normal EQ expressions private List eqList; private NaturalKeySet set; @@ -37,15 +49,34 @@ public class NaturalKeyQueryData { return false; } + /** + * Match for In Pairs expression. We only allow one IN clause. + */ + public boolean matchInPairs(Pairs pairs) { + if (hasIn) { + // only 1 IN allowed (to project naturalIds) + return false; + } + if (matchProperty(pairs.getProperty0()) && matchProperty(pairs.getProperty1())) { + this.hasIn = true; + this.inProperty0 = pairs.getProperty0(); + this.inProperty1 = pairs.getProperty1(); + this.inPairs = pairs.getEntries(); + return true; + } + return false; + } + /** * Match for IN expression. We only allow one IN clause. */ public boolean matchIn(String propName, Collection sourceValues) { - if (inProperty != null) { + if (hasIn) { // only 1 IN allowed (to project naturalIds) return false; } if (matchProperty(propName)) { + this.hasIn = true; this.inProperty = propName; this.inValues = sourceValues; return true; @@ -77,16 +108,22 @@ public class NaturalKeyQueryData { } this.set = new NaturalKeySet(); - if (inValues == null) { - // only one - a findOne() - set.add(new NaturalKeyEntry(naturalKey, eqList)); - - } else { + if (inValues != null) { // a findList() with an IN clause so we project // for every IN value a natural key combination for (Object inValue : inValues) { set.add(new NaturalKeyEntry(naturalKey, eqList, inProperty, inValue)); } + } else if (inPairs != null) { + // 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 NaturalKeyEntry(naturalKey, eqList, inProperty0, inProperty1, entry)); + } + + } else { + // only one - a findOne() + set.add(new NaturalKeyEntry(naturalKey, eqList)); } return set; @@ -110,6 +147,12 @@ public class NaturalKeyQueryData { if (inProperty != null) { exprProps.add(inProperty); } + if (inProperty0 != null) { + exprProps.add(inProperty0); + } + if (inProperty1 != null) { + exprProps.add(inProperty1); + } if (eqList != null) { for (NaturalKeyEq eq : eqList) { exprProps.add(eq.property); @@ -133,6 +176,7 @@ public class NaturalKeyQueryData { private boolean expressionCount() { int defined = (inValues == null) ? 0 : 1; + defined += (inPairs == null) ? 0 : 2; defined += (eqList == null) ? 0 : eqList.size(); return defined == naturalKey.length; } @@ -161,6 +205,11 @@ public class NaturalKeyQueryData { Object naturalKey = hit.getKey(); Object inValue = set.getInValue(naturalKey); inValues.remove(inValue); + + } else if (inPairs != null) { + Object naturalKey = hit.getKey(); + Pairs.Entry inValue = (Pairs.Entry)set.getInValue(naturalKey); + inPairs.remove(inValue); } beans.add(hit.getBean()); } diff --git a/src/main/java/io/ebeaninternal/server/core/BaseDbExpression.java b/src/main/java/io/ebeaninternal/server/core/BaseDbExpression.java new file mode 100644 index 000000000..fe2c97893 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/core/BaseDbExpression.java @@ -0,0 +1,19 @@ +package io.ebeaninternal.server.core; + +/** + * Not supported JSON or ARRAY expression handler. + */ +abstract class BaseDbExpression implements DbExpressionHandler { + + private final String concatOperator; + + BaseDbExpression(String concatOperator) { + this.concatOperator = concatOperator; + } + + @Override + public String getConcatOperator() { + return concatOperator; + } + +} diff --git a/src/main/java/io/ebeaninternal/server/core/NotSupportedDbExpression.java b/src/main/java/io/ebeaninternal/server/core/BasicDbExpression.java similarity index 85% rename from src/main/java/io/ebeaninternal/server/core/NotSupportedDbExpression.java rename to src/main/java/io/ebeaninternal/server/core/BasicDbExpression.java index ef5a9fc29..ef8ad5b4a 100644 --- a/src/main/java/io/ebeaninternal/server/core/NotSupportedDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/core/BasicDbExpression.java @@ -6,7 +6,11 @@ import io.ebeaninternal.server.expression.Op; /** * Not supported JSON or ARRAY expression handler. */ -public class NotSupportedDbExpression implements DbExpressionHandler { +public class BasicDbExpression extends BaseDbExpression { + + BasicDbExpression(String concatOperator) { + super(concatOperator); + } @Override public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) { diff --git a/src/main/java/io/ebeaninternal/server/core/DbExpressionHandler.java b/src/main/java/io/ebeaninternal/server/core/DbExpressionHandler.java index 14083802d..7ce5a4074 100644 --- a/src/main/java/io/ebeaninternal/server/core/DbExpressionHandler.java +++ b/src/main/java/io/ebeaninternal/server/core/DbExpressionHandler.java @@ -8,6 +8,11 @@ import io.ebeaninternal.server.expression.Op; */ public interface DbExpressionHandler { + /** + * Return the DB concat operator (Usually SQL standard "||"). + */ + String getConcatOperator(); + /** * Write the db platform specific json expression. */ diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 9215b8883..92ca70639 100644 --- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -91,6 +91,8 @@ public class InternalConfiguration { private final BootupClasses bootupClasses; + private final DatabasePlatform databasePlatform; + private final DeployInherit deployInherit; private final TypeManager typeManager; @@ -138,8 +140,8 @@ public class InternalConfiguration { this.serverConfig = serverConfig; this.bootupClasses = bootupClasses; - DatabasePlatform databasePlatform = serverConfig.getDatabasePlatform(); - this.expressionFactory = initExpressionFactory(serverConfig, databasePlatform); + this.databasePlatform = serverConfig.getDatabasePlatform(); + this.expressionFactory = initExpressionFactory(serverConfig); this.typeManager = new DefaultTypeManager(serverConfig, bootupClasses); this.multiValueBind = createMultiValueBind(databasePlatform.getPlatform()); @@ -161,7 +163,7 @@ public class InternalConfiguration { /** * Create and return the ExpressionFactory based on configuration and database platform. */ - private ExpressionFactory initExpressionFactory(ServerConfig serverConfig, DatabasePlatform databasePlatform) { + private ExpressionFactory initExpressionFactory(ServerConfig serverConfig) { boolean nativeIlike = serverConfig.isExpressionNativeIlike() && databasePlatform.isSupportsNativeIlike(); return new DefaultExpressionFactory(serverConfig.isExpressionEqualsWithNullAsNoop(), nativeIlike); @@ -260,16 +262,17 @@ public class InternalConfiguration { * Return the JSON expression handler for the given database platform. */ private DbExpressionHandler getDbExpressionHandler(DatabasePlatform databasePlatform) { - final Platform platform = databasePlatform.getPlatform(); + Platform platform = databasePlatform.getPlatform(); + String concatOperator = databasePlatform.getConcatOperator(); switch (platform) { case POSTGRES: - return new PostgresJsonExpression(); + return new PostgresDbExpression(concatOperator); case ORACLE: - return new OracleDbExpression(); + return new OracleDbExpression(concatOperator); case SQLSERVER: - return new SqlServerJsonExpression(); + return new SqlServerDbExpression(concatOperator); default: - return new NotSupportedDbExpression(); + return new BasicDbExpression(concatOperator); } } diff --git a/src/main/java/io/ebeaninternal/server/core/OracleDbExpression.java b/src/main/java/io/ebeaninternal/server/core/OracleDbExpression.java index 5eff8280f..82480723d 100644 --- a/src/main/java/io/ebeaninternal/server/core/OracleDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/core/OracleDbExpression.java @@ -6,7 +6,11 @@ import io.ebeaninternal.server.expression.Op; /** * Oracle JSON expression handler, ARRAY expressions not supported. */ -public class OracleDbExpression implements DbExpressionHandler { +public class OracleDbExpression extends BaseDbExpression { + + OracleDbExpression(String concatOperator) { + super(concatOperator); + } @Override public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) { diff --git a/src/main/java/io/ebeaninternal/server/core/PostgresJsonExpression.java b/src/main/java/io/ebeaninternal/server/core/PostgresDbExpression.java similarity index 92% rename from src/main/java/io/ebeaninternal/server/core/PostgresJsonExpression.java rename to src/main/java/io/ebeaninternal/server/core/PostgresDbExpression.java index 2803e0172..a6f42bc68 100644 --- a/src/main/java/io/ebeaninternal/server/core/PostgresJsonExpression.java +++ b/src/main/java/io/ebeaninternal/server/core/PostgresDbExpression.java @@ -6,7 +6,11 @@ import io.ebeaninternal.server.expression.Op; /** * Postgres JSON and ARRAY expression handler */ -public class PostgresJsonExpression implements DbExpressionHandler { +public class PostgresDbExpression extends BaseDbExpression { + + PostgresDbExpression(String concatOperator) { + super(concatOperator); + } @Override public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) { diff --git a/src/main/java/io/ebeaninternal/server/core/SqlServerJsonExpression.java b/src/main/java/io/ebeaninternal/server/core/SqlServerDbExpression.java similarity index 87% rename from src/main/java/io/ebeaninternal/server/core/SqlServerJsonExpression.java rename to src/main/java/io/ebeaninternal/server/core/SqlServerDbExpression.java index a25ddea46..06a7824a1 100644 --- a/src/main/java/io/ebeaninternal/server/core/SqlServerJsonExpression.java +++ b/src/main/java/io/ebeaninternal/server/core/SqlServerDbExpression.java @@ -6,7 +6,11 @@ import io.ebeaninternal.server.expression.Op; /** * Microsoft SQL Server JSON. ARRAY expressions not supported. */ -public class SqlServerJsonExpression implements DbExpressionHandler { +public class SqlServerDbExpression extends BaseDbExpression { + + SqlServerDbExpression(String concatOperator) { + super(concatOperator); + } @Override public void json(final SpiExpressionRequest request, final String propName, diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java index ed473b201..4725d41e5 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -6,6 +6,7 @@ import io.ebean.ExpressionFactory; import io.ebean.ExpressionList; import io.ebean.Junction; import io.ebean.LikeType; +import io.ebean.Pairs; import io.ebean.Query; import io.ebean.bean.EntityBean; import io.ebean.search.Match; @@ -361,6 +362,14 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { return new LikeExpression(propertyName, value, true, LikeType.CONTAINS); } + /** + * In - property has a value in the collection of values. + */ + @Override + public Expression inPairs(Pairs pairs) { + return new InPairsExpression(pairs, false); + } + /** * In - property has a value in the array of values. */ diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index 8e2a4f895..50c477c2a 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -11,6 +11,7 @@ import io.ebean.FutureRowCount; import io.ebean.Junction; import io.ebean.OrderBy; import io.ebean.PagedList; +import io.ebean.Pairs; import io.ebean.Query; import io.ebean.QueryIterator; import io.ebean.Version; @@ -21,13 +22,13 @@ import io.ebean.search.TextCommonTerms; import io.ebean.search.TextQueryString; import io.ebean.search.TextSimple; import io.ebeaninternal.api.ManyWhereJoins; +import io.ebeaninternal.api.NaturalKeyQueryData; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionList; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiJunction; import io.ebeaninternal.server.deploy.BeanDescriptor; -import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.sql.Timestamp; @@ -789,6 +790,12 @@ public class DefaultExpressionList implements SpiExpressionList { return this; } + @Override + public ExpressionList inPairs(Pairs pairs) { + add(expr.inPairs(pairs)); + return this; + } + @Override public ExpressionList in(String propertyName, Query subQuery) { add(expr.in(propertyName, subQuery)); diff --git a/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java b/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java new file mode 100644 index 000000000..ac02ef1d6 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java @@ -0,0 +1,142 @@ +package io.ebeaninternal.server.expression; + +import io.ebean.Pairs; +import io.ebean.event.BeanQueryRequest; +import io.ebeaninternal.api.NaturalKeyQueryData; +import io.ebeaninternal.api.SpiExpression; +import io.ebeaninternal.api.SpiExpressionRequest; +import io.ebeaninternal.server.persist.MultiValueWrapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +class InPairsExpression extends AbstractExpression { + + private final boolean not; + + private final Pairs pairs; + + private final String property0, property1; + + private final List entries; + + private boolean multiValueSupported; + + private final String separator; + + private final String suffix; + + private List concatBindValues; + + InPairsExpression(Pairs pairs, boolean not) { + super(pairs.getProperty0()); + this.pairs = pairs; + this.property0 = pairs.getProperty0(); + this.property1 = pairs.getProperty1(); + this.entries = pairs.getEntries(); + this.not = not; + this.separator = pairs.getConcatSeparator(); + this.suffix = pairs.getConcatSuffix(); + } + + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + return !not && data.matchInPairs(pairs); + } + + @Override + public void prepareExpression(BeanQueryRequest request) { + + // at this stage translating pairs into varchar via DB concat + multiValueSupported = request.isMultiValueSupported(String.class); + } + + @Override + public void writeDocQuery(DocQueryContext context) throws IOException { + throw new RuntimeException("Not supported with document query"); + } + + @Override + public void addBindValues(SpiExpressionRequest request) { + + // Note at this point entries may have been removed when used with l2 caching + // ... for each l2 cache hit an entry was removed + this.concatBindValues = new ArrayList<>(entries.size()); + for (Pairs.Entry entry : entries) { + concatBindValues.add(concat(entry.getA(), entry.getB())); + } + request.addBindValue(new MultiValueWrapper(concatBindValues, String.class)); + } + + /** + * Using DB concat at this stage. Usually a DB expression index should match the concat. + */ + private String concat(Object key, Object value) { + StringBuilder sb = new StringBuilder(30); + sb.append(key); + sb.append(separator); + sb.append(value); + if (suffix != null) { + sb.append(suffix); + } + return sb.toString(); + } + + @Override + public void addSql(SpiExpressionRequest request) { + + if (entries.isEmpty()) { + String expr = not ? "1=1" : "1=0"; + request.append(expr); + return; + } + + String concat = request.getDbPlatformHandler().getConcatOperator(); + + String concatFormula = "(" + property0 + concat + "'" + separator + "'" + concat + property1; + if (suffix != null && !suffix.isEmpty()) { + concatFormula += concat + "'" + suffix + "'"; + } + concatFormula += ")"; + request.append(concatFormula); + request.appendInExpression(not, concatBindValues); + } + + /** + * Based on the number of values in the in clause. + */ + @Override + public void queryPlanHash(StringBuilder builder) { + if (not) { + builder.append("NotInPairs["); + } else { + builder.append("InPairs["); + } + builder.append(property0).append("-"); + builder.append(property1).append("-"); + builder.append(separator).append("-"); + builder.append(suffix).append(" ?"); + if (!multiValueSupported) { + // query plan specific to the number of parameters in the IN clause + builder.append(entries.size()); + } + builder.append("]"); + } + + @Override + public int queryBindHash() { + int hc = 92821; + for (Pairs.Entry entry : entries) { + hc = 92821 * hc + entry.hashCode(); + } + return hc; + } + + @Override + public boolean isSameByBind(SpiExpression other) { + + InPairsExpression that = (InPairsExpression) other; + return this.entries.size() == that.entries.size() && entries.equals(that.entries); + } +} diff --git a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index 180a154f1..a2360f9c3 100644 --- a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -10,6 +10,7 @@ import io.ebean.FutureRowCount; import io.ebean.Junction; import io.ebean.OrderBy; import io.ebean.PagedList; +import io.ebean.Pairs; import io.ebean.Query; import io.ebean.QueryIterator; import io.ebean.Version; @@ -20,12 +21,12 @@ import io.ebean.search.TextCommonTerms; import io.ebean.search.TextQueryString; import io.ebean.search.TextSimple; import io.ebeaninternal.api.ManyWhereJoins; +import io.ebeaninternal.api.NaturalKeyQueryData; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiJunction; import io.ebeaninternal.server.deploy.BeanDescriptor; -import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.sql.Timestamp; @@ -586,6 +587,11 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression return exprList.ilike(propertyName, value); } + @Override + public ExpressionList inPairs(Pairs pairs) { + return exprList.inPairs(pairs); + } + @Override public ExpressionList in(String propertyName, Collection values) { return exprList.in(propertyName, values); diff --git a/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java index 4eaee90ed..9e4d1528a 100644 --- a/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java +++ b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java @@ -10,6 +10,11 @@ public class MultiValueWrapper { private final Collection values; private Class type; + public MultiValueWrapper(Collection values, Class type) { + this.values = values; + this.type = type; + } + public MultiValueWrapper(Collection values) { this.values = values; this.type = values.iterator().next().getClass(); diff --git a/src/test/java/io/ebeaninternal/server/expression/BaseExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/BaseExpressionTest.java index e1a39f669..a9a8e939a 100644 --- a/src/test/java/io/ebeaninternal/server/expression/BaseExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/BaseExpressionTest.java @@ -1,8 +1,13 @@ package io.ebeaninternal.server.expression; import io.ebean.BaseTestCase; +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebean.event.BeanQueryRequest; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.server.deploy.BeanDescriptor; +import org.tests.model.basic.Customer; import org.tests.model.basic.Order; import static org.assertj.core.api.StrictAssertions.assertThat; @@ -29,4 +34,56 @@ public abstract class BaseExpressionTest extends BaseTestCase { protected void different(SpiExpression one, SpiExpression two){ assertThat(hash(one)).isNotEqualTo(hash(two)); } + + /** + * Request with Multi-Value support. + */ + protected InExpressionTest.TDQueryRequest multi() { + return MULTI_VALUE; + } + + /** + * Request with NO Multi-Value support. + */ + protected InExpressionTest.TDQueryRequest noMulti() { + return NO_MULTI_VALUE; + } + + + private static final TDQueryRequest MULTI_VALUE= new TDQueryRequest<>(true); + private static final TDQueryRequest NO_MULTI_VALUE = new TDQueryRequest<>(false); + + static class TDQueryRequest implements BeanQueryRequest { + + final boolean supported; + + TDQueryRequest(boolean supported) { + this.supported = supported; + } + + @Override + public EbeanServer getEbeanServer() { + return null; + } + + @Override + public Transaction getTransaction() { + return null; + } + + @Override + public Query getQuery() { + return null; + } + + @Override + public boolean isMultiValueIdSupported() { + return supported; + } + + @Override + public boolean isMultiValueSupported(Class valueType) { + return supported; + } + } } diff --git a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java index e5fc5cab2..cd49f4a34 100644 --- a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java @@ -1,11 +1,6 @@ package io.ebeaninternal.server.expression; -import io.ebean.EbeanServer; -import io.ebean.Query; -import io.ebean.Transaction; -import io.ebean.event.BeanQueryRequest; import org.junit.Test; -import org.tests.model.basic.Customer; import java.util.ArrayList; import java.util.Arrays; @@ -15,20 +10,6 @@ import static org.assertj.core.api.StrictAssertions.assertThat; public class InExpressionTest extends BaseExpressionTest { - /** - * Request with Multi-Value support. - */ - private TDQueryRequest multi() { - return MULTI_VALUE; - } - - /** - * Request with NO Multi-Value support. - */ - private TDQueryRequest noMulti() { - return NO_MULTI_VALUE; - } - @Test public void queryPlanHash_given_diffPropertyName_should_differentPlanHash() throws Exception { @@ -182,41 +163,5 @@ public class InExpressionTest extends BaseExpressionTest { } - private static final TDQueryRequest MULTI_VALUE= new TDQueryRequest<>(true); - private static final TDQueryRequest NO_MULTI_VALUE = new TDQueryRequest<>(false); - - static class TDQueryRequest implements BeanQueryRequest { - - final boolean supported; - - TDQueryRequest(boolean supported) { - this.supported = supported; - } - - @Override - public EbeanServer getEbeanServer() { - return null; - } - - @Override - public Transaction getTransaction() { - return null; - } - - @Override - public Query getQuery() { - return null; - } - - @Override - public boolean isMultiValueIdSupported() { - return supported; - } - - @Override - public boolean isMultiValueSupported(Class valueType) { - return supported; - } - } } diff --git a/src/test/java/io/ebeaninternal/server/expression/InPairsExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/InPairsExpressionTest.java new file mode 100644 index 000000000..eee6f1556 --- /dev/null +++ b/src/test/java/io/ebeaninternal/server/expression/InPairsExpressionTest.java @@ -0,0 +1,92 @@ +package io.ebeaninternal.server.expression; + +import io.ebean.Pairs; +import org.junit.Test; + +import static junit.framework.TestCase.assertFalse; +import static org.junit.Assert.assertTrue; + +public class InPairsExpressionTest extends BaseExpressionTest { + + private Pairs pairs() { + return pairs("sku", "code"); + } + + private Pairs pairs(String property0, String property1) { + Pairs pairs = new Pairs(property0, property1) + .add("2", 1000) + .add("2", 1001) + .add("3", 1000); + + return pairs; + } + + @Test + public void same_samePlan_sameBind() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs(), false); + + same(e0, e1); + + assertTrue(e0.isSameByBind(e1)); + } + + @Test + public void same_samePlan_diffBind() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs().add("4", 1000), false); + e0.prepareExpression(multi()); + e1.prepareExpression(multi()); + + // when multi() ... same as bind count not important + same(e0, e1); + assertFalse(e0.isSameByBind(e1)); + } + + @Test + public void same_noMulti_diffPlan() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs().add("4", 1000), false); + e0.prepareExpression(noMulti()); + e1.prepareExpression(noMulti()); + + // when noMulti() ... bind count different so different plan + different(e0, e1); + } + + @Test + public void diffProperty0_diff() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs("k", "code"), false); + different(e0, e1); + } + + @Test + public void diffProperty1_diff() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs("sku", "c"), false); + different(e0, e1); + } + + @Test + public void diffSeparator_diff() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs().setConcatSeparator(":"), false); + different(e0, e1); + } + + @Test + public void diffSuffix_diff() throws Exception { + + InPairsExpression e0 = new InPairsExpression(pairs(), false); + InPairsExpression e1 = new InPairsExpression(pairs().setConcatSuffix(":"), false); + different(e0, e1); + } + +} diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java index 26919f861..3aa3919fb 100644 --- a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java +++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java @@ -2,6 +2,7 @@ package org.tests.model.basic.cache; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import io.ebean.Pairs; import io.ebean.cache.ServerCache; import io.ebean.cache.ServerCacheManager; import io.ebean.cache.ServerCacheStatistics; @@ -247,4 +248,75 @@ public class TestCacheViaComplexNaturalKey3 extends BaseTestCase { assertBeanCacheHitMiss(0, 0); } + @Test + public void findList_inPairs_standardConcat() { + + setup(); + loadSomeIntoCache(); + + Pairs pairs = new Pairs("sku", "code") + .add("2", 1000) + .add("2", 1001) + .add("3", 1000); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "def") + .inPairs(pairs) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + + assertThat(list).hasSize(3); + assertNaturalKeyHitMiss(1, 2); + assertBeanCacheHitMiss(1, 0); + + if (isH2()) { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||'-'||t0.code) in (?, ? ) order by t0.sku desc; --bind(def,Array[2]={2-1000,3-1000})"); + } else { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||'-'||t0.code)"); + } + + } + + @Test + public void findList_inPairs_userConcat() { + + setup(); + loadSomeIntoCache(); + + Pairs pairs = new Pairs("sku", "code") + .setConcatSeparator(":") + .setConcatSuffix("-foo") + .add("2", 1000) + .add("2", 1001) + .add("3", 1000); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "def") + .inPairs(pairs) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + + assertThat(list).hasSize(3); + assertNaturalKeyHitMiss(1, 2); + assertBeanCacheHitMiss(1, 0); + + if (isH2()) { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||':'||t0.code||'-foo') in (?, ? ) order by t0.sku desc; --bind(def,Array[2]={2:1000-foo,3:1000-foo})"); + } else { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||':'||t0.code||'-foo')"); + } + + } }