Chapter 7. Expressions

Table of Contents

Expressions Overview
Path Expressions
Creating Expressions from Strings
Creating Expressions via API
Evaluating Expressions in Memory
Translating Expressions to EJBQL

Expressions Overview

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.

Path Expressions

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.

Creating Expressions from Strings

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.

Note

A formal definition of the expression grammar is provided in Appendix C

Creating Expressions via API

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); 

Note

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.

Evaluating Expressions in Memory

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);

Note

Current limitation of in-memory expressions is that no collections are permitted in the property path.

Translating Expressions to EJBQL

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);