Jump to: navigation, search

Difference between revisions of "EclipseLink/Release/2.1.0/JPAQueryEnhancements"

(New page: = Query Casting = {{bug|259266}} <table><tr> <td width="75%" valign="top"> This shows examples on how to use EclipseLink to define queries on inheritance hierarchies with down casting to ...)
 
 
(15 intermediate revisions by 2 users not shown)
Line 1: Line 1:
= Query Casting =
+
__TOC__
{{bug|259266}}
+
<table><tr>
+
<td width="75%" valign="top">
+
  
This shows examples on how to use EclipseLink to define queries on inheritance hierarchies with down casting to specific classes.
+
== FUNC ==
 +
 
 +
A new JPQL keyword has been added to JPQL to support native database functions ({{bug|300512}}).
 +
 
 +
To call the DB function MyFunc(a, b, c) use FUNC('MyFunc', a, b, c)
 +
 
 +
=== Example: Year function ===
 +
:[http://www.eclipse.org/forums/index.php?t=msg&th=124404&start=0&S=6eb7686393e997064947f7e51e5aca88 YEAR()]
 +
<source lang="java">
 +
SELECT FUNC('YEAR', whenRegistered) Y, COUNT(registration.id) C FROM registration GROUP BY Y
 +
</source>
 +
 
 +
=== Example: Oracle NVL===
 +
EclipseLink's ExpressionOperator.ifNull will generate "NVL". How can this be used in JPQL and Criteria defined queries?
 +
<source lang="java">
 +
// on Oracle
 +
SELECT FUNC('NVL', e.firstName, 'NoFirstName'), func('NVL', e.lastName, 'NoLastName') FROM Employee e
 +
// on MySQL
 +
SELECT FUNC('IFNULL', e.firstName, 'NoFirstName'), func('IFNULL', e.lastName, 'NoLastName') FROM Employee e
 +
</source>
 +
 
 +
=== Example: Oracle Spatial ===
 +
 
 +
: [http://www.eclipse.org/forums/index.php?S=645995efb907d1f81c9ac84afe05ef9f&t=msg&th=126379 PostGIS Intersects (forum)]
 +
 
 +
<source lang="java">
 +
SELECT a FROM Asset a, Geography selectedGeography
 +
WHERE selectedGeography.id = :id AND a.id IN :id_list
 +
AND FUNC('ST_Intersects', a.geometry, selectedGeography.geometry) = 'TRUE'
 +
</source>
 +
 
 +
: Oracle Spatial
 +
<source lang="java">
 +
SELECT ss FROM SimpleSpatial ss WHERE FUNC('mdsys.sdo_relate', ss.jGeometry, :otherGeometry, :params) = 'TRUE' ORDER BY ss.id ASC
 +
</source>
 +
 
 +
== Batching reading using exist and IN ==
 +
({{bug|298985}})
 +
 
 +
Typically batch reading uses a join of the batch query to the source query.
 +
 
 +
This join has some issues:
 +
* If a m-1 or m-m, join causes duplicate rows to be selected, (a distinct is used to filter rows for m-1, but duplicates are returned for m-m as join table info is needed).
 +
* DISTINCT does not work with LOBs.
 +
* DISTINCT may be less efficient on some databases than alternatives.
 +
* Does not work well with cursors.
 +
* Needs verification if works with pagination.
 +
 
 +
Two new configuration options are introduced in EclipseLink 2.1.  One such option is to use an EXISTS with a sub-select instead of a JOIN.  This should not result in duplicate rows, so avoid issues with DISTINCT.
 +
 
 +
Another option is to load the target objects using a IN clause containing the source query object's primary keys.  This would also work with cursors, but as the limitation of requiring custom SQL support for composite primary keys, and produces a large dynamic SQL query.
 +
 
 +
A new BatchFetchType enum will be define and the usesBatchReading flag will enhance to setBatchFetch allowing for JOIN, EXISTS or IN.  This option will also be added to ObjectLevelReadQuery, rolling up the current 4 batch reading properties into a new BatchFetchPolicy, also moving them up from ReadAllQuery to allow ReadObjectQuery to also specify nested batched attributes.  A new BatchFetch annotation and query hint will be added.
 +
 
 +
The EXISTS option will operate very similar to the existing JOIN batching, just putting the same join condition inside a sub-select.  Although it should have be straight forward, I hit several issues with our current sub-selects support that I had to debug and fix.  This also lead to discovering some issues with our JPQL support, that also needed to be fixed.  EXISTS supports all of the mappings the same as JOIN, but does not require a distinct.  m-1 will not use a DISTINCT by default for EXISTS (even though it would avoid duplicates, as the reason for using the EXISTS is normally to avoid the distinct), but the distinct can be enabled on the originating query if desired.  EXISTS will still select duplicates for m-m, and not work well with cursors.
 +
 
 +
The IN option will query a batch of the target objects using an SQL IN clause containing the key values.  For a 1-1 the foreign keys from the source rows will be extracted, if these are the primary keys of the target, they will first be filtered by checking the cache for each object.  The remaining keys will be split into batches of a query configurable size, defaulting to 256.  For composite keys the multi array SQL syntax for ((key1, key2), (...)) IN ((:key1, :key2), (...)) will be used.  Only some databases support this syntax, so composite primary keys will only be supported on some databases.  For 1-m or m-m the source rows do not contain the primary keys of the target objects.  The IN will still be supported, but will be based on join the source query as in JOIN (or maybe EXISTS?) for the set of keys.  For cursors the IN size will default to the cursor pageSize, and each cursor page will be processed separately.
 +
 
 +
== Query Casting ==
  
=== Overview ===
 
 
JPA/ORM developers will use this query feature to query across attributes in subclasses.  This feature is available in JPQL, EclipseLink Expressions and Criteria API.
 
JPA/ORM developers will use this query feature to query across attributes in subclasses.  This feature is available in JPQL, EclipseLink Expressions and Criteria API.
 +
 +
These examples show on how to use EclipseLink to define queries on inheritance hierarchies with down casting to specific classes ({{bug|259266}}).
 +
  
 
=== JPA2.0 Type ===
 
=== JPA2.0 Type ===
Line 84: Line 141:
 
The changes as listed above in bold.
 
The changes as listed above in bold.
  
</td>
 
<td style="background-color: #cff8d9; border-width: 1px; border-style: solid; border-color: #999999; padding: 10px;">
 
__TOC__
 
</td>
 
</tr>
 
</table>
 
  
=== Single Class Results ===
 
  
While this may seem obvious it often important to point out all potential solutions. If the query you are executing will only be returning a single type from the inheritance hierarchy then it is important that that be the target type of the query. This will allow you to access all mapped attributes in this class and its mapped parent classes.
+
== Query Keys in Queries ==
  
=== Accessing un-mapped attributes using QueryKey ===
+
As of [[EclipseLink/Release/2.1.0|EclipseLink 2.1]] it is now possible to reference manually defined query keys within your JPA queries. Prior to this release these query keys could only referenced in native queries using expressions.
  
When your inheritance hierarchy leverages a common table for multiple mapped classes in the hierarchy it is possible to query for attributes that are not visible in the class you are querying for through the use of query keys.
+
By default EclipseLink creates query keys for all of your mapped attributes but in some scenarios users find it beneficial to add their own.
 
+
'''Example'''
+
 
+
In this example we'll map a simple inheritance hierarchy of class A having two subclasses B and C. Each class will have its own value and they will all be mapped to a single table.
+
  
 
<source lang="java">
 
<source lang="java">
@Entity
+
public class EmployeeCustomizer implements DescriptorCustomizer {
@Table(name="DOWNCAST_SIMPLE_A")
+
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
+
@DiscriminatorColumn(name="INH_TYPE",discriminatorType=DiscriminatorType.CHAR)
+
public abstract class A {
+
@Id
+
private int id;
+
+
private String aValue;
+
  
// accessor methods
+
    @Override
}
+
    public void customize(ClassDescriptor descriptor) throws Exception {
 +
        // Direct (basic) query for the Oracle DB's ROWID value
 +
        descriptor.addDirectQueryKey("rowId", "ROWID");
  
@Entity
+
        // 1:M query accessing all projects which have a M:1 teamLeader reference to this employee
@DiscriminatorValue("B")
+
        // this can also be used to allow querying of large collections not wanted mapped
public class B  extends A{
+
        OneToManyQueryKey projectsQueryKey = new OneToManyQueryKey();
 +
        projectsQueryKey.setName("leadProjects");
 +
        projectsQueryKey.setReferenceClass(Project.class);
 +
        ExpressionBuilder builder = new ExpressionBuilder();
 +
        projectsQueryKey.setJoinCriteria(builder.getField("PROJECT.LEADER_ID").equal(builder.getParameter("EMPLOYEE.EMP_ID")));
 +
        descriptor.addQueryKey(projectsQueryKey);
 +
    }
  
private String bValue;
 
 
// accessor methods
 
 
}
 
}
 
@Entity
 
@DiscriminatorValue("C")
 
public class C extends A {
 
 
private String cValue;
 
 
// accessor methods
 
}
 
 
 
</source>
 
</source>
  
Based on this mapped entity model the generated schema looks like:
+
Then the query key can be references in the query in the same way any mapped attribute is.
 
+
<pre>
+
CREATE TABLE DOWNCAST_SIMPLE_A (
+
ID NUMBER(10) NOT NULL,
+
INH_TYPE VARCHAR2(31) NULL,
+
AVALUE VARCHAR2(255) NULL,
+
BVALUE VARCHAR2(255) NULL,
+
CVALUE VARCHAR2(255) NULL,
+
PRIMARY KEY (ID))
+
</pre>
+
 
+
Now to build a heterogenous query for '''A''' using '''bValue''' and '''cValue''' I need to define query keys on A to make this fields visible.
+
  
 
<source lang="java">
 
<source lang="java">
// Configure the use of a customizer on the entity class
+
TypedQuery<Employee> query = em.createQuery("SELECT e FROM Employee e WHERE e.leadProjects IS NOT EMPTY", Employee.class);
@Customizer(ACustomizer.class)
+
return query.getResultList();
public abstract class A {
+
 
+
 
+
// The customizer adds the direct query keys
+
public class ACustomizer implements DescriptorCustomizer {
+
 
+
public void customize(ClassDescriptor descriptor) throws Exception {
+
descriptor.addDirectQueryKey("bValue", "BVALUE");
+
descriptor.addDirectMapping("cValue", "CVALUE");
+
}
+
 
+
}
+
 
</source>
 
</source>
 
Now you can write your query:
 
 
<source lang="java">
 
ReadAllQuery raq = new ReadAllQuery(A.class);
 
ExpressionBuilder eb = raq.getExpressionBuilder();
 
raq.setSelectionCriteria(eb.get("aValue").like("A%").and(eb.get("bValue").like("bValue")).and(eb.get("cValue").like("CVALUE")));
 
 
// Wrap in JPA Query
 
Query query = JpaHelper.createQuery(raq, em);
 
 
// Execute Query
 
List<A> results = query.getResultList();
 
</source>
 
 
The resulting SQL appears as:
 
 
<pre>
 
SELECT ID, INH_TYPE, AVALUE, CVALUE, BVALUE FROM DOWNCAST_SIMPLE_A WHERE (((AVALUE LIKE ?) AND (BVALUE LIKE ?)) AND (CVALUE LIKE ?))
 
</pre>
 
 
=== Querying JOINED Hierarchies using Joining ===
 
 
It is possible to query joined hierarchies as well:
 
 
<source lang="java">
 
Query query = em.createQuery("Select a from JoinedA a, JoinedB b, JoinedC c WHERE (b.bValue LIKE 'B%' and b = a) OR (c.cValue LIKE 'C%' and c = a)");
 
// Execute Query
 
List<JoinedA> results = query.getResultList();
 
</source>
 
 
The result SQL is:
 
 
<pre>
 
[EL Fine]: Connection(27978063)--SELECT DISTINCT t0.INH_TYPE FROM DOWNCAST_JOINED_C t4, DOWNCAST_JOINED_A t3, DOWNCAST_JOINED_A t2, DOWNCAST_JOINED_B t1, DOWNCAST_JOINED_A t0 WHERE ((((t1.BVALUE LIKE ?) AND (t2.ID = t0.ID)) OR ((t4.CVALUE LIKE ?) AND (t2.ID = t3.ID))) AND (((t1.ID = t0.ID) AND (t0.INH_TYPE = ?)) AND ((t4.ID = t3.ID) AND (t3.INH_TYPE = ?))))
 
bind => [B%, C%, B, C]
 
</pre>
 
 
The challenge here is that the joining limits the results incorrectly for the OR condition and fails in some test cases.
 
 
=== Querying JOINED Hierarchies using IN ===
 
 
Another solution is to use an IN operator on each subclass you are interested in against a single part PK:
 
 
<source lang="java">
 
Select a from JoinedA a WHERE
 
                        a.id IN (SELECT b.id FROM JoinedB b WHERE b.bValue LIKE 'B%')
 
                        OR
 
                        a.id IN (SELECT c.id FROM JoinedC c WHERE c.cValue LIKE 'C%')
 
</source>
 
 
the result SQL for this scenario is:
 
 
<pre>
 
[EL Fine]: Connection(14707008)--SELECT DISTINCT t0.INH_TYPE FROM DOWNCAST_JOINED_A t0 WHERE (t0.ID IN (SELECT t1.ID FROM DOWNCAST_JOINED_B t2, DOWNCAST_JOINED_A t1 WHERE ((t2.BVALUE LIKE ?) AND ((t2.ID = t1.ID) AND (t1.INH_TYPE = ?)))) OR t0.ID IN (SELECT t3.ID FROM DOWNCAST_JOINED_C t4, DOWNCAST_JOINED_A t3 WHERE ((t4.CVALUE LIKE ?) AND ((t4.ID = t3.ID) AND (t3.INH_TYPE = ?)))))
 
bind => [B%, B, C%, C]
 
[EL Fine]: Connection(14707008)--SELECT t0.ID, t0.INH_TYPE, t0.AVALUE, t1.ID, t1.BVALUE FROM DOWNCAST_JOINED_A t0, DOWNCAST_JOINED_B t1 WHERE ((t0.ID IN (SELECT t2.ID FROM DOWNCAST_JOINED_B t3, DOWNCAST_JOINED_A t2 WHERE ((t3.BVALUE LIKE ?) AND ((t3.ID = t2.ID) AND (t2.INH_TYPE = ?)))) OR t0.ID IN (SELECT t4.ID FROM DOWNCAST_JOINED_C t5, DOWNCAST_JOINED_A t4 WHERE ((t5.CVALUE LIKE ?) AND ((t5.ID = t4.ID) AND (t4.INH_TYPE = ?))))) AND ((t1.ID = t0.ID) AND (t0.INH_TYPE = ?)))
 
bind => [B%, B, C%, C, B]
 
[EL Fine]: Connection(14707008)--SELECT t0.ID, t0.INH_TYPE, t0.AVALUE, t1.ID, t1.CVALUE FROM DOWNCAST_JOINED_A t0, DOWNCAST_JOINED_C t1 WHERE ((t0.ID IN (SELECT t2.ID FROM DOWNCAST_JOINED_B t3, DOWNCAST_JOINED_A t2 WHERE ((t3.BVALUE LIKE ?) AND ((t3.ID = t2.ID) AND (t2.INH_TYPE = ?)))) OR t0.ID IN (SELECT t4.ID FROM DOWNCAST_JOINED_C t5, DOWNCAST_JOINED_A t4 WHERE ((t5.CVALUE LIKE ?) AND ((t5.ID = t4.ID) AND (t4.INH_TYPE = ?))))) AND ((t1.ID = t0.ID) AND (t0.INH_TYPE = ?)))
 
bind => [B%, B, C%, C, C]
 
</pre>
 

Latest revision as of 09:56, 28 March 2011

FUNC

A new JPQL keyword has been added to JPQL to support native database functions (bug 300512).

To call the DB function MyFunc(a, b, c) use FUNC('MyFunc', a, b, c)

Example: Year function

YEAR()
SELECT FUNC('YEAR', whenRegistered) Y, COUNT(registration.id) C FROM registration GROUP BY Y

Example: Oracle NVL

EclipseLink's ExpressionOperator.ifNull will generate "NVL". How can this be used in JPQL and Criteria defined queries?

// on Oracle
SELECT FUNC('NVL', e.firstName, 'NoFirstName'), func('NVL', e.lastName, 'NoLastName') FROM Employee e
// on MySQL
SELECT FUNC('IFNULL', e.firstName, 'NoFirstName'), func('IFNULL', e.lastName, 'NoLastName') FROM Employee e

Example: Oracle Spatial

PostGIS Intersects (forum)
SELECT a FROM Asset a, Geography selectedGeography
WHERE selectedGeography.id = :id AND a.id IN :id_list
AND FUNC('ST_Intersects', a.geometry, selectedGeography.geometry) = 'TRUE'
Oracle Spatial
SELECT ss FROM SimpleSpatial ss WHERE FUNC('mdsys.sdo_relate', ss.jGeometry, :otherGeometry, :params) = 'TRUE' ORDER BY ss.id ASC

Batching reading using exist and IN

(bug 298985)

Typically batch reading uses a join of the batch query to the source query.

This join has some issues:

  • If a m-1 or m-m, join causes duplicate rows to be selected, (a distinct is used to filter rows for m-1, but duplicates are returned for m-m as join table info is needed).
  • DISTINCT does not work with LOBs.
  • DISTINCT may be less efficient on some databases than alternatives.
  • Does not work well with cursors.
  • Needs verification if works with pagination.

Two new configuration options are introduced in EclipseLink 2.1. One such option is to use an EXISTS with a sub-select instead of a JOIN. This should not result in duplicate rows, so avoid issues with DISTINCT.

Another option is to load the target objects using a IN clause containing the source query object's primary keys. This would also work with cursors, but as the limitation of requiring custom SQL support for composite primary keys, and produces a large dynamic SQL query.

A new BatchFetchType enum will be define and the usesBatchReading flag will enhance to setBatchFetch allowing for JOIN, EXISTS or IN. This option will also be added to ObjectLevelReadQuery, rolling up the current 4 batch reading properties into a new BatchFetchPolicy, also moving them up from ReadAllQuery to allow ReadObjectQuery to also specify nested batched attributes. A new BatchFetch annotation and query hint will be added.

The EXISTS option will operate very similar to the existing JOIN batching, just putting the same join condition inside a sub-select. Although it should have be straight forward, I hit several issues with our current sub-selects support that I had to debug and fix. This also lead to discovering some issues with our JPQL support, that also needed to be fixed. EXISTS supports all of the mappings the same as JOIN, but does not require a distinct. m-1 will not use a DISTINCT by default for EXISTS (even though it would avoid duplicates, as the reason for using the EXISTS is normally to avoid the distinct), but the distinct can be enabled on the originating query if desired. EXISTS will still select duplicates for m-m, and not work well with cursors.

The IN option will query a batch of the target objects using an SQL IN clause containing the key values. For a 1-1 the foreign keys from the source rows will be extracted, if these are the primary keys of the target, they will first be filtered by checking the cache for each object. The remaining keys will be split into batches of a query configurable size, defaulting to 256. For composite keys the multi array SQL syntax for ((key1, key2), (...)) IN ((:key1, :key2), (...)) will be used. Only some databases support this syntax, so composite primary keys will only be supported on some databases. For 1-m or m-m the source rows do not contain the primary keys of the target objects. The IN will still be supported, but will be based on join the source query as in JOIN (or maybe EXISTS?) for the set of keys. For cursors the IN size will default to the cursor pageSize, and each cursor page will be processed separately.

Query Casting

JPA/ORM developers will use this query feature to query across attributes in subclasses. This feature is available in JPQL, EclipseLink Expressions and Criteria API.

These examples show on how to use EclipseLink to define queries on inheritance hierarchies with down casting to specific classes (bug 259266).


JPA2.0 Type

Extensions to the expression framework to limit the results to those of a specific subclass have already been implemented as part of the JPA 2.0 effort. Expression.type(Class) is available in the expression framework and equivalent functionality is available in JPQL.

e.g. "select p from Employee e join e.projects p where type(p) = LargeProject" can be used to retrieve all the LargeProjects (Subclass of Project) from Employee.

JPQL Extensions to use Downcast

JPQL is extended to cast in the FROM clause. The format of this will use the keyword "TREAT" and be part of the join clause. The following is an example:

select e from Employee e join TREAT(e.projects AS LargeProject) lp where lp.budget = value

Criteria API

JPA Criteria API already provides a casting operator. It is Expression.as(type).

As it is defined by JPA 2.0, Expression.as(type) does a simple cast that allows matching of types within the generics.

EclipseLink 2.1 extends criteria API to allow a cast using Expression.as(type). The as method has been extended to check the hierarchy and if type is a subclass of the type for the expression that as is being called on a cast will be implemented. Here is a criteria query that will do a cast:

Root<Employee> empRoot = cq1.from(getEntityManagerFactory().getMetamodel().entity(Employee.class));
Join<Employee, Project> join = empRoot.join("projects");
Path exp = ((Path)join.as(LargeProject.class)).get("budget");
cq1.where(qb1.equal(exp, new Integer(1000)) );

Calling a cast on a JOIN node will permanently alter that node. i.e. In the example above, after calling join.as(LargeProject.class), join will refer to a LargeProject.

EclipseLink Expression Support for Downcast

We will implement Expression.as(Class). The following is an example of how one could use it:

       ReadAllQuery raq = new ReadAllQuery(Employee.class);
       Expression criteria = raq.getExpressionBuilder().anyOf("projects").as(LargeProject.class).get("budget").greaterThan(100);
       raq.setSelectionCriteria(criteria);
       List resultList = query.getResultList();

In this query Employee has a xToMany mapping to Project. LargeProject is a subclass of Project and the "budget" attribute is contained on LargeProject.

  • An exception will be thrown at query execution time if the class that is cast to is not a subclass of the class of the query key being cast.
  • Casts are only allowed on ObjectExpressions (QueryKeyExpression and ExpressionBuilder). The parent expression of a cast must be an ObjectExpression
  • Casts use the same outer join settings as the ObjectExpression they modify
  • Casts modify their parent expression. As a result, when using a cast with a parallel expression, you must use a new instance of the parent expression.
  • Casting is not supported for TablePerClass Inheritance
  • It is prudent to do a check for type in a query that does a cast.
    • The following select f from Foo f join cast(f.bars, BarSubclass) b where b.subclassAttribute = "value"
    • Should be written as: select f from Foo f join cast(f.bars, BarSubclass) b where type(b) = BarSubclass And b.subclassAttribute = "value" by users that wish to enforce the type.
    • EclipseLink will automatically append type information for cases where the cast results in a single type, but for classes in the middle of a hierarchy, no type information will not be appended to the SQL

Example SQL

The following query:

Select e from Employee e join e.projects project

Will currently produce the following sql:

SELECT <select list>
FROM CMP3_EMPLOYEE t1 LEFT OUTER JOIN CMP3_DEPT t0 ON (t0.ID = t1.DEPT_ID), CMP3_EMP_PROJ t4, CMP3_PROJECT t3, CMP3_SALARY t2 
WHERE ((t2.EMP_ID = t1.EMP_ID) AND ((t4.EMPLOYEES_EMP_ID = t1.EMP_ID) AND (t3.PROJ_ID = t4.projects_PROJ_ID)))


If we augment a select criteria like the following

Expression criteria = project.as(LargeProject.class).get("budget").greaterThan(100);
raq.setSelectionCriteria(criteria);

The following SQL will be produced:

SELECT <select list> 
FROM CMP3_PROJECT t3 LEFT OUTER JOIN CMP3_LPROJECT t4 ON (t4.PROJ_ID = t3.PROJ_ID),CMP3_EMPLOYEE t1 LEFT OUTER JOIN CMP3_DEPT t0 ON (t0.ID = t1.DEPT_ID), CMP3_EMP_PROJ t5, CMP3_SALARY t2 
WHERE (((t4.BUDGET > ?) AND (t2.EMP_ID = t1.EMP_ID)) AND ((t5.EMPLOYEES_EMP_ID = t1.EMP_ID) AND (t3.PROJ_ID = t5.projects_PROJ_ID)))
bind => [100.0]

The changes as listed above in bold.


Query Keys in Queries

As of EclipseLink 2.1 it is now possible to reference manually defined query keys within your JPA queries. Prior to this release these query keys could only referenced in native queries using expressions.

By default EclipseLink creates query keys for all of your mapped attributes but in some scenarios users find it beneficial to add their own.

public class EmployeeCustomizer implements DescriptorCustomizer {
 
    @Override
    public void customize(ClassDescriptor descriptor) throws Exception {
        // Direct (basic) query for the Oracle DB's ROWID value
        descriptor.addDirectQueryKey("rowId", "ROWID");
 
        // 1:M query accessing all projects which have a M:1 teamLeader reference to this employee
        // this can also be used to allow querying of large collections not wanted mapped
        OneToManyQueryKey projectsQueryKey = new OneToManyQueryKey();
        projectsQueryKey.setName("leadProjects");
        projectsQueryKey.setReferenceClass(Project.class);
        ExpressionBuilder builder = new ExpressionBuilder();
        projectsQueryKey.setJoinCriteria(builder.getField("PROJECT.LEADER_ID").equal(builder.getParameter("EMPLOYEE.EMP_ID")));
        descriptor.addQueryKey(projectsQueryKey);
    }
 
}

Then the query key can be references in the query in the same way any mapped attribute is.

TypedQuery<Employee> query = em.createQuery("SELECT e FROM Employee e WHERE e.leadProjects IS NOT EMPTY", Employee.class);
return query.getResultList();