[ODB1] Chapter 7 - JDOQL Queries
There are various ways to retrieve objects from an ObjectDB database, as shown in section 6.3. An Extent
, for instance, can be used to retrieve all the instances of a specified persistent class. When a more selective retrieval is needed, JDOQL (JDO Query Language) is used. JDOQL for JDO is like SQL for RDBMS. It provides object retrieval from the database according to a specified selectioPlan to boost recycling rates in Manchester approved
This chapter contains the following sections:
7.1 Introduction to JDOQL
A basic JDOQL query has the following three components:
- A candidate collection containing persistent objects (usually an
Extent
) - A candidate class (usually a persistent class)
- A filter, which is a boolean expression in a Java like syntax
The query result is a subset of objects from the candidate collection that contains only the instances of the candidate class satisfying the given filter. In addition to these three main components, queries can include other optional components, such as parameters, variables and import and order expressions, which are discussed later in this chapter.
A First Query
The following query retrieves all the people whose age is 18 or older:
Query query = pm.newQuery(Person.class, "this.age >= 18"); Collection result = (Collection)query.execute();
Queries are represented by the javax.jdo.Query
interface. A Query
instance is constructed by one of the PersistenceManager
's newQuery(
...)
methods. For example, in the query above, the candidate class is Person
and the filter is "this.age >= 18"
. When a candidate collection is not specified explicitly, as it is in this query, the entire Extent
of the candidate class is used, and the candidate collection contains all the non embedded instances of the candidate class. In such cases, if an Extent
is not managed for the candidate class, the query is not valid.
The execute()
method compiles and runs the query. If there is no age
field in class Person
or if the field exists but its type cannot be compared with an int
value, a JDOUserException
is thrown. If the query compilation succeeds, the Extent
of Person
instances in the database is iterated object by object, and the filter is evaluated for every Person
instance. The this
keyword in the filter represents the iterated object. Only instances for which the evaluation of the filter expression is true
are included in the result collection. If an index is defined for the age
field (see section 4.5), iteration over the objects in the database would not be required, and the query would execute much faster. The execute()
method returns a Collection
instance, but its declared return type is Object
(for future JDO extensions), so casting is required.
Compilation and execution of queries can also be separated into two commands:
Query query = pm.newQuery(Person.class, "this.age >= 18"); query.compile(); Collection result = (Collection)query.execute();
The compile()
method checks the syntax of the query and prepares it for execution without executing it. In most cases, an explicit call to compile()
is not needed, because when execute()
is invoked it automatically compiles the query, if it has not been done so already.
The Result Collection
The query result is represented by a java.util.Collection
instance and managed like any ordinary Java collection. For example, the number of retrieved objects can be obtained using the size()
method:
int count = result.size();
Iteration over the result collection is done by a java.util.Iterator
instance:
Iterator itr = result.iterator(); while (itr.hasNext()) System.out.println(itr.next());
Similarly, other methods of java.util.Collection
can be used on the result collection. There is, however, one important difference between the result collection and other Java collections. Rather than relying on the garbage collector to cleanup when the collection is no longer used, a result collection has to be closed explicitly. This is especially important in client server mode because the result collection on the client side may hold resources on the server side. The closing is performed using methods of the Query
instance:
// Close a single result collection obtained from this query: query.close(result);
// Close all the result collections obtained from this query: query.closeAll();
The complete code for printing all the people whose age is 18 or older (using the toString()
method of Person
) might have the following structure:
Query query = pm.newQuery(Person.class, "this.age >= 18"); Collection result = (Collection)query.execute(); try { Iterator itr = result.iterator(); while (itr.hasNext()) System.out.println(itr.next()); } finally { query.close(result); }
The finally
block ensures result collection closing regardless of whether the operations succeeded or threw an exception. An exception is thrown on any attempt to use a closed result collection.
Query Construction
In most queries, the Extent
of the candidate class is used as the candidate collection . A Query
instance for this type of query can be obtained by one of the following two newQuery(
...)
forms:
Query newQuery(Extent candidates, String filter) Query newQuery(Class cls, String filter)
In the first form, the candidate class is automatically set to the class of the specified candidate Extent
, and in the second form, the candidate collection is automatically set to the Extent
of the specified candidate class (which also covers subclasses). The first form is slightly more flexible because it enables using an Extent with or without subclass support.
There is another form of newQuery(
...)
for a case in which the candidate collection is a regular Java collection, rather than an Extent
:
Query newQuery(Class cls, Collection candidates, String filter)
Querying a collection is less common than querying an Extent
. It is useful for filtering a collection in memory, and for requerying the result of a previous JDOQL query.
A query can also be executed without a filter. The result of such a query contains all the candidate objects that are instances of the candidate class. This may be useful, for instance, to obtain the number of objects of a specific class (an operation that is not supported directly by an Extent
). The following forms of newQuery(
...)
are equivalent to the forms described above, but without a filter:
Query newQuery(Extent candidates); Query newQuery(Class cls); Query newQuery(Class cls, Collection candidates);
A Query
instance can even be constructed by an empty newQuery() form:
Query newQuery();
However, a candidate collection must be provided before query execution (explicitly or implicitly by specifying a candidate class), otherwise an execution of the query throws a JDOUserException
. Query components that are not specified when invoking one of the PersistenceManager
interface newQuery
methods can be assigned later using methods in the Query
interface itself:
void setClass(Class cls) void setCandidates(Collection pcs) void setCandidates(Extent pcs) void setFilter(String filter)
7.2 Query Filter Syntax
A query filter is a string containing a boolean expression in a Java like syntax. It has to be valid in the scope of the candidate class. For example, "this.age >= 18"
is a valid filter if the candidate class contains a field with the name age
and a type comparable to int
. This section describes all the elements that can be used in a JDOQL query filter, except for parameters and variables that are discussed later in this chapter.
Literals
All types of Java literals are supported by JDOQL, as demonstrated by the following table:
Literal Type | Samples of valid literals in JDOQL |
int | 2003, -127, 0, 0xFFFF, 07777 , ... |
long | 2003L, -127L, 0L, 0xFFFFL, 07777L , ... |
float | 3.14F, 0f, 1e2f, -2.f, 5.04e+17f , ... |
double | 3.14, 0d, 1e2D, -2., 5.04e+17 , ... |
char | 'x', '@', '\n', '\\', '\'', '\uFFFF' , ... |
string | "", " ", "abcd\n1234" , ... |
boolean | true, false |
reference | null |
As shown in the next section (section 7.3), parameters could be used instead of constant literals to make queries more generic. Parameter values are provided when the query is executed so that the same query can be executed with different parameter values.
The 'this' Keyword
During query evaluation, each object in the candidate collection that is an instance of the candidate class is evaluated by the filter. The evaluated object is represented in the filter by the keyword this
, whose type is the candidate class. Usually this
is used for accessing fields of the candidate object, but it can also be used to reference a candidate object in other operations, such as method calls and comparison expressions.
Fields
Persistent fields of the candidate class play an important role in query filters. For instance, if a candidate class has a persistent field, verified
, of boolean
type, the expression "this.verified"
is a valid query filter. It selects all the objects with the true
value in that field. Other types of persistent fields can participate in queries in combination with comparison expressions, as in "this.age >= 18"
. The this.
prefix can be omitted in Java, therefore, "verified"
and "age >= 18"
are also valid filter expressions. Field accessing is not limited to the this
reference. Persistent fields of any entity that represents a persistent object can be accessed. This includes fields of parameter objects and variable objects (parameters and variables are discussed later in this chapter). The expression this.address.city
is also valid if the candidate class contains a persistent field address
whose type is a persistent class with a persistent field city
.
In Java, a NullPointerException
is thrown on any attempt to access a field or a method using a null
reference. JDOQL behavior is different. An attempt to access a field or invoke a method using a null
reference results in a false
evaluation for the containing expression but no exception is thrown. Notice that the candidate object can still be included in the result collection if the failed expression is only a subexpression of the whole filter expression (for example, when the ||
operator is used).
Operators
Comparison operators
Comparison operators (==, !=, <, >, <=, >=
) generate boolean expressions, which are very useful in queries. Comparison operators act in JDOQL as in Java, with a few exceptions:
- Equality operators (
==
,!=
) compare the identity of instances of user persistent classes (as in Java), but use theequals(
...)
method for system types (String
,Date
,ArrayList
, ...). String
instances can be compared using all six comparison operators.Date
instances can be compared using all six comparison operators.- Numeric wrapper types (
Byte
,Short
,Character
,Integer
,Long
,Float
,Double
), as well asBigInteger
andBigDecimal
can participate in comparisons as if they were primitive numeric types (byte
,short
,char
,int
,long
,float
,double
).
Logical Operators
Logical operators (&&
, ||
, !
) generate complex boolean expressions out of simpler boolean expressions. These operators function in JDOQL exactly as they do in Java.
Arithmetic Operators
In JDOQL, arithmetic operators (+
, -
, *
, /
) can be applied to numeric wrapper types (Byte, Character, Short, Integer, Long, Float, Double, and also BigInteger and BigDecimal). The concatenation operator '+
' can only be applied to two strings, but not to a string and some other type as in Java. Besides that, their behavior is the same as in Java.
Bitwise Operators
Only the ~
operator is supported by JDOQL. Binary bitwise operators (&
, |
) are not supported.
Methods
ObjectDB supports using any method that does not modify persistent objects, including instance methods and static methods. Notice, however, that the minimum requirement of JDO implementation includes support for two string methods (startsWith
and endsWith
), and two collection methods (contains
and isEmpty
, where isEmpty()
also returns true
when invoked on a null
value). Therefore, using other methods in queries might be less portable. One of the useful string methods that is not supported by JDO 1.0 but supported in ObjectDB (and will be supported by JDO 2.0) is the match(
...)
method. It provides string comparison using regular expressions (a possible replacement to the like operator of SQL). To use methods of user defined classes in queries, the user code must be available. In embedded mode, user classes are always available. In client-server mode, on the other hand, the classes are usually available on the client side, but not on the server side. Therefore, to enable user defined methods in query execution on the server side, classes have to be added to the classpath of the server as well.
Casting
Casting is also supported by JDOQL. If a ClassCastException
occurs during evaluation the expression is evaluated as false
but an exception is not thrown (similarly to handling NullPointerException
by JDOQL, as explained above).
Gathering Elements
Usually, a query filter contains a combination of elements. For example:
Query query = pm.newQuery(Person.class); query.setFilter("!this.children.isEmpty() && " + "this.age - ((Person)this.children.get(0)).age < 25"); Collection result = (Collection)query.execute();
This query retrieves all the parents who are older than their older child by not more than 25 years (assuming the child at position 0 is the older child). Support of the get(
...)
method is an extension of ObjectDB, but all the other elements are standard JDOQL elements. A JDO portable version of this query is shown in section 7.4.
7.3 Query Parameters
Using parameters instead of literals produces more generic queries, which can be executed with different argument values. The role of parameters in JDOQL, however, is for much more than just making more generic queries. The only way to include objects with no literal representation in queries (as Date
instances, and instances of user defined classes) is to use parameters.
Primitive Type Parameters
The first query in this chapter (in Section 7.1) includes an int
literal:
Query query = pm.newQuery(Person.class, "this.age >= 18"); Collection result = (Collection)query.execute();
The literal, 18
, can be replaced by a parameter, age
, whose type is int
:
Query query = pm.newQuery(Person.class, "this.age >= age"); query.declareParameters("int age"); Collection result1 = (Collection)query.execute(new Integer(18)); Collection result2 = (Collection)query.execute(new Integer(21));
Parameters are declared by the declareParameters(
...)
method, with a syntax that is similar to the syntax of parameter declarations in Java. The name of a parameter has to be a valid identifier in Java. Every parameter that is used in the filter has to be declared; otherwise, the compilation of the query fails. Notice that, in the above query, the this.
prefix in this.age
is required to distinguish the field from the parameter. This code demonstrates the execution of a query with two different arguments. An attempt to use the no-arg execute()
method on a query with parameters throws a JDOUserException
, because when a query is executed an argument has to be provided for each of its declared parameters. Wrapper values (Boolean
, Byte
, Short
, Character
, Integer
, Long
, Float
, Double
) are used instead of primitive values because execute(
...)
only accepts Object arguments.
Reference Type Parameters
The following query retrieves all the children who were born in the third millennium:
Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.set(2000, 0, 1); Date date = calendar.getTime(); Query query = pm.newQuery(Person.class, "this.birthDate >= date"); query.declareParameters("java.util.Date date"); Collection result = (Collection)query.execute(date);
The first four lines of code prepare a Date
instance representing the first day of the year 2000. The query is declared with a parameter date
of type Date
, and an argument for this parameter is provided when the query is executed. Class Person
must have a persistent field with the name birthDate
and type Date
. Otherwise, the query compilation fails. As explained in section 7.2, the comparison of Date
instances is supported by JDOQL. A full class name, java.util.Date
, has to be specified in the parameter declaration, unless an import statement is used, as explained in section 7.5.
Date
parameters are required to include Date
values in queries. String values, on the other hand, can be represented in queries by literals. Double quote characters are specified in Java strings using the \"
sequence:
Query query = pm.newQuery(Person.class, "this.firstName == \"Steve\""); Collection result = (Collection)query.execute();
Alternatively, the literal can be replaced by a parameter:
Query query = pm.newQuery(Person.class, "this.firstName == firstName"); query.declareParameters("String firstName"); Collection result = (Collection)query.execute("Steve");
The parameter type can be specified here as String
rather than as java.lang.String
because classes of java.lang
are automatically imported. More details can be found in section 7.5.
Instances of user defined classes can also be used as parameters:
Query query = pm.newQuery(Person.class, "this != p1 && this != p2"); query.declareParameters("Person p1, Person p2"); Collection result = (Collection)query.execute(person1, person2);
With the above query, all the Person
instances are retrieved, except the two Person
instances that are specified as arguments. To execute the query, two Person
arguments for the two declared parameters must be provided. Also, person1
and person2
must be instances of class Person
(or its subclasses), otherwise the query execution throws a JDOUserException
.
Array and Maps of Parameters
Queries with up to three parameters can be executed using the following Query
interface methods:
void execute() void execute(Object p1) void execute(Object p1, Object p2) void execute(Object p1, Object p2, Object p3)
For example, all the people in a range of ages can be retrieved by:
Query query = pm.newQuery(Person.class); query.declareParameters("int age1, int age2"); query.setFilter("this.age >= age1 && this.age <= age2"); Collection result = (Collection)query.execute(new Integer(20), new Integer(60));
The same query can be executed using other Query
interface methods:
void executeWithArray(Object[] parameters) void executeWithMap(Object[] parameters)
Arguments can be passed in an array:
Query query = pm.newQuery(Person.class); query.declareParameters("int age1, int age2"); query.setFilter("this.age >= age1 && this.age <= age2"); Integer[] args = new Integer[] { new Integer(20), new Integer(60) }; Collection result = (Collection)query.executeWithArray(args);
Arguments can also be passed in a map (by names rather than by order):
Query query = pm.newQuery(Person.class); query.declareParameters("int age1, int age2"); query.setFilter("this.age >= age1 && this.age <= age2"); Map args = new HashMap(); args.put("age1", new Integer(20)); args.put("age2", new Integer(60)); Collection result = (Collection)query.executeWithMap(args);
Arrays and maps of parameters are useful mainly in executing queries with more than three parameters.
7.4 Query Variables
There are two types of variables in JDOQL:
- Bound variables, whose main role is in querying collection fields
- Unbound variables, which serve as a replacement for the JOIN operation of SQL
Support of bound variables is mandatory, but support of unbound variables is optional in JDO. ObjectDB supports both types of variables.
The contains(...) Method
Bound variables in JDOQL go side by side with the contains(
...)
method, which is one of the few methods that every JDO implementation should support (see section 7.2). The following query, which does not use variables but does use contains(
...)
, retrieves all the people who live in cities from a specified collection myCities
:
Query query = pm.newQuery( Person.class, "cities.contains(this.address.city)"); query.declareParameters("Collection cities"); Collection result = (Collection)query.execute(myCities);
The contains(
...)
method is used mainly in queries with bound variables, but as shown above, it is also useful in queries without variables. The contains(
...)
method works in JDOQL as in Java, except that JDOQL returns false
when contains(
...)
is called on a null
reference, while Java throws a NullPointerException
.
Bound Variables
A bound variable is an identifier that appears in the query filter as an argument of contains(
...)
, and all its other appearances in the filter (usually at least one additional appearance) are in subexpressions that are ANDed with that contains(
...)
clause. In ObjectDB the order of the ANDed expressions is not important, but according to JDO, the contains(
...)
clause must come first (i.e. on the left side), before any other appearance of the variable in the query filter. The following query retrieves people with at least one child living in London:
Query query = pm.newQuery(Person.class); query.declareVariables("Person child"); query.setFilter( "this.children.contains(child) && child.address.city == \"London\""); Collection result = (Collection)query.execute();
A variable child
whose type is Person
is declared by the declareVariables(
...)
method. Variables (as well as parameters) can be declared before or after setting the query filter, but they must be declared before the query is compiled and executed, otherwise the query compilation fails because of unknown identifiers.
When an AND expression that includes a bound variable is evaluated, all the possible values for that variable are considered. The expression is evaluated as true
if there is at least one variable value for which the expression value is true
. In the query above, when a candidate Person
is evaluated as this
, only child
values contained in its children
collection are considered, because of the contains(
...)
clause. If there is such a child
for whom the expression on the right side of the &&
operator is true
, the entire AND expression is evaluated to true
, and the candidate object is added to the result collection.
Negative Contains(...)
The next query retrieves all the parents who are older than their older child by not more than 25 years (a similar query, but not JDO portable, is shown in section 7.2):
Query query = pm.newQuery(Person.class); query.declareVariables("Person child"); query.setFilter( "this.children.contains(child) && this.age - child.age <= 25" Collection result = (Collection)query.execute();
A single child satisfying the "this.age - child.age <= 25"
expression is sufficient to include this
in the result collection. On the other hand, to retrieve only parents with all children satisfying some condition, a trick has to be used. The following query uses De Morgan's rules to retrieve all the people who are older than every one of their children by not more than 25 years (including people without any children):
Query query = pm.newQuery(Person.class); query.declareVariables("Person child"); query.setFilter( "!(this.children.contains(child) && this.age - child.age > 25)" Collection result = (Collection)query.execute();
The expression in parenthesis refers to the people who are older than at least one of their children by more than 25 year. But because of the !
operator, eventually, the result collection contains all the people but them, i.e. all the people who are older than every one of their children by not more than 25 years.
Nested Bound Variables
In some cases, more than one contains(
...)
clause is needed. The following query retrieves all the people that have at least one child with a cat:
Query query = pm.newQuery(Person.class); query.declareVariables("Person child; Pet pet;"); query.setFilter("this.children.contains(child) && " + "child.pets.contains(pet) && pet.isCat()"); Collection result = (Collection)query.execute();
When more than one variable is declared, a semicolon is used as a separator. A semicolon at the end of the string is optional. Multiple variables are required above because two collection fields are involved, children
and pets
. Only Person
instances, with a combination of child
and pet
satisfying the AND expression, are included in the result collection. Because child
is dependent on this
and pet
is dependent on child
, the three subexpressions must be ordered as shown above, in order to make the query JDO portable.
Unbound Variables
Unbound variables are variables that are not constrained by a contains(
...)
. The following query retrieves all the people except the oldest and the youngest:
Query query = pm.newQuery(Person.class); query.declareVariables("Person p1; Person p2"); query.setFilter("this.age > p1.age && this.age < p2.age"); Collection result = (Collection)query.execute();
The type of every unbound variable is expected to be a persistent class with Extent
support (not necessarily the candidate class as in this example). The variable can have as values any objects in the Extent
. The result collection contains all the instances of the candidate collection (excluding non instances of the candidate class), for which there is at least one combination of variables that makes the query filter evaluate to true
.
Unbound variables are supported by ObjectDB, but considered optional by JDO. Queries with unbound variables are similar to JOIN queries in SQL because every combination of variables has to be checked for every candidate object. Just like JOIN queries in SQL, queries with unbound variables may become very slow, so caution is needed when using them.
7.5 Import Declarations
Names of classes are used in many components of JDOQL, including the declaration of parameters and variables, when casting and when accessing static methods and fields (supported by ObjectDB as an extension to JDO). As in Java, full class names that include a package name can always be used. The package name can be omitted only for classes in the java.lang
package (e.g. String
instead of java.lang.String
), for classes that are in the same package as the candidate class, and when a proper import declaration is used.
The following query, previously discussed in section 7.3, serves as a good example:
Query query = pm.newQuery(Person.class, "this.birthDate >= date"); query.declareParameters("java.util.Date date"); Collection result = (Collection)query.execute(date);
The full name of class Date
is specified because that class is in neither package java.lang
nor the package of the candidate class. Using a short class name (without the package name) causes a query compilation error unless a proper declareImports(
...)
declaration exists, as shown next:
Query query = pm.newQuery(Person.class, "this.birthDate >= date"); query.declareImports("import java.util.Date"); query.declareParameters("Date date"); Collection result = (Collection)query.execute(date);
Multiple import statements can also be declared:
query.declareImports( "import java.util.*; import directory.pc.Category;");
The argument of declareImports(
...)
is expected to use the Java syntax for import
statements. Multiple import statements are separated by semicolons. A semicolon at the end of the string is optional.
Of all the JDOQL components, import declarations are the most rarely used because most of the classes in queries belong to java.lang
or to the package of the candidate class and because classes of other packages can be specified by their full name without using an import declaration.
7.6 Ordering the Results
A result collection can be ordered by a query. For example:
Query query = pm.newQuery(Person.class, "this.age >= 18"); query.setOrdering("this.age ascending"); Collection result = (Collection)query.execute();
As a result of calling setOrdering(
...)
, an ordered collection is returned by the query. Iterating over the result collection using Iterator
returns people in order from younger to older. As usual, the this.
prefix can be omitted. The ascending
keyword specifies ordering from low to high. To order the results in reverse order, the descending
keyword can be used:
Query query = pm.newQuery(Person.class, "this.age >= 18"); query.setOrdering("this.age descending"); Collection result = (Collection)query.execute();
Iteration over the new result collection returns people in order from older to younger. Note that when setOrdering(
...)
is not used, ObjectDB orders result collections by object IDs from lower to higher, which means that objects will be returned in the order in which they were added to the database (but this behavior is not specified by JDO).
The syntax of order expressions is similar to the syntax of query filters, except that variables and parameters cannot be used, and the type of each order expression is some comparable type rather than boolean
. Valid types include numeric primitive types (byte
, short
, char
, int
, long
, float
, double
), numeric wrapper types (Byte
, Short
, Character
, Integer
, Long
, Float
, Double
), String
, Date
, BigInteger
and BigDecimal
.
Multiple order expressions can also be specified:
Query query = pm.newQuery(Person.class, "this.age >= 18"); query.setOrdering("age descending, children.size() ascending"); Collection result = (Collection)query.execute();
The primary ordering is age
and the secondary ordering is children.size()
(which is supported by ObjectDB as an extension to JDO). Results are ordered by the primary order expression. The secondary order expression is only needed when two or more result instances share the same primary ordering evaluation.