BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles InfoQ Book Excerpt: Rails for Java Developers

InfoQ Book Excerpt: Rails for Java Developers

Bookmarks

Ruby and Rails bring a lot of good ideas to web development. There are many books about Ruby and Rails, but Rails for Java Developers brings a unique perspective. Instead of starting from scratch, we build on the shared knowledge of Java developers everywhere.

Building on Java knowledge allows us to work by comparison. Throughout the book we compare Rails examples with Java web framework examples, so that you can quickly see the similarities, and pay special attention to the differences.

As an example, consider a controller method that manages saving a simple model object. The biggest difference between most Java frameworks and Rails is that Java frameworks make a lot of the object model explicit on every controller method. In Rails, by contrast, much of the web object model is implicit. In Rails, you don't see it unless you need it.

Excerpt 1: Controller Save Method

Here is a Struts action method that will save or update a person:

code / appfuse_people / src/ web/ com/ relevancellc/ people/ webapp/ action/ PersonAction.java
public ActionForward save(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
ActionMessages messages =new ActionMessages();
PersonForm personForm = (PersonForm) form;
boolean isNew = ("".equals(personForm.getId()));
PersonManager mgr = (PersonManager) getBean("personManager");
Person person = (Person) convert(personForm);
mgr.savePerson(person);
if(isNew) {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.added"));
saveMessages(request.getSession(), messages);
return mapping.findForward("mainMenu");
} else {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.updated"));
saveMessages(request, messages);
return mapping.findForward("viewPeople");
}
}

Let's begin by considering the happy case where the user's edits are successful. Much of this code is similar to previous examples; the new part is the addition of a status message. In line 5 we create an ActionMessages instance to hold a status message, and in lines 12--14 and 17--19 we save the ActionMessages into the request so they can be rendered in the view.

Here is the Rails version of update:

code/people/app/controllers/people_controller.rb
def update
@person = Person.find(params[:id])
if @person.update_attributes(params[:person])
flash[:notice] = 'Person was successfully updated.'
redirect_to :action =>'show', :id => @person
else
render :action =>'edit'
end
end

The actual update happens on line 3. update_attributes is an ActiveRecord method that sets multiple attributes all at once. Like its cousins create and save, update_attributes automatically performs validations. Since the params[:person] hash contains all the name/value pairs from the input form, a single call to update_attributes does everything necessary to update the @person instance.

Like the Struts update, the Rails version of update sets a status message. In line 4, the message "Person was successfully updated." is added to a special object called the flash. The flash is designed to deal with the fact that updates are generally followed by redirects. So, saving a status into a member variable does no good---after the redirect, the status variable will be lost. Saving into the session instead will work, but then you have to remember to remove the status message from the session later. And that is exactly what the flash does: saves an object into the session and then automatically removes the status message after the next redirect.

The flash is a clever trick. Unfortunately, the data that is typically put into the flash is not clever at all. Out of the box, Rails does not support internationalization, and status messages are stored directly as strings (usually in English). Contrast this with the Struts application, which stores keys such as "person.added." The view can later use these keys to look up an appropriately localized string. The lack of internationalization support is one of the big missing pieces in Rails. If your application needs internationalization, you will have to roll your own or use a third-party library.

After a successful update operation, the controller should redirect to a URL that does a read operation. This makes it less likely that a user will bookmark a URL that does an update, which will lead to odd results later. Some possible choices are a show view of the object just edited, a list view of similar objects, or a top-level view. The Struts version does the redirect by calling findForward:

return mapping.findForward("mainMenu");

To verify that this forward does a redirect, you can consult the struts.xml configuration file. Everything looks good:

<global-forwards>
<forward name="mainMenu" path="/mainMenu.html" redirect="true"/>
<!-- etc. -->
</global-forwards>

Where Struts uses findForward for both renders and redirects, Rails has two separate methods. After a save, the controller issues an explicit redirect:

redirect_to :action => 'show', :id => @person

Notice that the redirect is named in terms of actions and parameters. Rails runs its routing table "backward" to convert from actions and parameters back into a URL. When using default routes, this URL will be /people/show/(some_int).

