Monday, September 11, 2006

Rolling with Spring and Hibernate, part 2: the model

Most of the first part consisted of preparing the development environment, and while Rails clearly is faster here, the interesting part begins now, when we look at how much work goes into each actual modification of the application. Almost all of part 1 can be condensed into a more sophisticated Maven archetype.

The first thing we'll do now is to begin building our domain model. As I said, we'll use Hibernate with annotations as our ORM, and were going to let the model drive the database design. Hibernate is configured with url, driver class and so on, and additionally we enable the automatic schema updating from mapping metadata:

hibernate.hbm2ddl.auto=update

in hibernate.properties. Another useful setting is dumping formatted SQL to standard out, so that we can easily inspect the generated SQL:

hibernate.show_sql=true
hibernate.format_sql=true

A new feature of Spring 2.0 is the ability to wire collaborators into domain model objects on instantiation, using the @Configurable annotation and AspectJ load-time weaving. Were going to use this to inject a Hibernate SessionFactory into the domain model, allowing use to completely skip the DAO layer and use a syntax similar to ActiveRecord, with CRUD metods in the domain object. For simple application such as this one, we don't need any additional abstraction on top of Hibernate.

Populate the Recipe class with a few properties, and make it an @Entity:

@Entity
@Configurable(autowire=Autowire.BY_NAME)
public class Recipe {

String title;
...
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
...
}

Yes, we're missing an id, but we're going to build a BasicEntity base class that provides CRUD for both our Recipe and Category classes. The main point here is that you simply add a property to a Java class marked as @Entity, and Hibernate will modify the database schema on deploy. This is contrary to Rails, where you modify the schema by hand and get the model properties on the fly. For a simple model, either way is sufficient and roughly equivalent (they follow the DRY principle), but as an application grows in complexity you may want to diverge a little from the default behaviour.

The CRUD-providing base class looks like this:

@MappedSuperclass
public abstract class BasicEntity implements Serializable {

private Long id;
protected SessionFactory sessionFactory;

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public Long getId() {
return id;
}

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

@Transient
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}

@Transient
protected Session getSession() {
return sessionFactory.getCurrentSession();
}

@Transient
protected Criteria getCriteria() {
return getSession().createCriteria(this.getClass());
}

@SuppressWarnings("unchecked")
@Transactional(readOnly=true)
public T load() {
getSession().refresh(this);
return (T) this;
}
...
// + methods for store, delete and findAll

}
There are many interesting things here: the @MappedSuperclass annotation allows us to use this as a base class for mapped entities, with the Id property being mapped in subclasses. We're using generics to make return values match the inherited class, a varargs parameter for relational properties to join (relations are lazy by default in Hibernate 3), and we're using the @Transactional annotation to specify that the load() method should be run in a transaction.

In other words, theres a lot going on here. But maybe the most important thing is the SessionFactory, which of course is the Hibernate interface through which you perform the actual database operations. On to the application context: here's what we need to inject a SessionFactory into all Recipe instances, even those created outside of our control, and to make all @Transactional methods transactional:

<bean id="transactionManager" class="org.springframework...HibernateTransactionManager">

<bean class="org.springframework...AnnotationBeanConfigurerAspect" method="aspectOf">

<bean class="org.springframework...AnnotationTransactionAspect" method="aspectOf">

<bean id="sessionFactory" class="org.springframework...AnnotationSessionFactoryBean">
<property name="annotatedClasses">
<list>
<value>se.petrix.cookbook.model.Recipe</value>
</list>
</property>
</bean>


In other words, we need a transaction manager (since @Transactional isn't tied to any particular transaction implementation), a SessionFactory of course, and finally the two AspectJ aspects that are applied to classes and methods with the @Configurable and @Transactional annotations. All of this goes into applicationContext-hibernate.xml. For the AnnotationBeanConfigurerAspect bean, we could use the shorter form:

<aop:spring-configured />

but since there's no corresponding short version for the transaction aspect (<tx:annotation-driven/> is for Spring AOP, not AspectJ...hopefully that'll change in the future). In general, Hibernate configuration could benefit in a major way from an XSD schema.

Comparing to Rails, we note that none of this is needed, since you don't get to choose/have to decide what transaction strategy or ORM solution you want. One annoying thing in Spring however is that you have to manually specifiy what classes should be mapped by Hibernate, even though they are all marked with @Entity. That's one area that Spring could improve upon Hibernate, by post-processing all classes in a package hierarchy specified by a wildcard pattern, for example. More on that in the last part.

The functionality of the BasicEntity class is provided by ActiveRecord in Rails, which your mapped domain objects inherit. This is a fair amount of overhead at this stage, on the other hand we could scale this by switching to injecting a complex business layer fairly easily, creating more layers in our application.

Finally we change the Recipe class to inherit from BasicEntity:

public class Recipe extends BasicEntity<Recipe> implements Serializable {

Note the parametrization!

No comments: