Table of Contents
Cayenne provides a simple yet powerful object-based expression language. The most common usese of expressions are to build qualifiers and orderings of queries that are later converted to SQL by Cayenne and to evaluate in-memory against specific objects (to access certain values in the object graph or to perform in-memory object filtering and sorting). Cayenne provides API to build expressions in the code and a parser to create expressions from strings.
Before discussing how to build expressions, it is important to understand one group of expressions widely used in Cayenne - path expressions. There are two types of path expressions - object and database, used for navigating graphs of connected objects or joined DB tables respectively. Object paths are much more commonly used, as after all Cayenne is supposed to provide a degree of isolation of the object model from the database. However database paths are helpful in certain situations. General structure of path expressions is the following:
[db:]segment[+][.segment[+]...]
"db:" is an optional prefix indicating that the following path is a DB path. Otherwise it is an object path.
"segment" is a name of a property (relationship or attribute in Cayenne terms) in the path. Path must have at least one segment; segments are separated by dot (".").
"+" An "OUTER JOIN" path component. Currently "+" only has effect when translated to SQL as OUTER JOIN. When evaluating expressions in memory, it is ignored.
An object path expression represents a chain of property names rooted in a certain (unspecified during expression creation) object and "navigating" to its related value. E.g. a path expression "artist.name" might be a property path starting from a Painting object, pointing to the related Artist object, and then to its name attribute. A few more examples:
"name" - can be used to navigate (read) the "name" property of a Person (or any other type of object that has a "name" property).
"artist.exhibits.closingDate" - can be used to navigate to a closing date of any of the exhibits of a Painting's Artist object.
"artist.exhibits+.closingDate" - same as the previous example, but when translated into SQL, an OUTER JOIN will be used for "exhibits".
Similarly a database path expression is a dot-separated path through DB table joins and columns. In Cayenne joins are mapped as DbRelationships with some symbolic names (the closest concept to DbRelationship name in the DB world is a named foreign key constraint. But DbRelationship names are usually chosen arbitrarily, without regard to constraints naming or even constraints presence). A database path therefore might look like this - "db:dbrelationshipX.dbrelationshipY.COLUMN_Z". More specific examples:
"db:NAME" - can be used to navigate to the value of "NAME" column of some unspecified table.
"db:artist.artistExhibits.exhibit.CLOSING_DATE" - can be used to match a closing date of any of the exhibits of a related artist record.
Cayenne supports "aliases" in path Expressions. E.g. the same expression can be written using explicit path or an alias:
"artist.exhibits.closingDate" - full path
"e.closingDate" - alias "e" is used for "artist.exhibits".
SelectQuery using the second form of the path expression must be made aware of the alias via "SelectQuery.aliasPathSplits(..)", otherwise an Exception will be thrown. The main use of aliases is to allow users to control how SQL joins are generated if the same path is encountered more than once in any given Expression. Each alias for any given path would result in a separate join. Without aliases, a single join will be used for a group of matching paths.
While in most cases users are likely to rely on API from the following section for
expression creation, we'll start by showing String expressions, as this will help
to understand the semantics. A Cayenne expression can be represented as a String, which
can be converted to an expression object using ExpressionFactory.exp
static
method. Here is an
example:
String expString = "name like 'A%' and price < 1000";
Expression exp = ExpressionFactory.exp(expString);
This particular expression may be used to match Paintings whose names that start with "A" and whose price is less than $1000. While this example is pretty self-explanatory, there are a few points worth mentioning. "name" and "price" here are object paths discussed earlier. As always, paths themselves are not attached to a specific root entity and can be applied to any entity that has similarly named attributes or relationships. So when we are saying that this expression "may be used to match Paintings", we are implying that there may be other entities, for which this expression is valid. Now the expression details...
Character constants that are not paths or numeric values should be enclosed in single or double quotes. Two of the expressions below are equivalent:
name = 'ABC' // double quotes are escaped inside Java Strings of course name = \"ABC\"
Case sensitivity. Expression operators are case sensitive and are usually lowercase. Complex words follow the Java camel-case style:
// valid name likeIgnoreCase 'A%' // invalid - will throw a parse exception name LIKEIGNORECASE 'A%'
Grouping with parenthesis:
value = (price + 250.00) * 3
Path prefixes. Object expressions are unquoted strings, optionally prefixed by "obj:" (usually they are not prefixed at all actually). Database expressions are always prefixed with "db:". A special kind of prefix, not discussed yet is "enum:" that prefixes an enumeration constant:
// object path name = 'Salvador Dali' // same object path - a rarely used form obj:name = 'Salvador Dali' // multi-segment object path artist.name = 'Salvador Dali' // db path db:NAME = 'Salvador Dali' // enumeration constant name = enum:org.foo.EnumClass.VALUE1
Binary conditions are expressions that contain a path on the left, a value on the right, and some operation between them, such as equals, like, etc. They can be used as qualifiers in SelectQueries:
name like 'A%'
Parameters. Expressions can contain named parameters (names that start with "$") that can be substituted with values either by name or by position. Parameterized expressions allow to create reusable expression templates. Also if an Expression contains a complex object that doesn't have a simple String representation (e.g. a Date, a DataObject, an ObjectId), parameterizing such expression is the only way to represent it as String. Here are the examples of both positional and named parameter bindings:
Expression template = ExpressionFactory.exp("name = $name"); ... // name binding Map p1 = Collections.singletonMap("name", "Salvador Dali"); Expression qualifier1 = template.params(p1); ... // positional binding Expression qualifier2 = template.paramsArray("Monet");
Positional binding is usually shorter. You can pass positional bindings to the
"exp(..)"
factory method (its second argument is a params
vararg):
Expression qualifier = ExpressionFactory.exp("name = $name", "Monet");
In parameterized expressions with LIKE clause, SQL wildcards must be part of the values in the Map and not the expression string itself:
Expression qualifier = ExpressionFactory.exp("name like $name", "Salvador%");
When
matching on a relationship, the value parameter must be either a Persistent object, an
org.apache.cayenne.ObjectId
, or a numeric ID value (for single column
IDs).
E.g.:
Artist dali = ... // asume we fetched this one already Expression qualifier = ExpressionFactory.exp("artist = $artist", dali);
When using positional binding, Cayenne would expect values for all parameters to be present. Binding by name offers extra flexibility: subexpressions with uninitialized parameters are automatically pruned from the expression. So e.g. if certain parts of the expression criteria are not provided to the application, you can still build a valid expression:
Expression template = ExpressionFactory.exp("name like $name and dateOfBirth > $date"); ... Map p1 = Collections.singletonMap("name", "Salvador%"); Expression qualifier1 = template.params(p1); // "qualifier1" is now "name like 'Salvador%'". // 'dateOfBirth > $date' condition was pruned, as no value was specified for // the $date parameter
Null handling. Handling of Java nulls as operands is no different from normal values. Instead of using special conditional operators, like SQL does (IS NULL, IS NOT NULL), "=" and "!=" expressions are used directly with null values. It is up to Cayenne to translate expressions with nulls to the valid SQL.
A formal definition of the expression grammar is provided in Appendix C
Creating expressions from Strings is a powerful and dynamic approach, however a safer
alternative is to use Java API. It provides compile-time checking of expressions
validity. The API in question is provided by ExpressionFactory
class (that
we've seen already), Property
class and Expression
class
itself. ExpressionFactory
contains a number of self-explanatory static
methods that can be used to build expressions. E.g.:
// String expression: name like 'A%' and price < 1000 Expression e1 = ExpressionFactory.likeExp("name", "A%"); Expression e2 = ExpressionFactory.lessExp("price, 1000); Expression finalExp = e1.andExp(e2);
The last line in the example above shows how to create a new expression by "chaining" two other epxressions. A common error when chaining expressions is to assume that "andExp" and "orExp" append another expression to the current expression. In fact a new expression is created. I.e. Expression API treats existing expressions as immutable.
As discussed earlier, Cayenne supports aliases in path Expressions, allowing to control how SQL joins are generated if the same path is encountered more than once in the same Expression. Two ExpressionFactory methods allow to implicitly generate aliases to "split" match paths into individual joins if needed:
Expression matchAllExp(String path, Collection values) Expression matchAllExp(String path, Object... values)
"Path" argument to both of these methods can use a split character (a pipe
symbol '|') instead of dot to indicate that relationship following a path
should be split into a separate set of joins, one per collection value. There can only
be one split at most in any given path. Split must always precede a relationship. E.g.
"|exhibits.paintings"
, "exhibits|paintings"
, etc.
Internally Cayenne would generate distinct aliases for each of the split expressions,
forcing separate joins.
While ExpressionFactory is pretty powerful, there's an even easier way to create expression using static Property objects generated by Cayenne for each persistent class. Some examples:
// Artist.NAME is generated by Cayenne and has a type of Property<String> Expression e1 = Artist.NAME.eq("Pablo"); // Chaining multiple properties into a path.. // Painting.ARTIST is generated by Cayenne and has a type of Property<Artist> Expression e2 = Painting.ARTIST.dot(Artist.NAME).eq("Pablo");
Property objects provide the API mostly analogius to ExpressionFactory, though it is significantly shorter and is aware of the value types. It provides compile-time checks of both property names and types of arguments in conditions. We will use Property-based API in further examples.
When used in a query, an expression is converted to SQL WHERE clause (or ORDER BY clause) by Cayenne during query execution. Thus the actual evaluation against the data is done by the database engine. However the same expressions can also be used for accessing object properties, calculating values, in-memory filtering.
Checking whether an object satisfies an expression:
Expression e = Artist.NAME.in("John", "Bob"); Artist artist = ... if(e.match(artist)) { ... }
Reading property value:
String name = Artist.NAME.path().evaluate(artist);
Filtering a list of objects:
Expression e = Artist.NAME.in("John", "Bob"); List<Artist> unfiltered = ... List<Artist> filtered = e.filterObjects(unfiltered);
Current limitation of in-memory expressions is that no collections are permitted in the property path.
EJBQL is a textual query language that can be used with Cayenne. In some situations, it is convenient to be able to convert Expression instances into EJBQL. Expressions support this conversion. An example is shown below.
String serial = ... Expression e = Pkg.SERIAL.eq(serial); List<Object> params = new ArrayList<Object>(); EJBQLQuery query = new EJBQLQuery("SELECT p FROM Pkg p WHERE " + e.toEJBQL(params,"p"); for(int i=0;i<params.size();i++) { query.setParameter(i+1, params.get(i)); }
This would be equivalent to the following purely EJBQL querying logic;
EJBQLQuery query = new EJBQLQuery("SELECT p FROM Pkg p WHERE p.serial = ?1"); query.setParameter(1,serial);