[ Team LiB ] |
9.6 The Query FilterThe query filter is a Boolean expression that is evaluated for each candidate instance; the query result includes only those instances that are true. The filter contains expressions supported by the JDO Query Language (JDOQL). Appendix D contains the Backus-Naur Form (BNF) syntax for JDOQL. The query filter is specified with respect to the object model defined by your persistent classes, using the field names in your persistent classes. You do not use the names and representation found in the underlying datastore. You write your applications using the single data model of your persistent classes. The filter can access the fields in your classes directly, even though they may be declared private. Some developers say that this breaks encapsulation, but database query languages express constraints on the values of fields. A JDOQL query will never modify the value of a field, and only the JDO implementation can access these fields in your application directly, which it needs to do anyway to manage their state. Those that argue this breaks encapsulation believe that only the methods of a class should access its fields. JDOQL has been designed so that query execution can take place in either the application's execution environment or the datastore server. Requiring the use of methods would require the datastore server to support Java and the loading of your application classes. This would severely limit the number of datastores that JDO could support. In most cases, the Java field names used in the query filter get remapped to the names of data constructs in the underlying datastore, which are then accessed in the datastore server environment. The names of persistent fields are supported as identifiers in query expressions. You may find some implementations supporting nonpersistent fields (including final and static fields), but implementations are not required to support these fields. So, if you want to write queries that will be portable across all implementations, do not use nonpersistent, final, or static fields in your filter expressions. You can provide the query filter to a Query when it is constructed, by using one of the newQuery( ) methods that takes a filter as a parameter, as we have done in the previous examples. Or, you can set the filter by calling the following Query method: void setFilter(String filter); 9.6.1 General Characteristics of ExpressionsThe identifiers in the filter should be in the namespace of the specified candidate class, with the addition of declared imports, parameters, and variables. As in the Java language, this is a reserved word that refers to the current candidate instance being evaluated from the collection or extent. JDOQL uses operators taken directly from the Java language, so Java developers will be familiar with them. Parentheses can be used to mark operator precedence explicitly. Whitespace—nonprinting characters, including space, tab, carriage return, and line-feed—in the filter is a separator and is otherwise ignored. Query expressions are nonmutating and have no side effects. The assignment operators (=, +=, etc.), pre- and post-increment, and pre- and post-decrement are not supported. JDOQL defines a few methods on String and Collection instances. But methods defined by the application, including object construction, are not supported. Nonmutating method calls may be supported in an implementation as a nonstandard extension. 9.6.2 Query OperatorsA subset of Java's operators can be used in the filter expression. The operators apply to all the types as defined in the Java language, except for a few cases that we will note in this section. You can use operator composition to construct arbitrarily complex expressions. You can use parentheses to control the precedence of multiple operators and make the expressions easier for others to read and understand. 9.6.2.1 Equality and inequality operatorsTable 9-1 specifies the equality operators. These expressions have a Boolean result. We have used these in our previous query examples.
The equal and not-equal operators are valid for all the operand types that are valid in Java. In addition, you can use them with the following operands:
In Java, the this.rating == movieRating expression compares the identity (references) of the String instances. In JDOQL, an expression evaluating the equality of Date and String values does not compare the object references as in Java. Instead, it tests the equality of their values. Comparisons between floating-point values are, by nature, inexact. Therefore, you should be cautious when using equality comparisons (== and !=) with floating-point values. If you need precise comparisons, use the type BigDecimal instead. Persistent instances compare equal if they have the same identity (i.e., they are the same instance in the datastore). Equality of references for nonpersistent types uses the equals( ) method defined for the class. A persistent and nonpersistent instance are never considered equal. If a datastore supports null values for Collection types, it is valid to compare a collection field to null. If you are using a datastore that does not support a null value for a Collection type, then a subexpression that compares a collection field to null evaluates false. If the datastore supports null values for Collection types, the javax.jdo.option.NullCollection option should be included in the list of supported options. 9.6.2.2 Comparison operatorsTable 9-2 lists the comparison operators, which have a Boolean result.
These comparison operators are valid for all the operand types defined in Java. In addition, they are valid for the following operands:
The comparison of two Date instances or two String instances compares the values represented by the instances. The ordering used in String comparisons is not defined in JDO. This allows implementations to order them according to a datastore-specific ordering, which might be locale-specific. 9.6.2.3 Boolean operatorsTable 9-3 lists the supported Boolean operators. These expressions have Boolean operands and compute a Boolean result.
The following example uses these Boolean operators to access all the Movie instances that have a rating other than G or PG and a running time between an hour and an hour and 45 minutes: public static void queryMovie4(PersistenceManager pm) { Extent movieExtent = pm.getExtent(Movie.class, true); String filter = "!(rating == \"G\" || rating == \"PG\") && " + "(runningTime >= 60 && runningTime <= 105)"; Query query = pm.newQuery(movieExtent, filter); Collection result = (Collection) query.execute( ); Iterator iter = result.iterator( ); while (iter.hasNext( )) { Movie movie = (Movie) iter.next( ); System.out.println(movie.getTitle( )); } query.close(result); } You can use these Boolean operators and parentheses to compose query expressions as nested and complex as necessary to express your filter. The previous example also demonstates the use of String and int literals. Since String literals in a JDOQL filter use Java's syntax of double-quote delimiters, you need to use the backslash character (\) when specifying your filter with a Java String literal in your application. These back-quotes are not needed in JDOQL's syntax, and they are not placed in this String filter we have declared. Query filters are simpler if you use a query parameter instead of a String literal. A parameter also provides more flexibility than a literal, because it allows you to provide an alternative value in the query. The operators listed in Table 9-3 lists correspond to Java's Boolean (&, |) and conditional (&&, ||) operators. In Java, the Boolean operators always evaluate both operands, but the conditional operators first evaluate the left operand and evaluate the right operand only if necessary to determine the Boolean result. In Java, && evaluates the right operand only if the value of the left operand is true, and || evaluates the right operand only if the value of the left operand is false. This aspect of Java's conditional operators is not preserved in JDOQL. There are no side effects of operators in JDOQL, which could be leveraged by such conditional evaluations. JDOQL implementations may or may not evaluate the right operand based on the evaluation of the left operand; this is purely an optimization decision. Some underlying datastores, such as those based on SQL, do not have such conditional operators. A SQL implementation would likely map both & and && to the SQL AND operator. 9.6.2.4 Arithmetic operatorsTable 9-4 lists the supported arithmetic operators.
The result type of these expressions depends on the operand types, as explained in the Promotion of Numeric Operands sidebar. Let's examine a query that uses these arithmetic operators: public static void queryProfits(PersistenceManager pm, BigDecimal value, BigDecimal sellCost, BigDecimal rentCost) { Query query = pm.newQuery(MediaItem.class); [1] query.declareImports("import java.math.BigDecimal"); query.declareParameters( [2] "BigDecimal value, BigDecimal sellCost, BigDecimal rentCost"); query.setFilter("soldYTD * (purchasePrice - sellCost) + " + [3] "rentedYTD * (rentalCode.cost - rentCost) > value"); Collection result = (Collection) query.execute(value, sellCost, rentCost); Iterator iter = result.iterator( ); while (iter.hasNext( )) { MediaItem item = (MediaItem) iter.next( ); // process MediaItem } query.close(result); } We initialize a Query instance on line [1], where we set the candidate class. Notice that we do not explicitly specify the candidate collection. If we specify the candidate class but not the candidate collection (as we do here), the candidate collection defaults to the Extent of the candidate class, with subclasses included (the Extent component that indicates subclasses should be included is true). In this query we retrieve all the MediaItem instances whose profit this year exceeds the value parameter. There are costs associated with the selling and renting of an item; the values for these costs are passed via the sellCost and rentCost query parameters, declared on line [2]. These values are subtracted from the price charged to purchase or rent the item in the filter specified on line [3]. We multiply the per-item profits by the number of items sold and rented year-to-date. We then determine whether the profits for an item exceed the threshold specified by the value query parameter. The query returns only those items whose profits exceed the value parameter. The precedence of the arithmetic operators in the JDOQL filter is identical to their precedence in Java. We have used parentheses to override the precedence. We could add additional parentheses to make the expression more clear for those that are not always certain of the operator precedences. 9.6.2.5 String expressionsTwo String methods are defined, startsWith( ) and endsWith( ): boolean startsWith(String str); boolean endsWith(String str); These methods operate on a String within a query. The startsWith( ) method returns true if the String begins with the value in the str argument. The endsWith( ) method returns true if the String ends with the value in the str argument. These methods provide support for wild card queries. However, no special semantics are associated with the str argument; in particular, no specific wild-card characters are supported. A typical nonstandard implementation based on a SQL datastore would map the JDOQL query expression: name.startsWith("%Tina") to the SQL LIKE operation: NAME LIKE ('%Tina%') The '%' wild-card character represents zero or more characters. The startsWith( ) method adds a '%' at the end of its parameter's value when it is mapped to SQL. The + operator can be used to specify String concatenation, but it is supported only for String operands. Thus, this is supported: "Movie: " + title But this expression is not: title + 5 9.6.3 ReferencesYou can use the . (dot) operator to navigate through reference fields, as in Java. You can also use the . operator to navigate through multiple references in your object model. For example, the following expression assumes that we have a filter operating on a set of RentalItem candidate instances: currentRental.customer.address.city We navigate from the RentalItem to the Rental instance by using the currentRental field, then use the customer field inherited from Transaction to access the specific Customer that has rented the RentalItem. We then use the address field to get the customer's address and access the city. This example also illustrates that your expressions can access inherited fields; we access the customer field in Transaction, the base class of Rental. Using such navigations does not change the candidate class; you cannot return the instances accessible via navigation. If your main goal is to query and return instances of a class accessible via such a navigation, the class of the instances that you want in your result should be your candidate class and you should provide a filter that may include a navigation that performs the inverse of your original navigation expression. In Java, when you navigate through a null reference, a NullPointerException is thrown. But if a subexpression in a query traverses through a null reference, the subexpression does not throw an exception; it evaluates as false. Only the subexpression is false, not the entire filter. Other subexpressions in the filter or other values for variables may still qualify the candidate instance for inclusion in the result set. 9.6.3.1 Cast expressionJava and JDO allow a base class reference to contain a reference to an instance of a subclass. In addition, Java and JDO allow you to declare a reference to an interface and initialize it with a reference to an instance of any class that has been declared to implement the interface. We have demonstrated that when you have a reference to a subclass (Rental), you can directly use fields in a base class (Transaction). But suppose you have a reference to a base class and want to have a query expression that determines whether the reference is to a particular subclass and, if so, accesses a field of the subclass. Likewise, suppose you have an interface reference. You cannot call the methods of the Java interface in a query expression, but you may want to determine whether the reference refers to an instance of a specific class and, if so, have a query expression using a field of that class. You can express such queries in JDOQL by using a cast expression. The syntax of the cast expression is identical to its use in Java. Precede the reference expression with a type name, enclosed in parentheses. If you cast a reference to a specific class, an attempt is made to convert the reference to the class. If the cast fails (which would throw a ClassCastException in Java), the most-nested Boolean subexpression in which the cast was performed is false. This behavior also occurs if you navigate through a null reference in JDOQL. If the cast succeeds, then the reference can be used to access the referenced instance as an instance of the type used in the cast. The following example uses the collection of historical transactions associated with a particular Customer as its candidate set of instances: public static void queryTransactions(PersistenceManager pm, Customer cust) { Query query = pm.newQuery(com.mediamania.store.Rental.class, [1] cust.getTransactionHistory( )); String filter = "((Movie)(rentalItem.mediaItem.content)).director." + [2] "mediaName == \"James Cameron\""; query.declareImports("import com.mediamania.content.Movie"); [3] query.setFilter(filter); [4] Collection result = (Collection) query.execute( ); Iterator iter = result.iterator( ); while (iter.hasNext( ) ){ Rental rental = (Rental) iter.next( ); MediaContent content = rental.getRentalItem().getMediaItem().getMediaContent( ); System.out.println(content.getTitle( )); } query.close(result); } The transactionHistory collection in Customer contains Transaction instances, which are either Rental or Purchase instances. We only want to process the Rental instances in the collection, so we set the Rental class as the candidate class in the call to newQuery( ) on line [1]. In the filter, declared on line [2], we navigate from the Rental instance to the RentalItem, from the RentalItem to the MediaItem, and from the MediaItem to the MediaContent instance. The MediaContent instance can be either a Movie or a Game instance. We want to determine which movies the customer is currently renting that were directed by James Cameron. So, we cast the MediaContent reference to a Movie instance on line [2]. This allows us to access the director field defined in the Movie class. We then determine whether this movie was directed by James Cameron. Line [4] sets the filter for the query. Since our Rental candidate class is defined in the com.mediamania.store package and we are casting to the Movie class, which is defined in the com.mediamania.content package, it is necessary to import the Movie class on line [3]. In this example, we constrain the transactionHistory collection to Rental instances by specifying Rental as the candidate class. An alternative, less-elegant approach would be to cast to Rental in the filter itself. Lines [1] and [2] could be replaced with the following lines: Query query = pm.newQuery(com.mediamania.store.Transaction.class, cust.getTransactionHistory( )); String filter = "((Movie)(((Rental)this).rentalItem.mediaItem.content))." + "director.mediaName == \"James Cameron\""; But the use of multiple casts results in a more-complex filter. The first solution is simpler. As we noted previously, we could simplify the filter by passing the director's name as a parameter instead of using the String literal. 9.6.4 CollectionsYou can also use collections in your query expressions. The isEmpty( ) and contains( ) methods are defined for use with a collection in a query. The method isEmpty( ) determines whether a collection is empty: boolean isEmpty( ); Not all datastores allow a null-valued collection to be stored. Portable queries on these collections should use isEmpty( ) instead of comparing to null. A null collection field is treated as if it is empty if a method is called on it. In particular, isEmpty( ) returns true, and contains( ) returns false. You can also have a query expression that examines a collection to determine whether an element exists in the collection that has a true value for a provided query expression. This allows you to navigate to a set of related instances in the datastore. You navigate by using the contains( ) method, which lets you associate a variable with the elements of a collection. The variable can then be used to express constraints on the collection elements. 9.6.4.1 Variable declarationTo access the elements of a collection, you must declare the variable with its name and type. Variables are declared in a String containing one or more variable declarations, separated by a semicolon if there there is more than one variable declaration. It uses the same syntax you use in Java to declare a method's local variables. The following Query method binds a variable declaration to the Query instance: void declareVariables(String variables); You will need to import the type using declareImports( ) if the variable's type is not already in the query's type namespace. 9.6.4.2 The contains( ) methodThe contains( ) method is used in conjunction with an AND expression to determine whether an element of a collection results in a true result for at least one element of the collection. You associate a variable with the elements of a collection by passing the variable to contains( ). The contains( ) method must be the left operand of an AND expression in which the variable used is the right operand: boolean contains(Object o); The contains( ) method returns true if at least one collection element results in a true result for the right operand of its associated AND expression. A portable query filter must constrain all of its variables that are used in any of its expressions, by applying the contains( ) clause to a persistent field of a persistent class. That is, each occurrence of an expression in the filter using the variable includes a contains( ) clause ANDed with an expression using the variable. The following example finds all Movie instances for which the director also played an acting role in the movie: Extent movieExtent = pm.getExtent(Movie.class, true); String filter = "cast.contains(role) && role.actor == director"; [1] Query query = pm.newQuery(movieExtent, filter); query.declareVariables("Role role"); [2] Collection result = (Collection) query.execute( ); In this query, we declare a variable, named role, on line [2] to reference the Role instances in the cast collection. We use the contains( ) method on line [1] to associate the role variable with the elements of cast. The contains( ) expression is the left operand of &&, and the right operand has an expression using the role variable. The right operand's expression checks to see whether the MediaPerson referenced by the actor field is equal to the director field in the Movie instance. You use the contains( ) method to see whether at least one element exists in the collection that is true for the expression in the right operand. Since only one collection element needs to have a true result for the right operand, not all of the collection elements need to be processed. Evaluation can stop once the first collection element is found with a true result for the right operand. The contains( ) method and its associated ANDed right operand are considered an expression. Negating this expression with the ! operator asks if it is true that no element exists in the collection that is true for the right operand (i.e., that there is no element in the collection for which the right operand is true). The following example illustrates the use of multiple variables. In fact, it navigates through multiple collections by using the second variable to access elements of a collection accessed by the first variable. This query finds all the Movie instances currently being rented by customers that live in a city with a given name. public static void queryMoviesSeenInCity(PersistenceManager pm, String city) { String filter = "mediaItems.contains(item) &&" + [1] "(item.rentalItems.contains(rentalItem) && " + [2] "(rentalItem.currentRental.customer.address.city == city))"; [3] Extent movieExtent = pm.getExtent(Movie.class, true); Query query = pm.newQuery(movieExtent, filter); query.declareImports("import com.mediamania.store.MediaItem; " + [4] "import com.mediamania.store.RentalItem"); query.declareVariables("MediaItem item; RentalItem rentalItem"); [5] query.declareParameters("String city"); Collection result = (Collection) query.execute(city); Iterator iter = result.iterator( ); while (iter.hasNext( )) { Movie movie = (Movie) iter.next( ); System.out.println(movie.getTitle( )); } query.close(result); } Line [5] declares the variables item and rentalItem. Line [1] associates the item variable with the MediaItem instances of the current Movie candidate instance. The rest of the filter is the right operand of the associated AND operator. We access the RentalItem instances associated with the MediaItem instances (referenced by item) by binding the rentalItem variable with the rentalItems collection. We then use the rentalItem variable to access the current Rental transaction and navigate to access the city of the customer renting the movie. For a portable query, the contains( ) clause must be the left expression of an AND expression in which the variable is used in the right expression. The filter specified on line [1] illustrates a situation where you need to use parentheses to override Java's left-associativity rule that applies when there are two or more operators with the same precedence in a filter expression. If we had declared the filter as: String filter = "mediaItems.contains(item) &&" + "item.rentalItems.contains(rentalItem) && " + "(rentalItem.currentRental.customer.address.city == city)"; it would have been evaluated as: String filter = "(mediaItems.contains(item) &&" + "item.rentalItems.contains(rentalItem)) && " + "(rentalItem.currentRental.customer.address.city == city)"; which is not valid, because rentalItem on the third line is not the right operand of an AND expression whose left operand binds rentalItem with a contains( ). A portable query will constrain all of its variables with a contains( ) method in each OR expression the filter may have. A variable that is not constrained with an explicit contains( ) method is constrained by the extent of the persistent class (including subclasses) in the database, based on the variable's declared class. Such a variable is referred to as an unbound variable. If the variable's class does not manage an Extent, then no results will satisfy the query. For example, the following query returns all movies from the same director that were released after a particular movie, specified by title: public static void queryRecentMovies(PersistenceManager pm, String title) { Extent movieExtent = pm.getExtent(Movie.class, true); String filter = "this.releaseDate > movie.releaseDate && " + "this.director == movie.director && movie.title == title"; Query query = pm.newQuery(movieExtent, filter); query.declareParameters("String title"); query.declareVariables("Movie movie"); Collection result = (Collection) query.execute(title); Iterator iter = result.iterator( ); while (iter.hasNext( )) { Movie movie = (Movie) iter.next( ); // process Movie } } The movie variable of type Movie is unconstrained, so it is evaluated relative to the Movie extent. In this particular query, the unbound variable accesses the same extent as the query, but this just a coincidence, as the extent accessed by an unconstrained variable is based on the variable's declared type. |
[ Team LiB ] |