From 474c2d559580c071df664acd64e66840c19f0d61 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Wed, 13 Jul 2016 16:40:52 +1200 Subject: [PATCH] EQL - Add / expose via EbeanServer.createQuery(type, eql) method --- src/main/java/com/avaje/ebean/Ebean.java | 75 +++++++++++-------- .../java/com/avaje/ebean/EbeanServer.java | 37 +++++++++ src/main/java/com/avaje/ebean/OrderBy.java | 3 +- .../com/avaje/ebeaninternal/api/SpiQuery.java | 8 ++ .../server/core/DefaultServer.java | 10 +++ .../server/grammer/EqlParser.java | 6 ++ .../server/querydefn/DefaultOrmQuery.java | 11 +++ .../server/querydefn/ONamedParam.java | 23 ++++++ .../com/avaje/ebean/EbeanServer_eqlTest.java | 71 ++++++++++++++++++ .../ebeaninternal/api/TDSpiEbeanServer.java | 5 ++ 10 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 src/test/java/com/avaje/ebean/EbeanServer_eqlTest.java diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java index 07d701dff..924c1c221 100644 --- a/src/main/java/com/avaje/ebean/Ebean.java +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -975,39 +975,7 @@ public final class Ebean { * {@link Query#findSet()} etc will execute against the same EbeanServer from * which is was created. *

- * - *
{@code
-   *   // Find order 2 additionally fetching the customer, details and details.product
-   *   // name.
-   * 
-   *   Order order = Ebean.find(Order.class)
-   *     .fetch("customer")
-   *     .fetch("details")
-   *     .fetch("detail.product", "name")
-   *     .setId(2)
-   *     .findUnique();
-   * 
-   *   // Find order 2 additionally fetching the customer, details and details.product
-   *   // name.
-   *   // Note: same query as above but using the query language
-   *   // Note: using a named query would be preferred practice
-   * 
-   *   String oql = "find order fetch customer fetch details fetch details.product (name) where id = :orderId ";
-   * 
-   *   Query query = Ebean.find(Order.class);
-   *   query.setQuery(oql);
-   *   query.setParameter("orderId", 2);
-   * 
-   *   Order order = query.findUnique();
-   * 
-   *   // Using a named query
-   *   Query query = Ebean.find(Order.class, "with.details");
-   *   query.setParameter("orderId", 2);
-   * 
-   *   Order order = query.findUnique();
-   * 
-   * }
- * + * * @param beanType * the class of entity to be fetched * @return A ORM Query object for this beanType @@ -1017,6 +985,47 @@ public final class Ebean { return serverMgr.getDefaultServer().createQuery(beanType); } + /** + * Parse the Ebean query language statement returning the query which can then + * be modified (add expressions, change order by clause, change maxRows, change + * fetch and select paths etc). + * + *

Example

+ * + *
{@code
+   *
+   *
+   *   // Find order additionally fetching the customer, details and details.product name.
+   *
+   *   String eql = "fetch customer fetch details fetch details.product (name) where id = :orderId ";
+   *
+   *   Query query = Ebean.createQuery(Order.class, eql);
+   *   query.setParameter("orderId", 2);
+   *
+   *   Order order = query.findUnique();
+   *
+   *   // This is the same as:
+   *
+   *   Order order = Ebean.find(Order.class)
+   *     .fetch("customer")
+   *     .fetch("details")
+   *     .fetch("detail.product", "name")
+   *     .setId(2)
+   *     .findUnique();
+   *
+   * }
+ * + * @param beanType The type of bean to fetch + * @param eql The Ebean query + * @param The type of the entity bean + * + * @return The query with expressions defined as per the parsed query statement + */ + public static Query createQuery(Class beanType, String eql) { + + return serverMgr.getDefaultServer().createQuery(beanType, eql); + } + /** * Create a query for a type of entity bean. *

diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index 9e2d4a77a..3392334a6 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -219,6 +219,43 @@ public interface EbeanServer { */ Query createQuery(Class beanType); + /** + * Parse the Ebean query language statement returning the query which can then + * be modified (add expressions, change order by clause, change maxRows, change + * fetch and select paths etc). + * + *

Example

+ * + *
{@code
+   *
+   *   // Find order additionally fetching the customer, details and details.product name.
+   *
+   *   String eql = "fetch customer fetch details fetch details.product (name) where id = :orderId ";
+   *
+   *   Query query = Ebean.createQuery(Order.class, eql);
+   *   query.setParameter("orderId", 2);
+   *
+   *   Order order = query.findUnique();
+   *
+   *   // This is the same as:
+   *
+   *   Order order = Ebean.find(Order.class)
+   *     .fetch("customer")
+   *     .fetch("details")
+   *     .fetch("detail.product", "name")
+   *     .setId(2)
+   *     .findUnique();
+   *
+   * }
+ * + * @param beanType The type of bean to fetch + * @param eql The Ebean query + * @param The type of the entity bean + * + * @return The query with expressions defined as per the parsed query statement + */ + Query createQuery(Class beanType, String eql); + /** * Create a query for a type of entity bean. *

diff --git a/src/main/java/com/avaje/ebean/OrderBy.java b/src/main/java/com/avaje/ebean/OrderBy.java index dcdb638c6..ff1b8cc1c 100644 --- a/src/main/java/com/avaje/ebean/OrderBy.java +++ b/src/main/java/com/avaje/ebean/OrderBy.java @@ -213,8 +213,9 @@ public final class OrderBy implements Serializable { * order by clause and replace. *

*/ - public void clear() { + public OrderBy clear() { list.clear(); + return this; } /** diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index dec547896..4216a17f8 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -296,6 +296,14 @@ public interface SpiQuery extends Query { */ void setLoadDescription(String loadMode, String loadDescription); + /** + * Check that the named parameters have had their values set. + */ + void checkNamedParameters(); + + /** + * Create a named parameter placeholder. + */ SpiNamedParam createNamedParameter(String parameterName); /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 30d2304fd..56c88112b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -40,6 +40,7 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; import com.avaje.ebeaninternal.server.deploy.BeanProperty; import com.avaje.ebeaninternal.server.deploy.InheritInfo; import com.avaje.ebeaninternal.server.el.ElFilter; +import com.avaje.ebeaninternal.server.grammer.EqlParser; import com.avaje.ebeaninternal.server.lib.ShutdownManager; import com.avaje.ebeaninternal.server.query.CQuery; import com.avaje.ebeaninternal.server.query.CQueryEngine; @@ -902,6 +903,14 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { throw new PersistenceException("No named query called " + namedQuery + " for bean:" + beanType.getName()); } + @Override + public Query createQuery(Class beanType, String eql) { + + DefaultOrmQuery query = createQuery(beanType); + EqlParser.parse(eql, query); + return query; + } + public DefaultOrmQuery createQuery(Class beanType) { BeanDescriptor desc = getBeanDescriptor(beanType); if (desc == null) { @@ -954,6 +963,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { SpiQuery spiQuery = (SpiQuery) query; spiQuery.setType(type); + spiQuery.checkNamedParameters(); return createQueryRequest(spiQuery, t); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlParser.java b/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlParser.java index d5e5754e1..083c49e49 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlParser.java +++ b/src/main/java/com/avaje/ebeaninternal/server/grammer/EqlParser.java @@ -7,8 +7,14 @@ import org.antlr.v4.runtime.ANTLRInputStream; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTreeWalker; +/** + * Parse EQL query language applying it to an ORM query object. + */ public class EqlParser { + /** + * Parse the raw EQL query and apply it to the supplied query. + */ public static void parse(String raw, SpiQuery query) { EQLLexer lexer = new EQLLexer(new ANTLRInputStream(raw)); diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index eb2a8deac..8e8caabc9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -29,6 +29,7 @@ import com.avaje.ebeaninternal.server.query.CancelableQuery; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -1395,6 +1396,16 @@ public class DefaultOrmQuery implements SpiQuery { this.generatedSql = generatedSql; } + @Override + public void checkNamedParameters() { + if (namedParams != null) { + Collection values = namedParams.values(); + for (ONamedParam value : values) { + value.checkValueSet(); + } + } + } + @Override public SpiNamedParam createNamedParameter(String name) { if (namedParams == null) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java index 04fa405e7..66975ab2d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/ONamedParam.java @@ -2,21 +2,44 @@ package com.avaje.ebeaninternal.server.querydefn; import com.avaje.ebeaninternal.api.SpiNamedParam; +import javax.persistence.PersistenceException; + +/** + * Named parameter used as placeholder in expressions created by EQL language parsing. + */ public class ONamedParam implements SpiNamedParam { private final String name; private Object value; + /** + * Create with the given name. + */ public ONamedParam(String name) { this.name = name; } + /** + * Set the bind value for this named parameter. + */ public void setValue(Object value) { this.value = value; } + /** + * Return the bind value for this named parameter. + */ public Object getValue() { return value; } + + /** + * Check the bind value has been set (so does not support null value). + */ + public void checkValueSet() { + if (value == null) { + throw new PersistenceException("Named parameter ["+name+"] has not had it's value set."); + } + } } diff --git a/src/test/java/com/avaje/ebean/EbeanServer_eqlTest.java b/src/test/java/com/avaje/ebean/EbeanServer_eqlTest.java new file mode 100644 index 000000000..c3479e470 --- /dev/null +++ b/src/test/java/com/avaje/ebean/EbeanServer_eqlTest.java @@ -0,0 +1,71 @@ +package com.avaje.ebean; + +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.ResetBasicData; +import org.junit.Test; + +import javax.persistence.PersistenceException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EbeanServer_eqlTest extends BaseTestCase { + + + @Test + public void basic() { + + ResetBasicData.reset(); + + Query query = server().createQuery(Customer.class, "order by id limit 10"); + query.setMaxRows(100); + query.findList(); + + assertThat(query.getGeneratedSql()).contains("order by t0.id "); + } + + @Test + public void basic_via_Ebean_defaultServer() { + + ResetBasicData.reset(); + + Query query = Ebean.createQuery(Customer.class, "order by id limit 10"); + query.findList(); + + assertThat(query.getGeneratedSql()).contains("order by t0.id "); + } + + @Test + public void orderBy_override() { + + ResetBasicData.reset(); + + Query query = server().createQuery(Customer.class, "order by id"); + + // use clear() and then effectively override the orderBy clause + query.orderBy().clear().asc("name"); + query.findList(); + + assertThat(query.getGeneratedSql()).contains("order by t0.name"); + } + + + @Test + public void namedParams() { + + ResetBasicData.reset(); + + Query query = server().createQuery(Customer.class, "where name startsWith :name order by name"); + query.setParameter("name", "Ro"); + query.findList(); + + assertThat(query.getGeneratedSql()).contains("where t0.name like ? "); + } + + @Test(expected = PersistenceException.class) + public void unboundNamedParams_expect_PersistenceException() { + + Query query = server().createQuery(Customer.class, "where name = :name"); + query.findUnique(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 1273014ad..97c93f13f 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -290,6 +290,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public Query createQuery(Class beanType, String eql) { + return null; + } + @Override public Query createQuery(Class beanType) { return null;