EclipseLink/Release/2.1.0/JPAQueryEnhancements

From Eclipsepedia

< EclipseLink‎ | Release‎ | 2.1.0
Revision as of 09:58, 11 June 2010 by Peter.krogh.oracle.com (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Query Casting

bug 259266

This shows examples on how to use EclipseLink to define queries on inheritance hierarchies with down casting to specific classes.

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.

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.

Contents

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.

Accessing un-mapped attributes using QueryKey

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.

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.

@Entity
@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
}
 
@Entity
@DiscriminatorValue("B")
public class B  extends A{
 
	private String bValue;
 
	// accessor methods
}
 
@Entity
@DiscriminatorValue("C")
public class C extends A {
 
	private String cValue;
 
	// accessor methods
}

Based on this mapped entity model the generated schema looks like:

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

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.

// Configure the use of a customizer on the entity class
@Customizer(ACustomizer.class)
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");
	}
 
}

Now you can write your query:

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

The resulting SQL appears as:

SELECT ID, INH_TYPE, AVALUE, CVALUE, BVALUE FROM DOWNCAST_SIMPLE_A WHERE (((AVALUE LIKE ?) AND (BVALUE LIKE ?)) AND (CVALUE LIKE ?))

Querying JOINED Hierarchies using Joining

It is possible to query joined hierarchies as well:

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

The result SQL is:

[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]

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:

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%')

the result SQL for this scenario is:

[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]