Custom expression - Exists(relationshipPath, subExpression)
Ricardo Parada <[email protected]> Tue, 30 Sep 2025 19:13:44 -0400
| Newsgroups | gmane.comp.java.cayenne.user |
|---|---|
| Message-ID | <[email protected]> |
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 =3D exp("name like 'G%' or name like =E2=80=98A%=E2=80=99=
=E2=80=9D);
// This is rooted on the Artist parent entity
Expression exp =3D new Exists("paintings", subExp);=20
The relationship path is =E2=80=9Cpaintings=E2=80=9D has Painting as the =
destination entity. The sub expression "name like 'G%' or name like =
=E2=80=98A%=E2=80=99=E2=80=9D 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.=20
You apply in memory like this:
List<Artist> artists =3D ..;
List<Artist> artistsSubset =3D exp.filterObjects(artists);
When you print the expression exp it prints like this:
(paintings: name like 'G%' or name like =E2=80=98A%=E2=80=99)=20
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=20
WHERE EXISTS (
SELECT t1.ID FROM PAINTING t1=20
WHERE ( t1.NAME LIKE ? OR t1.NAME LIKE ? )=20
AND ( t1.ARTIST_ID =3D t0.ID )
)=20
[bind: 1->NAME:'G%', 2->NAME:'A%']
INFO: =3D=3D=3D 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.=20
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
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - =
- - - - - - - - - - - - - - - - - - - - -=20
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:
*=20
* left child =3D path (to-one or to-many),
* right child =3D subExpression to apply to each element.
*/
public class Exists extends ASTExists {
private static final long serialVersionUID =3D 1L;
ASTObjPath path;
Expression subExp;
=20
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%".=20
// 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) );
=20
// Keep relationship path and original subExp because we need
// them in the overrides to evaluateNode() and appendAsString()
this.path =3D path;
this.subExp =3D 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 =3D path !=3D null ? path.evaluate(o) : null;
if (leftVal =3D=3D null) {
return Boolean.FALSE;
}
// If path is a to-many, e.g. (paintings: name like G%) then =
leftVal=20
// 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.=20
for (Object childObj : collection) {
Object match =3D subExp.evaluate(childObj);
if (Boolean.TRUE.equals(match)) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
// To-one relationship, e.g. (address: city =3D 'Miami')
// then leftVal would be the address object and subExp
// would be city =3D '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;
});
}
}