JDOQL Typed API
JDO 3.2 introduces a way of performing queries using a JDOQLTypedQuery API, that copes with refactoring of classes/fields. The API follows the same JDOQL syntax that we have seen earlier in terms of the components of the query etc. It produces queries that are much more elegant and simpler than the equivalent “Criteria” API in JPA.
Preparation
To set up your environment to use this JDOQLTypedQuery API you need to enable annotation processing, place your JDO 3.2 provider jars in your build path, and specify a @PersistenceCapable
annotation on your classes to be used in queries (you can still provide the remaining information in XML metadata if you wish to). This annotation processor will (just before compile of your persistable classes), create a query metamodel “Q” class for each persistable class. This is similar step to what QueryDSL requires, or indeed the JPA Criteria static metamodel.
Using Maven
For example, using the JDO 3.2 RI, with Maven you need to have the following in your POM
<dependencies>
<dependency>
<groupId>org.datanucleus</groupId>
<artifactId>datanucleus-jdo-query</artifactId>
<version>5.0.9</version>
</dependency>
<dependency>
<groupId>javax.jdo</groupId>
<artifactId>jdo-api</artifactId>
<version>3.2</version>
</dependency>
...
</dependencies>
This creates the “metamodel” Q classes under target/generated-sources/annotations/. You can change this location using the configuration property generatedSourcesDirectory of the maven-compiler-plugin.
Using Eclipse
With Eclipse you need to
- Go to Java Compiler and make sure the compiler compliance level is 1.8 or above
- Go to Java Compiler → Annotation Processing and enable the project specific settings and enable annotation processing
- Go to Java Compiler → Annotation Processing → Factory Path, enable the project specific settings and then add the following jars to the list:
datanucleus-jdo-query.jar
,jdo-api.jar
This creates the “metamodel” Q classes under target/generated-sources/annotations/. You can change this location on the Java Compiler → Annotation Processing page.
Query Classes
The above preparation will mean that whenever you compile, the DataNucleus annotation processor (in datanucleus-jdo-query.jar
) will generate a query class for each model class that is annotated as persistable. So what is a query class you ask. It is simply a mechanism for providing an intuitive API to generating queries. If we have the following model class
@PersistenceCapable
public class Product
{
@PrimaryKey
long id;
String name;
double value;
...
}
then the (generated) query class for this will be
public class QProduct extends org.datanucleus.api.jdo.query.PersistableExpressionImpl<Product>
implements PersistableExpression<Product>
{
public static QProduct candidate(String name) {...}
public static QProduct candidate() {...}
public static QProduct variable(String name) {...}
public static QProduct parameter(String name) {...}
public NumericExpression<Long> id;
public StringExpression name;
public NumericExpression<Double> value;
...
}
The generated class has the name of form \Q*{className}. Also the generated class, by default, has a public field for each persistable field/property and is of a type XXXExpression. These expressions allow us to give Java like syntax when defining your queries (see below). So you access your persistable members in a query as *candidate.name for example.
As mentioned above this is the default style of query class. However you can also create it in property style, where you access your persistable members as candidate.name() for example. The benefit of this approach is that if you have 1-1, N-1 relationship fields then it only initialises the members when called, whereas in the field case above it has to initialise all in the constructor, so at static initialisation.
The JDOQL Typed query mechanism only works for classes that are annotated, and not for classes that use XML metadata. This is due to the fact that it makes use of a Java annotation processor. |
Limitations
There are some corner cases where the use of expressions and this API may require casting to allow the full range of operations for JDOQL. Some examples
- If you have a List field and call
ListExpression.get(position)
this returns anExpression
rather than a specificNumericExpression
,StringExpression
, or whatever subtype. You would need to cast the result to do subsequent calls. - If you have a Map field and call
MapExpression.get(key)
this returns anExpression
rather than a specificNumericExpression
,StringExpression
, or whatever subtype. You would need to cast the result to do subsequent calls. - If you have a Collection parameter and call
CollectionParameter.contains(fieldExpression)
then you may need to cast thefieldExpression
toExpression
since theCollectionParameter
will not have adequate java generic information for the compiler to do it automatically - If you have a Map parameter and call
MapParameter.contains(fieldExpression)
then you may need to cast thefieldExpression
toExpression
since theMapParameter
will not have adequate java generic information for the compiler to do it automatically
Filtering
Let’s provide a sample usage of this query API. We want to construct a query for all products with a value below a certain level, and where the name starts with “Wal”. So a typical query in a JDO-enabled application
pm = pmf.getPersistenceManager();
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
QProduct cand = QProduct.candidate();
List<Product> results = tq.filter(cand.value.lt(40.00).and(cand.name.startsWith("Wal")))
.executeList();
This equates to the single-string query
SELECT FROM mydomain.Product WHERE this.value < 40.0 && this.name.startsWith("Wal")
As you see, we create a parametrised query, and then make use of the query class to access the candidate, and from that make use of its fields, and the various Java methods present for the types of those fields. Note that the API is fluent, meaning you can chain calls easily.
Ordering
We want to order the results of the previous query by the product name, putting nulls first.
tq.orderBy(cand.name.asc().nullsFirst());
This query now equates to the single-string query
SELECT FROM mydomain.Product WHERE this.value < 40.0 && this.name.startsWith("Wal") ORDER BY this.name ASCENDING NULLS FIRST
If you don’t want to specify null positioning, simply omit the nullsFirst()
call. Similarly to put nulls last then call nullsLast()
.
Methods
In the above example you will have seen the use of some of the normal JDOQL methods. With the JDOQLTyped API these are available on the different types of expressions. For example, cand.name is a StringExpression
and consequently it has all of the normal String methods available, just like in JDOQL and just like in Java. Similarly if we had a class Inventory
which had a Collection of Product
, then we could use the method contains on the CollectionExpression
.
The JDOQL methods JDOHelper.getObjectId and JDOHelper.getVersion are available on
PersistableExpression
, for the object that they would be invoked on.The JDOQL methods Math.{xxx} are available on
NumericExpression
, for the numeric that they would be invoked on.
Results
Let’s take the query in the above example and return the name and value of the Products only
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
QProduct cand = QProduct.candidate();
List<Object[]> results = tq.filter(cand.value.lt(40.00).and(cand.name.startsWith("Wal"))).orderBy(cand.name.asc())
.result(false, cand.name, cand.value).executeResultList();
This equates to the single-string query
SELECT this.name,this.value FROM mydomain.Product WHERE this.value < 40.0 && this.name.startsWith("Wal") ORDER BY this.name ASCENDING
A further example using aggregates
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
Object results =
tq.result(false, QProduct.candidate().value.max(), QProduct.candidate().value.min()).executeResultUnique();
This equates to the single-string query
SELECT max(this.value), min(this.value) FROM mydomain.Product
If you wanted to assign an alias to a result component you do it like this
tq.result(false, cand.name.as("THENAME"), cand.value.as("THEVALUE"));
Parameters
It is important to note that JDOQLTypedQuery only accepts named parameters. You obtain a named parameter from the JDOQLTypedQuery, and then use it in the specification of the filter, ordering, grouping etc. Let’s take the query in the above example and specify the “Wal” in a parameter.
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
QProduct cand = QProduct.candidate();
List<Product> results =
tq.filter(cand.value.lt(40.00).and(cand.name.startsWith(tq.stringParameter("prefix"))))
.orderBy(cand.name.asc())
.setParameter("prefix", "Wal").executeList();
This equates to the single-string query
SELECT FROM mydomain.Product WHERE this.value < 40.0 && this.name.startsWith(:prefix) ORDER BY this.name ASCENDING
Variables
Let’s try to find all Inventory objects containing a Product with a particular name. This means we need to use a variable. Just like with a parameter, we obtain a variable from the Q class.
JDOQLTypedQuery<Inventory> tq = pm.newJDOQLTypedQuery(Inventory.class);
QProduct var = QProduct.variable("var");
QInventory cand = QInventory.candidate();
List<Inventory> results = tq.filter(cand.products.contains(var).and(var.name.startsWith("Wal"))).executeList();
This equates to the single-string query
SELECT FROM mydomain.Inventory WHERE this.products.contains(var) && var.name.startsWith("Wal")
If-Then-Else
Let’s make use of an IF-THEN-ELSE expression to return the products based on whether they are “domestic” or “international” (in our case its just based on the “id”)
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
QProduct cand = QProduct.candidate();
IfThenElseExpression<String> ifElseExpr = tq.ifThenElse(String.class, cand.id.lt(1000), "Domestic", "International");
tq.result(false, ifElseExpr);
List<String> results = tq.executeResultList();
This equates to the single-string query
SELECT IF (this.id < 1000) "Domestic" ELSE "International" FROM mydomain.Product
Subqueries
Let’s try to find all Products that have a value below the average of all Products. This means we need to use a subquery
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
QProduct cand = QProduct.candidate();
TypesafeSubquery<Product> tqsub = tq.subquery(Product.class, "p");
QProduct candsub = QProduct.candidate("p");
List<Product> results = tq.filter(cand.value.lt(tqsub.selectUnique(candsub.value.avg()))).executeList();
Note that where we want to refer to the candidate of the subquery, we specify the alias (“p”) explicitly. This equates to the single-string query
SELECT FROM mydomain.Product WHERE this.value < (SELECT AVG(p.value) FROM mydomain.Product p)
When you are using a subquery and want to refer to the candidate (or field thereof) of the outer query in the subquery then you would use
cand
in the above example (or a field of it as required).
Candidates
If you don’t want to query instances in the datastore but instead query a collection of candidate instances, you can do this by setting the candidates, like this
JDOQLTypedQuery<Product> tq = pm.newJDOQLTypedQuery(Product.class);
QProduct cand = QProduct.candidate();
List<Product> results = tq.filter(cand.value.lt(40.00)).setCandidates(myCandidates).executeList();