Tuesday, April 28, 2009

Custom ORM with OpenJPA


While JPA provides a very rich set of mapping constructs, it doesn't offer much in terms of customization in situations where a domain model cannot be directly or easily mapped to the database using those constructs. This is more typically an issue when an existing application is being converted from some persistence mechanism such as a JDBC-based DAO to JPA and its domain classes cannot suffer more than minimal modifications.

For example, let's say there is an application XYZ which has a domain class named User. The User class contains an email address field which is of type string. Application XYZ expects email addresses in the form user@domain. However, the database table backing the User class stores the components of an email address in separate columns, EMAIL_USER and EMAIL_DOMAIN. The persistence mechanism used in XYZ (for example, JDBC) performs the necessary transforms in a DAO layer to compose the database columns to a single string value of the expected format and vice versa.

Converting this application to JPA would typically involve mapping the individual persistent fields for user and domain to the individual database columns and then wrappering them with the logic formerly provided by the DAO to do the transforms. While this is fairly straight forward, it requires modifications to the domain object and couples the transform logic to the entity. These types of changes can be invasive and undesirable in an existing application.

OpenJPA has an elegant solution for handling these types of mappings: mapping strategies. Custom mapping strategies can be applied to persistent fields using OpenJPA’s @org.apache.openjpa.persistence.jdbc.Strategy annotation. Strategies are most simply created by subclassing the org.apache.openjpa.jdbc.meta.strats.AbstractValueHandler class. Although, for complex mappings you may decide to provide a full implementation class by implementing the org.apache.openjpa.jdbc.meta.ValueHandler interface. To implement a basic strategy you simply need to provide column mapping information and the methods to transform to and from a given type. The name of the custom strategy class is specified on the @Strategy annotation. At runtime, OpenJPA applies this strategy to the persistent field or property. This decouples the mapping and data transformation logic from the domain class. Below is a simple strategy for the multi-column email address mapping. It maps a single persistent field to two database columns and provides the transforms to get the data in the proper format to/from the domain model and database table.

EmailAddressStrategy.java

package strategies;

import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.ValueMapping;
import org.apache.openjpa.jdbc.meta.strats.AbstractValueHandler;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ColumnIO;
import org.apache.openjpa.meta.JavaTypes;

public class EmailAddressStrategy extends AbstractValueHandler {

// Create the custom column mappings for this strategy
public Column[] map(ValueMapping vm, String name, ColumnIO io,
boolean adapt) {

Column user = new Column();
user.setName("EMAIL_USER");
user.setJavaType(JavaTypes.STRING);

Column domain = new Column();
domain.setName("EMAIL_DOMAIN");
domain.setJavaType(JavaTypes.STRING);
return new Column[]{ user, domain };
}

// Transform the email address value to its individual datastore column values.
public Object toDataStoreValue(ValueMapping vm, Object val,
JDBCStore store) {
if (val == null)
return null;

// Split the user and domain components into distinct values
if (val instanceof String) {
String strVal = (String)val;
String user = strVal;
String domain = null;
int atIndex = user.indexOf('@');
if (atIndex != -1) {
user = strVal.substring(0, atIndex);
domain = strVal.substring(atIndex+1);
}
return new Object[] { user, domain };
}
return val.toString();
}

// Transform the separate datastore values to an email address.
public Object toObjectValue(ValueMapping vm, Object val) {
if (val == null)
return null;

if (val instanceof Object[]) {
String user = null;
String domain = null;
Object[] objArray = (Object[])val;
if (objArray.length > 0)
user = objArray[0].toString();
if (objArray.length > 1)
domain = objArray[1].toString();
return user + "@" + domain;
}
return val.toString();
}
}

User.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.apache.openjpa.persistence.jdbc.Strategy;

@Entity
public class User {

@Id
@GeneratedValue
private int id;

// Use email address strategy on this persistent field
@Strategy("strategies.EmailAddressStrategy")
private String emailAddress;

public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}

public String getEmailAddress() {
return emailAddress;
}
}

In short, if you find that JPA's mapping constructs are not quite adequate for a particular mapping, creating your own mapping strategy may be just the ticket.

-Jeremy

No comments: