diff --git a/ebean-api/src/main/java/io/ebean/DInTuples.java b/ebean-api/src/main/java/io/ebean/DInTuples.java new file mode 100644 index 000000000..b244d8459 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/DInTuples.java @@ -0,0 +1,62 @@ +package io.ebean; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * + */ +final class DInTuples implements InTuples { + + private final String[] properties; + private final List entries = new ArrayList<>(); + + + DInTuples(String[] properties) { + this.properties = properties; + } + + /** + */ + @Override + public InTuples add(Object... values) { + entries.add(new DEntry(values)); + return this; + } + + /** + * Return the first property name. + */ + @Override + public String[] properties() { + return properties; + } + + /** + * Return all the value pairs. + */ + @Override + public List entries() { + return Collections.unmodifiableList(entries); + } + + /** + * A pair of 2 value objects. + *

+ * Used to support inPairs() expression. + */ + static final class DEntry implements InTuples.Entry { + + private final Object[] vals; + + DEntry(Object[] vals) { + this.vals = vals; + } + + @Override + public Object[] values() { + return vals; + } + } +} diff --git a/ebean-api/src/main/java/io/ebean/ExpressionFactory.java b/ebean-api/src/main/java/io/ebean/ExpressionFactory.java index e81f81636..1b67c6cc2 100644 --- a/ebean-api/src/main/java/io/ebean/ExpressionFactory.java +++ b/ebean-api/src/main/java/io/ebean/ExpressionFactory.java @@ -369,6 +369,11 @@ public interface ExpressionFactory { */ Expression inPairs(Pairs pairs); + /** + * In expression using multiple columns. + */ + Expression inTuples(InTuples pairs); + /** * In - property has a value in the array of values. */ diff --git a/ebean-api/src/main/java/io/ebean/ExpressionList.java b/ebean-api/src/main/java/io/ebean/ExpressionList.java index 05b9084e7..c3b272da7 100644 --- a/ebean-api/src/main/java/io/ebean/ExpressionList.java +++ b/ebean-api/src/main/java/io/ebean/ExpressionList.java @@ -1131,6 +1131,11 @@ public interface ExpressionList { */ ExpressionList inPairs(Pairs pairs); + /** + * In expression using multiple columns. + */ + ExpressionList inTuples(InTuples pairs); + /** * EXISTS a raw SQL SubQuery. * diff --git a/ebean-api/src/main/java/io/ebean/InTuples.java b/ebean-api/src/main/java/io/ebean/InTuples.java new file mode 100644 index 000000000..2ee158fcb --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/InTuples.java @@ -0,0 +1,56 @@ +package io.ebean; + +import java.util.List; + +/** + * IN expression using multiple columns. + *

+ * Produces SQL expression in the form of (A,B,C) IN ((a0,b0,c0), (a1,b1,c1), ... ) + * where A,B,C are the properties in the tuples. + */ +public interface InTuples { + + /** + * Create given the properties in the tuples. + */ + static InTuples of(String... properties) { + return new DInTuples(properties); + } + + /** + * Create given the properties in the tuples. + */ + static InTuples of(Query.Property... properties) { + String[] props = new String[properties.length]; + for (int i = 0; i < properties.length; i++) { + props[i] = properties[i].toString(); + } + return new DInTuples(props); + } + + /** + * Add a tuple entry. + */ + InTuples add(Object... values); + + /** + * Return the properties of the tuples. + */ + String[] properties(); + + /** + * Return all the tuple entries. + */ + List entries(); + + /** + * A tuple entry. + */ + interface Entry { + + /** + * Return all the values for this entry. + */ + Object[] values(); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java index 1ad8a9014..6b9900ba3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -471,6 +471,11 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { return new InPairsExpression(pairs, false); } + @Override + public Expression inTuples(InTuples pairs) { + return new InTuplesExpression(pairs, false); + } + /** * In - property has a value in the array of values. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index 83419a22f..5a7c47351 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -902,6 +902,10 @@ public class DefaultExpressionList implements SpiExpressionList { return add(expr.inPairs(pairs)); } + @Override + public ExpressionList inTuples(InTuples pairs) { + return add(expr.inTuples(pairs)); + } @Override public ExpressionList exists(String sqlSubQuery, Object... bindValues) { 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 new file mode 100644 index 000000000..e8f10fe22 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/InTuplesExpression.java @@ -0,0 +1,112 @@ +package io.ebeaninternal.server.expression; + +import io.ebean.InTuples; +import io.ebeaninternal.api.BindValuesKey; +import io.ebeaninternal.api.NaturalKeyQueryData; +import io.ebeaninternal.api.SpiExpression; +import io.ebeaninternal.api.SpiExpressionRequest; + +import java.util.List; + + +final class InTuplesExpression extends AbstractExpression { + + private final boolean not; + private final String[] properties; + private final List entries; + + InTuplesExpression(InTuples pairs, boolean not) { + super(pairs.properties()[0]); + this.properties = pairs.properties(); + // the entries might be modified on cache hit. + this.entries = pairs.entries(); + this.not = not; + } + + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + return false; + } + + @Override + public void writeDocQuery(DocQueryContext context) { + throw new RuntimeException("Not supported with document query"); + } + + @Override + public void addBindValues(SpiExpressionRequest request) { + for (InTuples.Entry entry : entries) { + for (Object val : entry.values()) { + request.addBindValue(val); + } + } + } + + @Override + public void addSql(SpiExpressionRequest request) { + if (entries.isEmpty()) { + request.append(not ? SQL_TRUE : SQL_FALSE); + return; + } + + request.append("("); + for (int i = 0; i < properties.length; i++) { + if (i > 0) { + request.append(","); + } + request.property(properties[i]); + } + request.append(") in ("); + final String eb = entryBinding(); + for (int i = 0; i < entries.size(); i++) { + if (i > 0) { + request.append(","); + } + request.append(eb); + } + request.append(")"); + } + + private String entryBinding() { + StringBuilder sb = new StringBuilder(); + sb.append('('); + for (int i = 0; i < properties.length; i++) { + if (i > 0) { + sb.append(','); + } + sb.append('?'); + } + return sb.append(')').toString(); + } + + /** + * Based on the number of values in the in clause. + */ + @Override + public void queryPlanHash(StringBuilder builder) { + if (not) { + builder.append("Not"); + } + builder.append("InTuple["); + for (String property : properties) { + builder.append(property).append("-"); + } + builder.append(entries.size()).append("]"); + } + + @Override + public void queryBindKey(BindValuesKey key) { + key.add(entries.size()); + for (InTuples.Entry entry : entries) { + for (Object val : entry.values()) { + key.add(val); + } + } + } + + @Override + public boolean isSameByBind(SpiExpression other) { + InTuplesExpression that = (InTuplesExpression) other; + return this.entries.size() == that.entries.size() && entries.equals(that.entries); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index 9bed90231..7c93917da 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -710,6 +710,11 @@ final class JunctionExpression implements SpiJunction, SpiExpression, Expr return exprList.inPairs(pairs); } + @Override + public ExpressionList inTuples(InTuples pairs) { + return exprList.inTuples(pairs); + } + @Override public ExpressionList in(String propertyName, Collection values) { return exprList.in(propertyName, values); diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java index 4e13d8238..84be587c8 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -1219,6 +1219,14 @@ public abstract class TQRootBean { return root; } + /** + * In expression using multiple columns. + */ + public R inTuples(InTuples inTuples) { + peekExprList().inTuples(inTuples); + return root; + } + /** * Marker that can be used to indicate that the order by clause is defined after this. *

diff --git a/ebean-querybean/src/test/java/org/querytest/QContactTest.java b/ebean-querybean/src/test/java/org/querytest/QContactTest.java index e2cec545e..78dd89b57 100644 --- a/ebean-querybean/src/test/java/org/querytest/QContactTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QContactTest.java @@ -1,12 +1,35 @@ package org.querytest; +import io.ebean.InTuples; import org.example.domain.query.QContact; import org.junit.jupiter.api.Test; import java.time.ZonedDateTime; +import static org.assertj.core.api.Assertions.assertThat; + public class QContactTest { + @Test + void findInTuples() { + + var c = QContact.alias(); + + InTuples inTuples = InTuples.of(c.firstName, c.lastName) + .add("Rob", "B") + .add("Bob", "C") + .add("Mob", "D"); + + var query = new QContact() + .firstName.isNotNull() + .email.isNotNull() + .inTuples(inTuples) + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("(t0.first_name,t0.last_name) in ((?,?),(?,?),(?,?))"); + } + @Test public void test_oneToManyMap() { findThem(); diff --git a/ebean-test/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java b/ebean-test/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java index 6a97a95ea..bdf0ff741 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java +++ b/ebean-test/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java @@ -1,5 +1,6 @@ package org.tests.model.basic.cache; +import io.ebean.InTuples; import io.ebean.xtest.BaseTestCase; import io.ebean.CacheMode; import io.ebean.DB; @@ -419,6 +420,31 @@ public class TestCacheViaComplexNaturalKey3 extends BaseTestCase { } else { assertSql(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and concat(t0.sku,':',t0.code,'-foo')"); } + } + @Test + public void findList_inTuples_noCache() { + setup(); + + InTuples tuples = InTuples.of("sku", "code") + .add("2", 1000) + .add("2", 1001) + .add("3", 1000); + + LoggedSql.start(); + + List list = DB.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "def") + .inTuples(tuples) + .setUseCache(false) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSql.stop(); + + assertThat(tuples.entries()).hasSize(3); + assertThat(list).hasSize(3); + assertSql(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (sku,code) in ((?,?),(?,?),(?,?)) order by t0.sku desc;"); } }