Now that you have seen a successful update, we'll show the case where the update fails. Both Struts and Rails provide mechanisms to validate user input. In Struts, the Validator object automatically validates form beans, based on declarative settings in an XML file. Validations are associated with the form. To specify that the first name is required, you can use XML like this:

code/appfuse_people/snippets/person_form.xml
<form name="personForm">
<field property="firstName" depends="required">
<arg0 key="personForm.firstName"/>
</field>
<!-- other fields -->
</form>

The original intention of the separate validation language was separation of concerns. Sometimes it is more convenient to keep related concerns together. Instead of writing the validation.xml file by hand, we generate the validations with XDoclet annotations in the Person model class:

code/appfuse_people/src/dao/com/relevancellc/people/model/Person.java
/**
* @hibernate.property column="first_name" length="50"
* @struts.validator type="required"
*/
public String getFirstName() {
return firstName;
}

During an Ant build step, the struts.validator annotation generates the appropriate lines in the validation.xml file. (In Java 5 and later, annotations provide a simpler and more integrated annotation mechanism.)

In Rails, there is no separate form bean, and the validations are declared on the Person model class directly. You have already seen this in Section 4.5, Validating Data Values.

code/people/app/models/person.rb
class Person < ActiveRecord::Base
validates_presence_of :first_name, :last_name
end

Both the Struts version and the Rails version handle a validation error in the same way: Render the page again, with error messages marking the form fields that need to be corrected. In Struts, this redirection is handled in the Validator. Form beans such as PersonForm extend a Struts class, org.apache.struts.validator.ValidatorForm. The ValidatorForm class provides a validate method. The Struts framework calls validate automatically, and if any item fails validation, the form page is rendered again.

The Rails approach is more explicit. When you call save or update_attributes on an ActiveRecord model, a boolean false may indicate a validation failure. If this happens, you can use render to render the edit action again:

code/people/snippets/update_fails.rb
if @person.update_attributes(params[:person])
# ...success case elided...
else
render :action => 'edit'
end

The validation errors are stored in the errors property on the @person object, so you do not need to do anything else to pass the errors to the form view. Section 6.5, Building HTML Forms, describes how validations are rendered in the view.

By comparing similar sections of Ruby code and Java code as in the section above, one can quickly pick up Ruby syntax by example. This is useful, but also dangerous, because Ruby is a very different language. (If you have ever read Java code written in a C accent, you know exactly the kind of danger here!) An important part of being effective in Rails is building a thorough understanding of the Ruby language. Accordingly, we spend two chapters of the book on pure Ruby. The next excerpt shows one of the most important, and powerful, strengths of the Ruby language: the ability to make modifications to core classes.

Excerpt #2: Extending Core Classes

Programmers often need to add methods to classes that are part of the language runtime itself. Subclassing the class is typically not an option here, since the method needs to be available to instances of the base class itself. For example, neither Java nor Ruby have a method that tells whether a String is blank, in other words, null, empty, or just whitespace. A blank-testing method is useful, because many applications want to treat all blank inputs in the same way. For both Java and Ruby, the open source community has provided methods that test for blankness. Here is a Java implementation of isBlank() from Apache Commons Lang:

