Chapter 7. Expressions

Table of Contents

Expressions Overview
Path Expressions
Creating Expressions from Strings
Creating Expressions with API
Evaluating Expressions in Memory

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 understanding the semantics. A Cayenne expression can be represented as a String, which can be later converted to an expression object using Expression.fromString static method. Here is an example:

String expString = "name like 'A%' and price < 1000";
Expression exp = Expression.fromString(expString);

This particular expression may be used to match Paintings with names that start with "A" and a price 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 all 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%'

Named parameters. Expressions can have named parameters (names that start with "$"). 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 some examples:

Expression template = Expression.fromString("name = $name");
...
Map p1 = Collections.singletonMap("name", "Salvador Dali");
Expression qualifier1 = template.expWithParameters(p1);
...
Map p2 = Collections.singletonMap("name", "Monet");
Expression qualifier2 = template.expWithParameters(p2);

To create a named parameterized expression with a LIKE clause, SQL wildcards must be part of the values in the Map and not the expression string itself:

Expression template = Expression.fromString("name like $name");
...
Map p1 = Collections.singletonMap("name", "Salvador%");
Expression qualifier1 = template.expWithParameters(p1);

When matching on a relationship, parameters can be Persistent objects or ObjectIds:

Expression template = Expression.fromString("artist = $artist");
...
Artist dali = // asume we fetched this one already
Map p1 = Collections.singletonMap("artist", dali);
Expression qualifier1 = template.expWithParameters(p1);

Uninitialized parameters will be automatically pruned from expressions, so a user can omit some parameters when creating an expression from a parameterized template:

Expression template = Expression.fromString("name like $name and dateOfBirth > $date");
...
Map p1 = Collections.singletonMap("name", "Salvador%");
Expression qualifier1 = template.expWithParameters(p1);

// qualifier1 is now equals to "name like 'Salvador%'", the 'dateOfBirth' 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 can be used directly with null values. It is up to Cayenne to translate expressions with nulls to the valid SQL.

Note

A formal definition of all possible valid expressions in a form of JavaCC grammar is provided in Appendix C

Creating Expressions with API

Creating expressions from Strings is a powerful and dynamic approach, however a safer alternative is to use Java API. It provides some degree of compile-time checking of expressions validity. The API is cenetred around ExpressionFactory class, and the Expression class. ExpressionFactory contains a number of rather self-explanatory factory methods. We won't be going over all of them in detail, but will rather show a few general examples and some gotchas.

The following code recreates the expression from the previous chapter, but now using expression API:

// String expression: name like 'A%' and price < 1000
Expression e1 = ExpressionFactory.likeExp(Painting.NAME_PROPERTY, "A%");
Expression e2 = ExpressionFactory.lessExp(Painting.PRICE_PROPERTY, 1000);
Expression finalExp = e1.andExp(e2); 

This is more verbose than creating it from String, but it is also more resilient to the entity properties renaming and precludes semantic errors in the expression String.

Note

The last line in the example above shows how to create a new expression by "chaining" 2 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.

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 = ExpressionFactory.inExp(User.NAME_PROPERTY, "John", "Bob");
User user = ...
if(e.match(user)) {
   ...
}

Reading property value:

Expression e = Expression.fromString(User.NAME_PROPERTY);
String name = e.evaluate(user);

Filtering a list of objects:

Expression e = ExpressionFactory.inExp(User.NAME_PROPERTY, "John", "Bob");
List<User> unfiltered = ...
List<User> filtered = e.filterObjects(unfiltered);

Note

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