Search |
||||||||||||||||||||||||||||||||||
RAD That Ain't Bad: Domain-Driven Development with Trails
Thu, 2005-06-23
|
||||||||||||||||||||||||||||||||||
| |||||||||||
By now, there is a good chance you have at least heard of Ruby on Rails. For those who haven't, Rails is a framework using the Ruby language that allows one to create database-driven web applications in a fraction of the time it would normally take. I'm not going to cover Rails in this article, as Curt Hibbs has already done a masterful job in "Rolling on Rails." Instead, this article will focus on how we can do Rails-esque "rapid application development the right way" in Java.
I first heard of Rails as I was hanging out after a meeting of my local Java users group with my good friend Jim Weirich. Jim is a well-known Ruby nut and all around good, smart guy. So when he started talking very excitedly about "this new Ruby web thing I can't remember the name of," I decided to hang out a little longer and take a look at a video he wanted to show me. I probably wouldn't have bothered to read a long article about it, but heck, even I can spare a few minutes to watch a video. The video was a screen capture of a developer creating a fully functional database-driven web application in ten minutes.
As a Java developer my first reaction to Rails was sheer, unabashed envy. Developing a web application in Java, even with best-of-breed technologies such as Spring, Hibernate, and Tapestry, is still much more difficult than cranking out a Rails application. My next reaction was to think about how we can bring some of the brilliant ideas of Rails to Java. I'm here to say with certainty that we can, and I've spent the last several months working to make it possible for any Java developer to do it.
The fruit of this effort is a framework named, unoriginally enough, Trails. Despite the name, Trails is in no way a port of Rails. Rather, it is a framework designed to bring a similarly radical productivity increase to J2EE web application development.
The first thing we need to figure out is what makes Java
development with our current technologies and methods more
difficult than we would like. To highlight the problem, let's
imagine we are developing a J2EE web application using Spring and
Hibernate, and that we need to add a new type of domain object
called Person to the application. The precise steps will vary
depending on what web framework we select, but here are the steps we would typically need to perform:
Person class.PersonDAO class.Person table in database.PersonDAO in Spring application context XML
file.Person page or action class.Person pages to web framework XML
configuration files.personList page to list Person
instances.personEdit page to edit Person
instances.Of course, these steps will vary, depending on our specific application design and the frameworks we select, but in general they are representative. I'm hoping that seeing these list of steps has you thinking "Phew, that's a lot of work!" Can we do better?
What is the real problem here? I'm going to suggest that we need to stop repeating ourselves. In fact, this point is so important that it's worth saying again: We need to stop repeating ourselves. All we really want is to add a new type of entity to our system, yet we have at least eight different things we need to do. What if we could dramatically reduce the number of steps required? What if we could reduce the number of steps to:
Person class?How could that be possible? Well, let's think about those eight
steps again. I'm going to propose that all of the information we
really need to produce a simple, working application is contained
in the Person class. From it, we can determine:
Using just this information, we can make enough assumptions to produce a working application. What if we assume:
Of course, these assumptions will not always be correct, but in many applications they will be. If we had a framework that could use these assumptions to produce a working application based on our domain model, we could greatly accelerate development in many cases. Furthermore, if this framework let us easily override these assumptions where necessary, we could quickly produce a working prototype application and "flesh it out" into our final application.
Trails is a domain-driven development (DDD) framework for Java. Its goal is to allow us to develop J2EE web applications with the fewest redundant steps. The term "domain-driven development" refers to the process of developing an application with a rich domain model: in the most basic example of a Trails application, the domain model will be the only code we write! Trails uses this domain model as the only source of information it needs to dynamically create a basic application. As mentioned above, it makes a lot of assumptions to be able to do this, and we'll explore how to override those assumptions in a future installment. But that's getting ahead of ourselves. First, let's start with a very simple Trails application.
If you have not already done so, download and unzip Trails. You will also need the following installed on your system to use Trails:
ANT_HOME system property set correctly.
Trails will use this property to add a .jar to
ANT_HOME/lib.For this article, we will gradually recreate the Recipe
application from Curt Hibbs' RoR article. To create our application,
you need to be in the same directory where you unzipped Trails. In
this directory, do ant create-new-project. You will be
prompted for the following:
For the name of the project, enter "recipe." This will build a project with the following directory structure:
| <basedir>/recipe/ | The directory containing your new project. This contains a build.properties file you will need to customize as well as a build.xml file. Point your IDE at this directory. |
| <basedir>/recipe/src | The directory in which to place your source code. The
compile and build-hibernate-config
targets will start from here. |
| <basedir>/recipe/context | This contains your web application. |
| <basedir>/recipe/context/WEB-INF | This directory contains the web.xml file and the Tapestry page definitions. The hibernate.properties file is also located in this directory. Editing it will allow you to change your database configuration. |
| <basedir>/recipe/lib | This contains all of the .jars Trails depends upon. |
Be sure, if you have not already done so, to add a user to Tomcat with privileges to use the manager application, as the Trails Ant build will be unable to deploy our application otherwise. By default, Tomcat does not have such a user, but it's easy to add one. Edit your conf/tomcat-users.xml file (relative to where you installed Tomcat). Add a line like this:
<user name="craigmcc" password="secret"
roles="standard,manager" />
To complete the setup process, edit the build.properties file in the directory where you installed Trails. Below is a list of the properties in this file.
tomcat.home |
The directory where Tomcat is installed. |
tomcat.url |
The URL to use to connect to your Tomcat server. |
manager.username |
The username to use when connecting the Tomcat manager application. |
manager.password |
The password to use when connecting the Tomcat manager application. |
Alright, now that setup is out of the way, let's write some
code. If you have been paying attention, you can probably guess
what code we'll develop first. If you said "domain object," give
yourself a pat on the back. Domain objects in Trails are just plain
old Java objects (POJOs). Because Trails uses Hibernate to persist
our domain model into a relational database, we will also need to
add some JSR-220 persistence annotations to tell Hibernate some
extra information it needs. For a first domain object, let's create
a Recipe class, as follows:
package org.trails.recipe;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratorType;
import javax.persistence.Id;
@Entity
public class Recipe
{
private Integer id;
private String title;
private String description;
private Date date;
@Id(generate=GeneratorType.AUTO)
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public String getDescription()
{
return description;
}
public void setDescription(String description)
{
this.description = description;
}
public Date getDate()
{
return date;
}
public void setDate(Date date)
{
this.date = date;
}
}
This is a fairly simple JavaBean: we've got properties for
Title, Description, and Date,
and an Id property. We also have two JSR-220
annotations. We have an @Entity annotation that tells
Hibernate that this class is persistent. We also have an
@Id(generate=GeneratorType.AUTO) that tells Hibernate
which property is the identifier property (a "primary key," in
database parlance), and that we want this property to be
automatically generated. Notice that we don't need to explicitly
mark our other properties as persistent. This is because Hibernate,
in conformance to the EJB3 spec, will assume all of our properties are
persistent unless we explicitly mark them as
@Transient.
Believe it or not, we've now developed our first Trails
application. Let's deploy it and see it in action. If it is not
already running, start your Tomcat instance. Next, go into the
directory where you created the project and do ant
deploy. This will build our application and deploy it in our
running Tomcat instance. For maximum simplicity, Trails uses HSQL
as a simple in-memory database and lets Hibernate create all the
tables (this is, of course, configurable). To see our application
in action, we simply point your browser at
http://localhost:8080/recipe. For nothing more than
the cost of our simple domain class, Trails gives us a simple
application that will lets us work with recipes.
The default home page of a Trails application will show a list of all of the domain object types in our application, as seen in Figure 1.

