9.5 Query Execution

When a query executes, the query filter is evaluated for each element of the candidate collection. Those instances that evaluate to true for the filter are included in the query result, which is a subset of the instances in the candidate collection. The query result should be cast to a Collection (execute( ) is declared to return an Object). You should then aquire an Iterator to access the instances in the result.

9.5.1 Parameter Declarations

When you execute a query, you often need to provide one or more values to be used in the query filter's expressions. One technique is to generate the query filter string dynamically, providing the necessary values directly in the filter. But this approach does not allow the same query to be compiled and reused in subsequent query executions, which are likely to require the same filter expressions but with different values.

Query parameters allow you to specify such values dynamically when the query is executed. The parameter names are used in the filter expression to specify constraints. A parameter name can be used zero, one, or multiple times in the query filter. When you execute the query, each parameter must be provided a value; these values are substituted for each use of the parameter name in the filter. You can use parameters to minimize the need to construct a unique query filter dynamically each time you execute a query.

You need to declare a name and type for each query parameter. In addition, you may need to import the type of the parameter using declareImports( ). The parameter declaration is a String containing one or more parameter type declarations, separated by commas. This follows the Java syntax for declaring the parameters of a method. All the query parameters are declared in a single String. The following Query method binds the parameter declarations to the Query instance:

void declareParameters(String parameters);

Each parameter must be bound to a value when the query is executed. They are passed to the query execute( ) methods as Java Objects; these values might be of simple wrapper types or more complex object types. The first example in this chapter had the following query parameter declaration:

query.declareParameters("String city, String state");

You may want to have a parameter of a primitive type, such as int. You can declare a parameter to have type int, but the value passed in the call to execute( ) must be the primitive's wrapper type, since it is passed as an Object. So, a query parameter declared with type int requires an Integer value to be passed to execute( ). In addition, the parameter value passed to execute( ) for primitive type parameters cannot be null, because there would not be a valid value for the parameter in the query expressions. A query parameter can be used in the filter as an operand of any query operator that accepts a value of the parameter's type.

You can also have a query parameter that is an instance of a persistent class. Such a parameter and the fields it references can be used with any of the supported query expressions, including the ability to navigate to other instances. The instances should be persistent or transactional and be associated with the same PersistenceManager as the Query instance. If a persistent instance associated with another PersistenceManager is passed as a parameter, a JDOUserException is thrown during execute( ). Some implementations may support a query parameter that is a transient instance of a persistent class, but implementations are not required to support this.

9.5.2 Executing a Query

The Query interface provides methods to execute a query with zero or more parameters. The execute( ) method has been overloaded so you can pass zero, one, two, or three parameters:

Object execute(  );
Object execute(Object parameter1);
Object execute(Object parameter1, Object parameter2);
Object execute(Object parameter1, Object parameter2, Object parameter3);

Two other methods, described later in this section, allow you to pass more query parameters using a different parameter-passing technique. Each query parameter is an Object. As discussed earlier, you use a wrapper type (Integer) to pass the value for a primitive parameter (int). The parameters passed to execute( ) are associated with the declared parameters, based on their order. The parameters passed to the execute methods are used only for the current execution and are not preserved for use in subsequent query executions. If the PersistenceManager that constructed a Query is closed when an execute method is called, a JDOUserException is thrown.

In the following example, we access all the Movie instances with a specific rating, a running time shorter than a specific duration, and a particular director:

public static void queryMovie1(PersistenceManager pm,
                               String rating, int runtime, MediaPerson dir) {
    Extent movieExtent = pm.getExtent(Movie.class, true);
    String filter =
        "rating == movieRating && runningTime <= runTime && dir == director";
    Query query = pm.newQuery(movieExtent, filter);
    query.declareParameters("String movieRating, int runTime, MediaPerson dir");     [1]
    Collection result = (Collection)
                            query.execute(rating, new Integer(runtime), dir);     [2]
    Iterator iter = result.iterator(  );
    while (iter.hasNext(  )) {
        Movie movie = (Movie) iter.next(  );
        System.out.println(movie.getTitle(  ));
    }
    query.close(result);
}

We declare three parameters on line [1]. The second parameter is of type int, and the third parameter is of type MediaPerson, one of our persistent classes. Since MediaPerson is in the same package as the Movie candidate class, we do not need to import MediaPerson explicitly with an import declaration. The JDOQL implementation will convert the Integer parameter passed on line [2] to the int declared on line [1]. The query would also have been valid if we had declared the runTime query parameter to be an Integer. Even though we compare runTime with the int field runningTime, JDOQL handles such conversions automatically (see the Promotion of Numeric Operands sidebar in this chapter).

The execute( ) methods execute the query with the supplied parameters and return a result. An element of the candidate collection is returned in the result if it is assignment-compatible with the candidate class of the Query, and for all variables in the query there exists a value for which the query filter expression evaluates to true. We will cover variables later in this chapter. If the query filter is not specified when the query is executed, then the filter defaults to true and the input collection is filtered to include only instances of the candidate class.

