Wednesday, January 30, 2008

Customizing EclipseLink JPA/ORM Relationship Joins

Sometimes you want to add join criteria in a one-to-one or one-to-many mapping with an arbitrary constraint. There are a few situations in which you'd want to do this but I won't go into that here. But hopefully when it happens to you you'll know how to deal with it in EclipseLink.

As an example, let's consider a two class object model with classes A and B. A has a one-to-one with B based on a foreign key relationship. Let's say that we only ever want the one-to-one to B to resolve to an object if the B's status is "active". What we need to do is ensure that the SQL query for B includes this constraint.

Behinds the Scenes

When you define a one-to-one or one-to-many mapping in either JPA (e.g., @OneToOne) or using the EclipseLink native mapping format, behind the scenes EclipseLink builds a Mapping object. In the Mapping object is an Expression that defines the selection criteria that will be used to resolve the object or objects that are the target of the mapping. At runtime EclipseLink generates SQL from this Expression. So the key to changing the SQL used in the join is to augment or change in some way this Expression.

Making it Work

To get our hands on the Expression of the Mapping object we need to define a Descriptor Customizer. In my TopLink blog I described how to define a TopLink customizer to make a class read-only. We'll take the same approach here with EclipseLink. There are only two things you need to do to get this working:
  1. Create a customizer class that implements org.eclipse.persistence.internal.sessions.factories.DescriptorCustomizer
  2. Associate the customizer with class A in the persistence.xml
Here's a customizer that adds a constraint on the "status" property of the target object (error handling left out for brevity):

public class ACustomizer implements DescriptorCustomizer {

public void customize(ClassDescriptor desc) {
OneToOneMapping mapping = (OneToOneMapping) desc.getMappingForAttributeName("b");
Expression origExp = mapping.buildSelectionCriteria();
ExpressionBuilder b = origExp.getBuilder();
Expression constantExp = b.get("status").equal("active");
Expression newExp= origExp.and(constantExp);
mapping.setSelectionCriteria(newExp);
}
}
In the customizer we get the ClassDescriptor associated with class A that EclipseLink built after processing all annotations and mapping files. From it we get the OneToOneMapping for the "b" attribute. And then we get the selection criteria expression that is based on the mappings. From the Expression we get the ExpressionBuilder and proceed to build a new Expression that we "and" with the original Expression. To finish up we set the selection criteria of the OneToOneMapping to the new Expression.

To enable the use of the customizer you'll need to add a property to your persistence.xml.

<properties>
<property name="eclipselink.descriptor.customizer.A"
value="model.ACustomizer"/>

When I setup this customizer in my example I see the following SQL in the console when I try to read the B associated with an A:

SELECT ID, STATUS FROM B WHERE ((ID = 2) AND (STATUS = 'active'))

Cool--just what I wanted!

--Shaun

1 comment:

Austin said...

This is just what I was looking for as it is similar to Hibernate's where condition on a relationship. Do join know if they are adding this in to the orm.xml file in the future?

Next, I need to get batch reading to mimic Hibernate's feth="subselect" behavior. However, when I set usesBatchReading to true in the customizer it doesn't seem to have any effect.

Thanks,
Austin