Friday, September 15, 2006

Rolling with Spring and Hibernate, part 4: adding a relationship

We now have a basic application in place, that we can use to list, create, edit and delete recipes. The next step is to add the Category to our domain model, and create the relationship between Recipe and Category: a category contains many recipes, and a recipe belongs to one category.

The first step will be to create the Java class, which will inherit from BasicEntity. That way, we already have the id property and CRUD operations in place, and we only need to add one property (name) and the relation to Recipie. But we're going to start with just the name property. Here's the entire Category class:

@Entity
@Configurable(autowire=Autowire.BY_NAME)
public class Category extends BasicEntity<Category> {

String name;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

}

That's the model, now we need the view and the controller. Controller first:

public class CategoryController extends MultiActionController {

public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws Exception {
List<Category> categories = new Category().findAll();
return new ModelAndView("category/list", "categories", categories);
}
...
// + create, edit and delete
}

The views are very similar to the Recipie, we won't go into detail right now.

That's all you need to do in order to add a new model object and controller the project! On deploy, Hibernate will update the schema to include the new @Entity, and the controller mapping is implicitly derived from the class name CategoryController. Check out http://localhost:8080/cookbook/category/list.htm.

At this point we're almost at par with Rails productivity. We don't have scaffolding, but the code generated by "scripts/generate Category scaffold" is more or less the same as what we've just written, and we edit the Java model class and get the table generated, instead of the other way around. Generated code still needs to be tested, understood and maintained, and code completion makes writing actual code easier in a statically typed language. And in a final product, you will still need to re-write all the scaffolded views, so sooner or later Rails will lose that particular advantage.

The next step is to implement the relation between Recipe and Category, and that's an area where Rails will come out on top. First, the model: Recipe belongs to one Category:
@ManyToOne
@JoinColumn
public Category getCategory() {
return category;
}

And Category has many recipes:

@OneToMany(mappedBy="category")
public Set getRecipes() {
return recipes;
}

This is the inverse side of the relationship, which means that changes to the relationships are persisted when the Recipe is persisted. The default behaviour of Hibernate with the above annotations is to add a column "category_id" to the Recipie table, which holds the id of the owning Category and has a foreign key constraint. All of this is of course handled by Hibernate, since we're using automatic schema updating.

However, in order to bind a Recipe to a Category, we're going to have to write a custom PropertyEditor that converts the incoming parameter "1" to the Category entity with id == 1. The recipe/edit.ftl view is modified to include the following selector:

<h4>Category</h4>
<select name="category">
<#list categories as c>
<option value="${c.id}"
<#if (recipe.category)?exists && (recipe.category.id == c.id)>selected="selected"</#if>>
${c.name}
</option>
</#list>
</select>

We also add all categories to the model, in RecipeController:

List<Category> categories = new Category().findAll();
...
model.put("categories", categories);

And here's the custom binder we need:

public void setAsText(String text) throws IllegalArgumentException {
Long id = Long.valueOf(text);
Category category = new Category();
category.setId(id);
setValue(category.load());
}

This binder needs to be registered in the RecipeController:

protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
throws Exception {
binder.registerCustomEditor(Category.class, new CategoryPropertyEditor());
}

This means that for all properties of class Category, use this binder to convert the incoming request parameter string to an object of the Category type. With this binder in place, we can keep using this form of handler method for storing a Recipe:

public ModelAndView store(HttpServletRequest request, HttpServletResponse response, Recipie recipie)
throws Exception {
if (recipie.getDate() == null) {
recipie.setDate(Calendar.getInstance());
}
recipie.store();
return new ModelAndView("redirect:list.htm");
}

The request parameters are automatically bound to a Recipe object, and we simply pass it to the
handler method and store it.

An clear advantage of Rails is that relationship binding is provided out of the box, whereas in Spring MVC we need to build a custom PorpertyEditor for that. Simple properties however, are automatically bound to the Recipie object.

This concludes the application part, a CRUD interface for recipies and categories. In part 5, we move on to testing.

No comments: