Bridge the gap between Struts and Hibernate

Extend Struts for a more object-oriented relationship with Hibernate

<!–

Summary
–>

Summary
Hibernate and Struts are currently among the most popular open source libraries on the market. Effectively, they are the default developer selections among competing libraries when building Java enterprise applications. Although they are often used in conjunction with one another, Hibernate was not primarily designed to be used with Struts, and Struts was released years before the birth of Hibernate. To put them to work together, some challenges remain. This article identifies some of the gaps between Struts and Hibernate, particularly related to object-oriented modeling. It also describes a solution for bridging these gaps that involves an extension to the Struts framework. All Web applications built upon Struts and Hibernate can derive benefit from this generic extension. (1,870 words; March 6, 2006)

<!–

–>

In the book Hibernate in Action (Manning, October 2004), authors Christian Bauer and Gavin King reveal the paradigm mismatch between the two worlds of the object-oriented domain model and the relational database. Hibernate does an excellent job at the persistence layer of gluing these paradigms together; however, a mismatch remains between the domain model (the Model-View-Controller model layer) and HTML pages (the MVC view layer). In this article, we examine this mismatch and consider an approach for resolving the disparity.

The paradigm mismatch rediscovered
Let’s look at a classic parent-child relationship example (illustrated in the code below): product and category. The Category class defines an identifier id of type Long and a property name of type String. The Product class also has an id of type Long as well as a property category of type Category, representing a many-to-one relationship with instances of the Category class (i.e., many Products can belong to one Category).

/**
* @hibernate.class table="CATEGORY"
*/
public class Category {
private Long id;

private String name;

/**
* @hibernate.id generator-class="native" column="CATEGORY_ID"
*/
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

/**
* @hibernate.property column="NAME"
*/
public String getName() {
return name;
}

public void setName(Long name) {
this.name = name;
}
}

/**
* @hibernate.class table="PRODUCT"
*/
public class Product {
private Long id;
private Category category;

/**
* @hibernate.id generator-class="native" column="PRODUCT_ID"
*/
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

/**
* @hibernate.many-to-one
* column="CATEGORY_ID"
* class="Category"
* cascade="none"
* not-null="false"
*/
public Category getCategory() {
return category;
}

public void setCategory(Category category) {
this.category = category;
}
}

It is desirable that a product be re-categorized, so our HTML view provides a drop-down list of all categories to which products can be assigned:

No Category Category 1 Category 2 Category 3

The categoryId field specifies the user selection of a Category. In Struts, we define ProductForm as an ActionForm subclass to collect the user’s inputs from the HTTP request:

public class ProductForm extends ActionForm {
private Long id;
private Long categoryId;
...
}

Here’s the mismatch: in the Product domain object, the category property is of type Category, whereas the ProductForm only has a categoryId property of type Long. This mismatch not only increases inconsistency, but also involves special code for converting between primitive type identifiers and associated objects.

Part of this discrepancy is due to the HTML form itself: it resembles a relational model, instead of an object-oriented model. The object-oriented versus relational mismatch in the persistence layer is addressed by object-relational mapping (O/RM). The similar mismatches in the view layer still remain. The key is finding a solution to make them work together seamlessly.

Struts capabilities and limitations
Fortunately, Struts is able to render and interpret nested object properties. The category drop-down list can be rewritten using the Struts page-construction (html) tag library:

property="category.id">
No Category

We assume that categories is a list of all the Category objects. Now we can change ProductForm to be more object-oriented by exchanging the categoryId property for a category property of type Category. This change will render the copying of property values between Product and ProductForm a trivial task, because they have mostly the same properties and types:

public class ProductForm extends ActionForm {
private Long id;
private Category category;
...
}

Once we have created and configured the rest of the Struts actions, the configuration, validators, the JavaServer Pages (JSP), and the data persistence layer with Hibernate, and start to test, we immediately run into a NullPointerException when accessing ProductForm.category.id. Of course! The category reference has not been set yet, and Hibernate also sets many-to-one associated objects to null if no value is in the corresponding database field. Struts requires all objects to be instantiated before rendering (when displaying a form) and populating (when submitting a form).

Let’s look at how we bridge this gap by using ActionForm.reset().

(Not so) notorious Struts ActionForm
During my first week using Struts, one of my main questions was why I had to maintain exactly the same two copies of properties and getter and setter methods between domain objects and ActionForm beans. This tedious exercise has become one of the major complaints within the Struts community.

In my opinion, ActionForm beans exist for good reasons. First, they can be distinguished from domain objects because they serve different roles. In the MVC pattern, domain objects are part of the model layer, while ActionForm beans are part of the view layer. Because the fields on the Webpages may differ from those in the database, some custom conversion is common. Second, the ActionForm.validate() function comes in handy for nonstandard validation rules not covered by the validator framework. Third, there may be other custom, view-specific behavior that we require, as shown below, but don’t want to implement as part of the domain layer, especially when a persistence framework manages domain objects.

Submitting a form
Let’s leverage one of ActionForm‘s built-in methods—reset()—to address the mismatch between the view and model layers. The reset() method is called before the ActionForm properties are populated by the controller servlet when handing a request. The method is typically used when checkbox fields must be explicitly set to false so unchecked checkboxes can be correctly recognized. reset() is also a perfect place for instantiating associated objects required during view rendering. The code will look like this:

public class ProductForm extends ActionForm {
private Long id;
private Category category;
...
public void reset(ActionMapping mapping, HttpServletRequest request)
{
super.reset( mapping, request );
if ( category == null ) { category = new Category(); }
}

}

Before Struts populates ProductForm with the user submitted values, it calls reset() so the category property will have a non-null reference. Please note that it is necessary to check category to see if it is null as explained next.

Editing a form
So far, I have addressed the problem that occurs when a form is submitted. What about when rendering a form? The html:select tag also expects a non-null reference, so we’re going to call the reset() method again before the form is rendered. We do that by adding a line in the action class. In our case, the EditProductAction:

public class EditProductAction extends Action {
public final ActionForward execute( ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response ) throws Exception
{
...
Product product = createOrLoadProduct();
ProductForm productForm = (ProductForm)form;
PropertyUtils.copyProperties( productForm, product );
productForm.reset( mapping, request );
...
}
}

I assume readers are familiar with Struts action classes and the Jakarta Commons Beanutils package. The createOrLoadProduct() method creates a new Product instance or loads an existing record from the database, depending on whether a create or modify action is being invoked. After productForm is populated, the productForm.category property value is set; therefore, it is ready for rendering. We must also ensure we don’t accidentally overwrite associated objects if they are indeed valid and loaded by Hibernate. So before the object is instantiated, we must check to see if it is null.

Because the reset() method is defined in ActionForm, we can generically rewrite the above code and move it into a superclass, e.g., CommonEditAction, that deals with Struts action routines:

...
Product product = createOrLoadProduct();
PropertyUtils.copyProperties( form, product );
form.reset( mapping, request );
...

If you want a read-only view of a form without editing it, either check to see if the associated object is null on the JSP page or copy the domain object to the ActionForm bean and call the ActionForm bean’s reset() method.

Saving a domain object
We solved the problem when submitting and rendering a form, so Struts is happy. What about Hibernate? When a user selects a null ID option—in our example, the “no category” option—and submits the form, productForm.category is a newly created Category instance with id equal to null. When the category property is copied to the product object and then persisted, Hibernate complains that product.category is a transient object and needs to be persisted first. Of course, we know it is empty and don’t want it to be persisted; so we need to set product.category to null before the product is persisted. We also don’t want to change the way Hibernate works; so we choose to clean up these temporary objects before they are copied to the domain object by adding a method in ProductForm:

public class ProductForm extends ActionForm {
private Long id;
private Category category;
...
public void reset(ActionMapping mapping, HttpServletRequest request) {
super.reset( mapping, request );
if ( category == null ) { category = new Category(); }
}

public void cleanupEmptyObjects() {
if ( category.getId() == null ) { category = null; }
}

}

We call the cleanupEmptyObjects() method in SaveProductAction right before the property values are copied from the ActionForm bean to the domain object; so if ProductForm.category is just the “placeholder” instance with a null ID, it will be set to null. Then ProductForm.category will be copied to the domain object, and the corresponding property in the domain object will also be set to null:

public class SaveProductAction extends Action {
public final ActionForward execute( ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response ) throws Exception
{
...
Product product = new Product();
((ProductForm)form).cleanupEmptyObjects();
PropertyUtils.copyProperties( product, form );
SaveProduct( product );
...
}
}

