From 96e79248dce01f5c3e1fb24ecc2c79b8acd291b5 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 3 Nov 2022 11:16:30 +1300 Subject: [PATCH] Add StdFunctions, StdExpressions, TQColumn marker interface --- .../io/ebean/typequery/StdExpressions.java | 35 +++++++++++ .../java/io/ebean/typequery/StdFunctions.java | 52 +++++++++++++++ .../java/io/ebean/typequery/TQColumn.java | 7 +++ .../java/io/ebean/typequery/TQProperty.java | 3 +- .../java/io/ebean/typequery/TQRootBean.java | 38 ++++++++--- .../test/java/org/querytest/QOrderTest.java | 63 ++++++++++++++----- 6 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/StdExpressions.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/StdFunctions.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TQColumn.java diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/StdExpressions.java b/ebean-querybean/src/main/java/io/ebean/typequery/StdExpressions.java new file mode 100644 index 000000000..8a0b5233c --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/StdExpressions.java @@ -0,0 +1,35 @@ +package io.ebean.typequery; + +import io.ebean.Expr; +import io.ebean.Expression; + +public class StdExpressions { + + public static Expression gt(TQColumn property, Object value) { + return Expr.gt(property.toString(), value); + } + + public static Expression like(TQColumn property, String value) { + return Expr.like(property.toString(), value); + } + + public static Expression ilike(TQColumn property, String value) { + return Expr.ilike(property.toString(), value); + } + + public static Expression startsWith(TQColumn property, String value) { + return Expr.startsWith(property.toString(), value); + } + + public static Expression istartsWith(TQColumn property, String value) { + return Expr.istartsWith(property.toString(), value); + } + + public static Expression contains(TQColumn property, String value) { + return Expr.contains(property.toString(), value); + } + + public static Expression icontains(TQColumn property, String value) { + return Expr.icontains(property.toString(), value); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/StdFunctions.java b/ebean-querybean/src/main/java/io/ebean/typequery/StdFunctions.java new file mode 100644 index 000000000..d370c967a --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/StdFunctions.java @@ -0,0 +1,52 @@ +package io.ebean.typequery; + +public final class StdFunctions { + + public static TQColumn max(TQColumn property) { + return new Standard("max(" + property + ")"); + } + + public static TQColumn sum(TQColumn property) { + return new Standard("sum(" + property + ")"); + } + + public static TQColumn concat(TQColumn property, Object... values) { + StringBuilder expression = new StringBuilder(50); + expression.append("concat(").append(property.toString()); + for (Object value : values) { + expression.append(",").append(sqlStringExpression(value)); + } + expression.append(")"); + return new Standard(expression.toString()); + } + + public static TQColumn coalesce(TQColumn property, Object value) { + StringBuilder expression = new StringBuilder(50); + expression.append("coalesce(").append(property.toString()).append(","); + expression.append(sqlStringExpression(value)); + expression.append(")"); + return new Standard(expression.toString()); + } + + private static String sqlStringExpression(Object value) { + if (value instanceof TQColumn || value instanceof Number) { + return value.toString(); + } else { + return "'" + value + "'"; + } + } + + private static class Standard implements TQColumn { + + private final String expression; + + private Standard(String expression) { + this.expression = expression; + } + + @Override + public String toString() { + return expression; + } + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQColumn.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQColumn.java new file mode 100644 index 000000000..dfe5fd9d1 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQColumn.java @@ -0,0 +1,7 @@ +package io.ebean.typequery; + +/** + * Marker interface for type safe properties and expressions. + */ +public interface TQColumn { +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java index 364452832..f5d6bfd2e 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java @@ -7,7 +7,7 @@ import io.ebean.ExpressionList; * * @param The type of the owning root bean */ -public class TQProperty { +public class TQProperty implements TQColumn { protected final String _name; @@ -31,6 +31,7 @@ public class TQProperty { this._name = TQPath.add(prefix, name); } + @Override public String toString() { return _name; } 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 5680185fb..4a98d0c0e 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -233,8 +233,13 @@ public abstract class TQRootBean { } /** - * Tune the query by specifying the properties to be loaded on the - * 'main' root level entity bean (aka partial object). + * Specify the properties to be loaded on the 'main' root level entity bean. + *

+ * The resulting entities with be "partially loaded" aka partial objects. + *

+ * Alternatively we can use a {@link #select(FetchGroup)} to specify all properties + * to load on all parts of the graph. + * *

{@code
    *
    *   // alias for the customer properties in select()
@@ -245,12 +250,12 @@ public abstract class TQRootBean {
    *
    *   List customers =
    *     new QCustomer()
-   *       // tune query
+   *       // specify the parts of the graph we want to load
    *       .select(cust.id, cust.name)
    *       .contacts.fetch(contact.firstName, contact.lastName, contact.email)
    *
    *       // predicates
-   *       .id.greaterThan(1)
+   *       .id.gt(1)
    *       .findList();
    *
    * }
@@ -263,14 +268,25 @@ public abstract class TQRootBean { return root; } - private Set properties(TQProperty[] properties) { + private Set properties(TQColumn[] properties) { Set props = new LinkedHashSet<>(); - for (TQProperty property : properties) { - props.add(property.propertyName()); + for (TQColumn property : properties) { + props.add(property.toString()); } return props; } + /** + * Specify the properties to be loaded on the 'main' root level entity bean + * also allowing for functions to be used like {@link StdFunctions#max(TQColumn)}. + * + * @param properties the list of properties to fetch + */ + public final R select(TQColumn... properties) { + ((SpiQueryFetch) query).selectProperties(properties(properties)); + return root; + } + /** * Specify a path to load including all its properties. * @@ -499,6 +515,14 @@ public abstract class TQRootBean { return root; } + /** + * Add an expression to the WHERE or HAVING clause. + */ + public R add(Expression expression) { + peekExprList().add(expression); + return root; + } + /** * Set root table alias. */ diff --git a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java index e8563d879..2d9d88e29 100644 --- a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java @@ -4,7 +4,7 @@ import io.ebean.DB; import io.ebean.FetchGroup; import io.ebean.Query; import io.ebean.test.LoggedSql; -import io.ebean.typequery.TQProperty; +import io.ebean.typequery.StdExpressions; import org.example.domain.Customer; import org.example.domain.Order; import org.example.domain.OrderDetail; @@ -18,6 +18,9 @@ import org.junit.jupiter.api.Test; import java.util.List; +import static io.ebean.typequery.StdExpressions.gt; +import static io.ebean.typequery.StdExpressions.ilike; +import static io.ebean.typequery.StdFunctions.*; import static org.assertj.core.api.Assertions.assertThat; public class QOrderTest { @@ -218,14 +221,51 @@ public class QOrderTest { } + @Test + public void stdExpression_iLikeConcatCoalesce() { + QOrder o = QOrder.alias(); + + // LOWER(CONCAT(COALESCE(a.name, ""), ":", a.description)) LIKE LOWER(:param) + Query query = new QOrder() + .select(o.id, o.status) + .add(ilike(concat(coalesce(o.customer.name, "Not-Provided"), ":", o.status), "rob")) + .query(); + + query.findList(); + String generatedSql = query.getGeneratedSql(); + assertThat(generatedSql).contains("where lower(concat(coalesce(t1.name,'Not-Provided'),':',t0.status)) like ? escape''"); + } + + @Test + public void stdExpression_gtCoalesce() { + QOrder o = QOrder.alias(); + + Query query = new QOrder() + .select(o.id, o.status) + .or() + .add(gt(coalesce(o.customer.version, 0), 42)) + .id.lt(12) + .endOr() + .query(); + + query.findList(); + + String sql = query.getGeneratedSql(); + assertThat(sql).contains(" where (coalesce(t1.version,0) > ? or t0.id < ?)"); + assertThat(sql).isEqualTo("select t0.id, t0.status from o_order t0 join be_customer t1 on t1.id = t0.customer_id where (coalesce(t1.version,0) > ? or t0.id < ?)"); + } + @Test void geSubQuery() { + var c = QCustomer.alias(); + var o = QOrder.alias(); + var subQuery = new QOrder() - .select(StdFunctions.max(QOrder.Alias.orderDate)) + .select(max(o.orderDate)) .query(); var query = new QCustomer() - .select(QCustomer.Alias.id) + .select(c.id) .registered.ge(subQuery) .query(); query.findList(); @@ -236,22 +276,22 @@ public class QOrderTest { @Test void gtSubQuery() { var subQuery = new QOrder() - .select("max(orderDate)") + .select(sum(QOrder.Alias.version)) .query(); var query = new QCustomer() .select(QCustomer.Alias.id) - .registered.gt(subQuery) + .version.gt(subQuery) .query(); query.findList(); - assertThat(query.getGeneratedSql()).contains("select t0.id from be_customer t0 where t0.registered > (select max(t0.order_date) from o_order t0)"); + assertThat(query.getGeneratedSql()).contains("select t0.id from be_customer t0 where t0.version > (select sum(t0.version) from o_order t0)"); } @Test void leSubQuery() { var subQuery = new QOrder() - .select("max(orderDate)") + .select(max(QOrder.Alias.orderDate)) .query(); var query = new QCustomer() @@ -266,7 +306,7 @@ public class QOrderTest { @Test void ltSubQuery() { var subQuery = new QOrder() - .select("max(orderDate)") + .select(max(QOrder.Alias.orderDate)) .query(); var query = new QCustomer() @@ -296,7 +336,7 @@ public class QOrderTest { @Test void neSubQuery() { var subQuery = new QOrder() - .select(StdFunctions.max(QOrder.Alias.orderDate)) + .select(max(QOrder.Alias.orderDate)) .query(); var query = new QCustomer() @@ -375,9 +415,4 @@ public class QOrderTest { order.save(); } - static class StdFunctions { - static String max(TQProperty prop) { - return "max(" + prop + ")"; - } - } }