code/Language/IsBlank.java
public class StringUtils { 
public static boolean isBlank(String str) {
int strLen;
if (str == null || (strLen = str.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if ((Character.isWhitespace(str.charAt(i)) == false)) {
return false;
}
}
return true;
}
}

Since methods cannot be added to core classes, Commons Lang uses a standard Java idiom, collecting extensions methods as static methods in another class. The implementation of isBlank() lives inside a StringUtils class.

Callers of isBlank() prefix each call with the helper class name StringUtils:

code/java_xt/src/TestStringUtils.java
import junit.framework.TestCase;
import org.apache.commons.lang.StringUtils;

public class TestStringUtils extends TestCase {
public void testIsBlank() {
assertTrue(StringUtils.isBlank(" "));
assertTrue(StringUtils.isBlank(""));
assertTrue(StringUtils.isBlank(null));
assertFalse(StringUtils.isBlank("x"));
}
}

Ruby classes are open---you can modify them at any time. So, the Ruby approach is to add blank? to String, as Rails does:

code/rails/activesupport/lib/active_support/core_ext/blank.rb
class String
def blank?
empty? || strip.empty?
end
end

Here are some calls to blank?:

code/rails_xt/test/examples/blank_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class BlankTest < Test::Unit::TestCase
def test_blank
assert "".blank?
assert " ".blank?
assert nil.blank?
assert !"x".blank?
end
end

What about null?

The Java version of isBlank() uses a helper class, StringUtils, for a second reason. Even if you could hang the method isBlank() on String, in Java you would not want to do so. Calls to isBlank() need to return false for nullStrings. In Java, calling any method on null will cause a NullPointerException. By testing the first parameter to a static StringUtils method, you avoid the trap of trying to write a String method that (nonsensically) compares this to null. Why doesn't the Ruby approach work this way as well?

Ruby nil Is an Object

The Ruby equivalent of Java null is nil. However, nil is an actual object. You can call methods on nil, just like any other object. More important to the task at hand, you can add methods to nil, just like any other object: The following code causes nil.blank? to return true.

code/rails/activesupport/lib/active_support/core_ext/blank.rb
class NilClass #:nodoc:
def blank?
true
end
end

Rails provides reasonable definitions of blank? for several other objects too: true, false, empty arrays or hashes, numeric types, and even the Object class.

There are dozens of languages, and hundreds of web frameworks. What really sets Java apart is the entire ecosystem. When we start a new Java project, we know that any problem we encounter has a decent chance of already being solved in the open source Java world. In considering any new framework such as Rails, it is important to dask about the surrounding ecosystem. Are there good IDEs? What about automated testing and continuous integration? Are there standard libraries for every common programming task?

Java has the biggest ecosystem, so you are right to expect some frustrations moving to any other language. But we have found that the Ruby ecosystem to be much richer than we expected. In the next excerpt, we will look at Ruby's support for automated testing, as extended by Rails to test web controllers.

Excerpt #3: Rails Extensions to Test::Unit

The easiest way to get started with Rails' extensions to Test::Unit is to look at the tests you get for free with the Rails scaffold:

$ script/generate scaffold Person

Most of the scaffold code is examined in Section 1.2, Rails App in 15 Minutes. Here, we will focus on one generated file, the functional test test/functional/people_controller_test.rb. We will take the PeopleControllerTest class apart, line by line. First, the test includes fixtures:

code/rails_xt/test/functional/people_controller_test.rb
fixtures :people, :users

The scaffold generator assumes that the PeopleController deals with people, and it sets the fixtures accordingly. All but the most trivial applications will find that controllers sometimes interact with more than one model class. When this happens, simply add more other models to the fixtures line. For example:

fixtures :people, :widgets, :thingamabobs, :sheep

Next comes the setup method:

def setup
@controller = PeopleController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end

Almost all functional tests simulate one (or more) web request/response cycles. Therefore, the @request and @response variables are instantiated for each test.

Now for a real test. The scaffold generates an index page that simply renders a list view of the model contents. Here's the test for the index page:

def test_index
get :index
assert_response :success
assert_template 'list'
end

First, the get() method simulates an HTTP GET on a controller. The one-argument version seen here specifies a Rails action name. Then the Rails assertion assert_response :success asserts that the response is a success, that is, HTTP status 200. The Rails assertion assert_template 'list' asserts that the response was rendered from the list template.

As Java programmers, we are tempted to ask, "Where are the objects?" Maybe test_index() ought to look more like the following code, with explicit objects:

# hypothetical, with explicit objects
@controller.get :index
assert_equal :success, @response.status
assert_equal 'list', @response.template

The two previous examples are functionally equivalent. The difference is one of style. In Java, we tend to prefer to make objects explicit. In Ruby, but especially in Rails, we prefer to let the "obvious" thing be implicit where possible. Try reading both versions aloud to get a better sense of the difference.

Next, the scaffold tests the list action:

def test_list
get :list
assert_response :success
assert_template 'list'
assert_not_nil assigns(:people)
end

Most of this code is familiar from test_index(). The novel part is the following:

assert_not_nil assigns(:people)

The assigns variable is special. If you create an instance variable in your controller, that variable will magically be available to your view template. The magic is actually quite simple: Rails uses reflection to copy controller variables into a collection, which is then copied back into the view instance. The collection is named assigns, so the previous assertion can be read "Assert that the controller created a non-nil variable named people."

Next, the scaffold tests the show action:

def test_show
get :show, :id => 1
assert_response :success
assert_template 'show'
assert_not_nil assigns(:person)
assert assigns(:person).valid?
end

This test looks a little different, because the show method expects a specific person to show. Rails' default behavior is to identify specific model instances by adding an id to the URL, so the call to get() includes a second argument to pass in the id of a person:

get :show, :id => 1

The general form of get() can handle any possible context for a request:

get(action=nil, parameters=nil, session=nil, flash=nil)

How can we be sure that a person with an ID of 1 exists? Look to the fixture file test/fixtures/people.yml:

code/rails_xt/test/fixtures/people.yml
first:
id: 1
first_name: Stuart
last_name: Halloway

The other bit of novelty in test_show() is the valid?() test:

assert assigns(:person).valid?

This is just ActiveRecord's standard support for validation, discussed in Section 4.5, Validating Data Values. As you add validation methods to the Person class, the call to valid?() will automatically become smarter.

The scaffold's test_new() does not introduce any new concepts, so we'll skip it. Next, then, is test_create():

code/rails_xt/test/functional/people_controller_test.rb
def test_create
num_people = Person.count
post :create, :person => {}
assert_response :redirect
assert_redirected_to :action => 'list'
assert_equal num_people + 1, Person.count
end

This presents several new ideas. Unlike the methods discussed so far, create actually changes the database. This has several implications for our test. First, the test calls post() instead of get(), since the create() operation is not idempotent.1 Second, we want to test that the database changes in an appropriate way. The following line:

num_people = Person.count

captures the number of people before the create() operation, and the following line:

assert_equal num_people + 1, Person.count

verifies that exactly one person is created. (If you want, you could perform a more rigorous test here and make sure that the new person matches the arguments passed in.)

A third implication of mutating operations such as create() is that we should not expect a :success response. Instead, a successful update redirects to the show action. The following lines:

assert_response :redirect
assert_redirected_to :action => 'list'

verify that create() redirects correctly.

The remaining scaffold methods (test_edit(), test_update(), and test_destroy()) do not introduce any new testing concepts, although you may want to read them to cement your understanding of the scaffold.

Why the Scaffold Redirects After a POST

Redirecting after a POST makes it difficult for users to accidentally submit the same update twice. (You have probably seen the double-update problem in poorly written web applications. One symptom is the browser warning "You are about to resubmit a URL that contains POST data. Are you sure?")

Rails applications typically do not suffer from the double-update problem, because a reasonably good solution (the redirect) is baked into the scaffold.

Rails is a strong web framework, built on an excellent language (Ruby), and surrounded by a solid ecosystem. The same could be said for many web frameworks. Why should you spend your limited time on Rails? Because Rails programmers are getting things done, and fast. Rails programmers have made (and substantiated) some amazing claims about developer productivity. They are having a lot of fun, too.

Should Java programmers be alarmed by this upstart? Absolutely not. Java programmers are uniquely positioned to take advantage of Ruby on Rails. Rails for Java Developers will explain how to get started.


 

1An idempotent operation can be performed any number of times with no effect beyond the effect of executing once. Idempotent operations are very friendly to proxies and caches, because there is no harm (other than wasted bandwidth) in performing the operations an extra time, now and then. Idempotent operations have their own HTTP verb (GET).

 

Copyright © 2006, The Pragmatic Bookshelf.

Rate this Article

Adoption
Style

BT