One-to-many relationships
I have not yet addressed the one-to-many relationship of Category to Product. We can add it to the Category metadata:

public class Category {
...
private Set products;
...

/**
* @hibernate.set
* table="PRODUCT"
* lazy="true"
* outer-join="auto"
* inverse="true"
* cascade="all-delete-orphan"
*
* @hibernate.collection-key
* column="CATEGORY_ID"
*
* @hibernate.collection-one-to-many
* class="Product"
*/

public Set getProducts() {
return products;
}

public void setProducts(Set products) {
this.products = products;
}
}

Note that setting the Hibernate cascade property to all-delete-orphan indicates that Hibernate should automatically persist all Product objects in the set when persisting the containing Category. It’s not usually necessary to persist child objects along with the parent object; often, it’s better to control child persistent operations separately. In our case, it is convenient to do so if we are allowing the user to edit the Category and its Products on the same page. Dealing with the contained set of Products is fairly straightforward:

public class CategoryForm extends ActionForm {
private Set productForms;
...
public void reset(ActionMapping mapping, HttpServletRequest request) {
super.reset( mapping, request );

for ( int i = 0; i
}

public void cleanupEmptyObjects() {
for ( Iterator i = productForms.iterator(); i.hasNext(); ) {
ProductForm productForm = (ProductForm) i.next();
productForm.cleanupEmptyObjects();
}

}
}

Every project will have different requirements and variations, so I am not going describe every aspect of this particular implementation. The idea here is to demonstrate how one-to-many relationships can fit into the generic solution.

Work a bit smarter
We now have no problem viewing, editing, or submitting forms, and saving associated objects, but it remains somewhat cumbersome to reset and clean up all the associated placeholder objects for each ActionForm bean. We facilitate this task by implementing an abstract ActionForm class to manage this routine job.

For the generic implementation, we must iterate over all the domain objects managed by Hibernate, discover their identifiers, and test the values. Fortunately, the org.hibernate.metadata package contains two utility classes to retrieve domain object metadata. We use the ClassMetadata class to check if the object is Hibernate-managed, and if it is, we obtain the value of its identifier property. We also leverage the utility functionality in the Jakarta Commons Beanutils package to more easily obtain JavaBean metadata:

import java.beans.PropertyDescriptor;
import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.metadata.ClassMetadata;

public abstract class AbstractForm extends ActionForm {
public void reset(ActionMapping mapping, HttpServletRequest request) {
super.reset( mapping, request );

// Get PropertyDescriptor of all bean properties
PropertyDescriptor descriptors[] =
PropertyUtils.getPropertyDescriptors( this );

for ( int i = 0; i // This is a Hibernate object
String propName = descriptors[i].getName();
Object propValue = PropertyUtils.getProperty( this, propName );

// Evaluate property, create new instance if it is null
if ( propValue == null ) {
PropertyUtils.setProperty( this, propName, propClass.newInstance() );
}
}
}

}

public void cleanupEmptyObjects() {
// Get PropertyDescriptor of all bean properties
PropertyDescriptor descriptors[] =
PropertyUtils.getPropertyDescriptors( this );

for ( int i = 0; i // This is a Hibernate object
Serializable id = classMetadata.getIdentifier( this, EntityMode.POJO );

// If the object id has not been set, release the object.
// Define application specific rules of not-set id here,
// e.g. id == null, id == 0, etc.
if ( id == null ) {
String propName = descriptors[i].getName();
PropertyUtils.setProperty( this, propName, null );
}

}
}

}
}

Exception handling code has been removed from the above code for readability.

Our new abstract AbstractForm class extends Struts ActionForm and provides our generic behavior for resetting and cleaning up many-to-one associated objects. When the relationship cardinality is reversed (i.e., for one-to-many relationships), each case is quite different, so just implementing the custom behavior in the specific ActionForm bean implementation proves a better approach.

Our final task is to change all of our ActionForm bean classes (ProductForm, CategoryForm, etc.) to inherit from AbstractForm, instead of ActionForm.

Conclusion
Struts and Hibernate are popular and powerful frameworks that can be extended to work more effectively with one other by bridging the gap between the domain model and the MVC view. This article describes a generic implementation of such a bridge that can be used in any Struts and Hibernate project with no major changes to the existing code.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s