Hello,
One of the use cases in our existing EOF code base is that we have exists
qualifiers (the functional equivalent of an Expression in Cayenne) that are
rooted on the destination entity of a relationship path. For example:
// This is rooted on Painting entity
Expression subExp = exp("name like 'G%' or name like ‘A%’”);
// This is rooted on the Artist parent entity
Expression exp = new Exists("paintings", subExp);
The relationship path is “paintings” has Painting as the destination entity.
The sub expression "name like 'G%' or name like ‘A%’” is rooted on the
destination entity Painting. So name here refers to the name of the painting,
not the name of the artist.
The expression exp can be used to get the Artists that have a painting with
name starting with a letter G or A.
You apply in memory like this:
List<Artist> artists = ..;
List<Artist> artistsSubset = exp.filterObjects(artists);
When you print the expression exp it prints like this:
(paintings: name like 'G%' or name like ‘A%’)
It can also be used to fetch from the database and uses an EXISTS sub query
like this:
INFO: --- transaction started.
INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0
WHERE EXISTS (
SELECT t1.ID FROM PAINTING t1
WHERE ( t1.NAME LIKE ? OR t1.NAME LIKE ? )
AND ( t1.ARTIST_ID = t0.ID )
)
[bind: 1->NAME:'G%', 2->NAME:'A%']
INFO: === returned 1 row. - took 1 ms.
INFO: +++ transaction committed.
This is different from the ASTExists class in.
Summary
* The same expression can be used to filter objects in memory and to fetch from
the database.
* It takes a sub expression rooted on the destination entity of the
relationship path you specify.
I implemented it by subclassing the existing ASTExists class in Cayenne 5.0-M1
as shown below.
If anybody with more experience sees something terribly wrong or am missing an
override of an important method, please let me know. I only override the
constructor, the evaluateNode() and appendAsString() methods.
Thank you,
Ricardo Parada
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - -
package play.cay.utils.cayene.exp;
import java.io.IOException;
import java.util.Collection;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.parser.ASTExists;
import org.apache.cayenne.exp.parser.ASTObjPath;
import org.apache.cayenne.exp.path.CayennePath;
/**
* Custom EXISTS expression node:
*
* left child = path (to-one or to-many),
* right child = subExpression to apply to each element.
*/
public class Exists extends ASTExists {
private static final long serialVersionUID = 1L;
ASTObjPath path;
Expression subExp;
public Exists(ASTObjPath path, Expression subExp) {
// subExp uses keys rooted on the target entity of the path
// e.g. if path is "paintings" then subExp would be rooted on
// the Paintings entity, for example, "name like G%".
// In order to invoke the parent constructor we must first
// prefix every key in subExp with the relationship path
// e.g. "paintings.name like G%"
super( prefix(path.getPath(), subExp) );
// Keep relationship path and original subExp because we need
// them in the overrides to evaluateNode() and appendAsString()
this.path = path;
this.subExp = subExp;
}
public Exists(String path, Expression subExp) {
this(new ASTObjPath(path), subExp);
}
@Override
protected Object evaluateNode(Object o) throws Exception {
// This would evaluate the paintings path on the artist o
// and return a paintings java collection.
Object leftVal = path != null ? path.evaluate(o) : null;
if (leftVal == null) {
return Boolean.FALSE;
}
// If path is a to-many, e.g. (paintings: name like G%) then leftVal
// will be a java Collection containing the paintings.
if (leftVal instanceof Collection collection) {
// Iterate over every object in the collection, e.g. every painting
// and see if the subExp, e.g. name like G%, returns true on any of
// them.
for (Object childObj : collection) {
Object match = subExp.evaluate(childObj);
if (Boolean.TRUE.equals(match)) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
// To-one relationship, e.g. (address: city = 'Miami')
// then leftVal would be the address object and subExp
// would be city = 'Miami'
return subExp.evaluate(leftVal);
}
/**
* Override to print the relationship path and subExp passed into
* the constructor, e.g. (paintings: name like G% or name like A%)
* which conceptually means the parent entity Artist has a painting
* matching "name like G% or name like A%".
*/
@Override
public void appendAsString(Appendable out) throws IOException {
out.append("(");
path.appendAsString(out);
out.append(": ");
subExp.appendAsString(out);
out.append(")");
}
/**
* Utility helper method to prefix every key in subExpression with a
* relationship path. For example if subExp is "name like G%" and
* relPath is "paintings" then it returns a new expression for
* "pantings.name like G%".
*/
public static <T> Expression prefix(CayennePath relPath, Expression subExp)
{
return subExp.transform(o -> {
if (o instanceof CayennePath path) {
// e.g. "name" path becomes "paintings.name"
return relPath.dot(path);
}
return o;
});
}
}