Add StdFunctions, StdExpressions, TQColumn marker interface

This commit is contained in:
Rob Bygrave
2022-11-03 11:16:30 +13:00
parent 2789d0f97c
commit 96e79248dc
6 changed files with 176 additions and 22 deletions
@@ -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);
}
}
@@ -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;
}
}
}
@@ -0,0 +1,7 @@
package io.ebean.typequery;
/**
* Marker interface for type safe properties and expressions.
*/
public interface TQColumn {
}
@@ -7,7 +7,7 @@ import io.ebean.ExpressionList;
*
* @param <R> The type of the owning root bean
*/
public class TQProperty<R> {
public class TQProperty<R> implements TQColumn {
protected final String _name;
@@ -31,6 +31,7 @@ public class TQProperty<R> {
this._name = TQPath.add(prefix, name);
}
@Override
public String toString() {
return _name;
}
@@ -233,8 +233,13 @@ public abstract class TQRootBean<T, R> {
}
/**
* 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.
* <p>
* The resulting entities with be "partially loaded" aka partial objects.
* <p>
* Alternatively we can use a {@link #select(FetchGroup)} to specify all properties
* to load on all parts of the graph.
*
* <pre>{@code
*
* // alias for the customer properties in select()
@@ -245,12 +250,12 @@ public abstract class TQRootBean<T, R> {
*
* List<Customer> 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();
*
* }</pre>
@@ -263,14 +268,25 @@ public abstract class TQRootBean<T, R> {
return root;
}
private Set<String> properties(TQProperty<R>[] properties) {
private Set<String> properties(TQColumn[] properties) {
Set<String> props = new LinkedHashSet<>();
for (TQProperty<R> 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<T, R> {
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.
*/
@@ -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<Order> 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<Order> 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 + ")";
}
}
}