Figure 1. Application home page
Following the List Recipes link takes us to a page which, if you can believe it, lists all the recipes. As you can see in Figure 2, there aren't any yet.

Figure 2. List recipes
Following the New Recipe link takes us to a screen that will let us create a new recipe, shown in Figure 3. Notice the date widget provided for us at no extra charge.

Figure 3. Edit recipe
Not bad for just coding one class, eh?
Some of you already thinking "How is all that code being generated?" There's a short and simple answer to that question: It's not. Trails eschews code generation for the simple reason that code generated is still code to maintain. Rather than generate code, Trails dynamically creates your application at runtime. For each domain class, a set of metadata is built up through a combination of reflection and Hibernate mapping information. Intelligent web components then use this metadata to produce a UI.
Sounds simple enough, but as you probably can guess, there's a lot going on under the covers. Fortunately, Trails has a lot of help. One of the key goals of Trails is to eliminate unnecessary code, so it only makes sense that Trails avoids reinventing wheels wherever possible. In fact, Trails leverages other frameworks to do a lot of the heavy lifting. Trails uses:
We have an application, but it's not very interesting. What
makes an application interesting is not objects in isolation, but
objects and their relationships. Let's introduce the concept of
categories to our domain model and assert that a
Recipe is in exactly one Category. We'll
start by creating a Category class:
package org.trails.recipe;
import javax.persistence.Entity;
import javax.persistence.GeneratorType;
import javax.persistence.Id;
import org.apache.commons.lang.builder.EqualsBuilder;
@Entity
public class Category
{
private Integer id;
private String name;
@Id(generate=GeneratorType.AUTO)
public Integer getId()
{
return id;
}
/**
* @param id The id to set.
*/
public void setId(Integer id)
{
this.id = id;
}
public String getName()
{
return name;
}
/**
* @param name The name to set.
*/
public void setName(String name)
{
this.name = name;
}
public boolean equals(Object obj)
{
return EqualsBuilder.reflectionEquals(this, obj);
}
public String toString()
{
return getName();
}
}
Like Recipe, this is a basic POJO with two
annotations. Notice that we have overridden two methods from
Object: toString() and isEquals(). These
methods are necessary for Trails to build a web interface for
objects that are related to Category. The
toString() method is necessary to tell Trails how to
display a Category. The isEquals() method
is necessary for Category objects to work properly
when used in a List. We will see how these are important
shortly.
Now that we have created a Category class, we can
add a category property to our Recipe class:
private Category category;
@ManyToOne
public Category getCategory()
{
return category;
}
public void setCategory(Category category)
{
this.category = category;
}
Nothing fancy here, just a simple JavaBean property with an annotation to tell Hibernate what kind of relationship this is.
Now we'll probably need to actually get into the nitty gritty
and start typing some HTML, right? Nope, not yet. Trails will give
us an application that manages the Recipe-Category relationship for free, no grunt coding
required. Don't believe me? Run ant redeploy.
When you visit the initial page, notice the link to List
Categories. Follow this link and click on the New Category link.
Create a couple of categories. Now go back to the Recipe page and
create a new Recipe. You should see now see a Category
field on the Edit Recipe page, as seen in Figure 4.

Figure 4. Assigning a category to a recipe
Trails will give you, free of charge, a
<select> list of all of the Category objects for
you to choose which Category a Recipe belongs in. This is where the
toString() and isEquals() methods come
into play. The toString() method is used to display
the label in the select list, and isEquals() is used
to determine which Category was selected.
Let's take stock of what we've done. We've built a complete (though simple) J2EE application that lets us manage recipes and assign them to categories. The only code we've written has been our domain model, and in return, we have an application that includes a web UI and database persistence. We have solid architecture that builds on proven frameworks such as Spring, Hibernate and Tapestry. And we've built this in just a few minutes.
In the next installment we will explore Trails in greater depth. We will learn how to customize a Trails application to override the assumptions Trails makes. We will also see how Trails also handles relationships more complex than a simple many-to-one. And finally, we'll explore how Trails supports validation by annotating your domain classes.
|