The return type of the execute( ) methods is Object. In JDO 1.0.1, the execute( ) methods return an object that supports the operations of an unmodifiable Collection; the value returned should be cast to a Collection. A future JDO release may support queries that return a single instance; the method has been defined to return Object to allow for this future extension. An implementation of a non-JDOQL query language might return a value of a different type (e.g., java.sql.ResultSet).

You can iterate the unmodifiable Collection returned by the execute( ) methods to access the query results. Executing any operation that might change the Collection causes an UnsupportedOperationException. Although the object returned by execute( ) is declared to implement Collection, most implementations do not return a collection that has been fully populated with the results of the query. The primary use of the returned object is to acquire an Iterator via the iterator( ) method defined in the Collection interface. The returned Collection can also serve as the set of candidate instances for an additional query, supporting a form of subqueries.

The execute( ) methods described in this section support a maximum of three parameters. It is also possible to pass parameters via a Map:

Object executeWithMap(Map parameters);

The executeWithMap( ) method is similar to execute( ), but it takes its parameters from a Map instance. The Map contains key/value pairs, where the key is the parameter's declared name and the value is the actual value to use for the parameter in the query. Unlike execute( ), you can pass an unlimited number of parameters to executeWithMap( ).

The following example extends the previous example to return only Movie instances that were released after a specified date. This query requires four parameters, so we will use executeWithMap( ). At line [1], we begin populating a HashMap with the query parameters. The Map entry's key is the parameter name, as specified in declareParameters( ), and its value is the value to use for the parameter.

public static void queryMovie2(PersistenceManager pm,
                            String rating, int runtime, MediaPerson dir, Date date) {
    Extent movieExtent = pm.getExtent(Movie.class, true);
    String filter = "rating == movieRating && runningTime <= runTime && " +
                    "dir == director && releaseDate >= date";
    Query query = pm.newQuery(movieExtent, filter);
    query.declareImports("import java.util.Date");
    query.declareParameters(
        "String movieRating, int runTime, MediaPerson dir, Date date");
    HashMap parameters = new HashMap(  );
    parameters.put("movieRating", rating);     [1]
    parameters.put("runTime", new Integer(runtime));
    parameters.put("dir", dir);
    parameters.put("date", date);
    Collection result = (Collection) query.executeWithMap(parameters);     [2]
    Iterator iter = result.iterator(  );
    while (iter.hasNext(  )) {
        Movie movie = (Movie) iter.next(  );
        System.out.println(movie.getTitle(  ));
    }
    query.close(result);
}

Parameters can also be passed with an array:

Object executeWithArray(Object[] parameters);

The executeWithArray( ) method is also similar to execute( ), but it takes its parameters from an array instance. The array contains Objects; the position of parameters in the parameter declaration determines the position of their corresponding values in the array. The number of elements in the array must be equal to the number of parameters that have been declared. Similar to executeWithMap( ), the number of parameters is not limited.

The following example performs the same query as the previous one, except this time we use executeWithArray( ). The order in which the parameters are declared on line [1] must correspond with the order in which the values are populated in the array on line [2].

public static void queryMovie3(PersistenceManager pm,
                               String rating, int runtime, MediaPerson dir,
                               Date date) {
    Extent movieExtent = pm.getExtent(Movie.class, true);
    String filter = "rating == movieRating && runningTime <= runTime && " +
                    "dir == director && releaseDate >= date";
    Query query = pm.newQuery(movieExtent, filter);
    query.declareImports("import java.util.Date");
    query.declareParameters(
        "String movieRating, int runTime, MediaPerson dir, Date date");     [1]
    Object[] parameters = { rating, new Integer(runtime), dir, date };     [2]
    Collection result = (Collection) query.executeWithArray(parameters);
    Iterator iter = result.iterator(  );
    while (iter.hasNext(  )) {
        Movie movie = (Movie) iter.next(  );
        System.out.println(movie.getTitle(  ));
    }
    query.close(result);
}

The result of a query can be very large, depending on the size of the candidate collection and filter. An application can iterate through the result or pass it to another Query as its candidate instances. The size( ) method defined in Collection might return Integer.MAX_VALUE if the actual size of the result is not known. A portable application should not use size( ).

You can call any of these execute methods repeatedly for the same Query instance. All of the query components, including the candidate collection, are maintained by the Query instance after execution. This allows you to reexecute the same query with different query parameter values. You can also change any of the query components of a Query after it has been executed. The Query will be recompiled before it is executed.

9.5.3 Compiling a Query

Before you can execute a query, it is compiled to verify its correctness. Compiling a Query validates its components and reports any inconsistencies by throwing a JDOUserException. When execute( ) is called, if the Query has not compiled or if a query component has been changed since the Query was last compiled, the Query compiles automatically.

You can verify the correctness of a query before executing it by compiling it directly. The following Query method compiles a query:

void compile(  );

Calling compile( ) tells the Query instance to prepare and optimize an execution plan for the query. Once a Query is compiled, it can be executed repeatedly without incurring the initial parsing and optimization overhead.