Randy and I recently completed a project implemented with Ruby on Rails. This is a writeup of the tools and strategies we used and what we learned, liked and disliked about Rails development.
The project was fairly small; we pair-programmed almost all of the development, totaling about 10 person weeks of work. The application replaces a hybrid FileMaker/Excel solution our client was using to collect and analyze research data on corporate practices. The application is used by a small number of users (<10) but all day, every day.
We did our development on OS X using IntelliJ IDEA 7 beta with the Ruby plugin. While we suffered bleeding edge instability issues with some plugin versions, IDEA and the Ruby plugin were great tools providing support for rake, RDoc, unit testing, and running our development server. The PDF version of Agile Web Development with Rails was our bible for reference information.
We used Atlassian Bamboo for continuous integration since we are already using it for our Java projects. The ci_reporter and rails_rcov Rails plugins provide test and code coverage information to Bamboo’s statistics database.
We hosted our staging server at Carbon Five running a pack of Mongrels with Apache mod_proxy_balancer. Our production server is hosted at Joyent (aka TextDrive).
We used capistrano and capistrano-ext to manage deployment to multiple targets (staging and production).
We jumped straight in to using the Rails 2.0 RESTful support. To our surprise we found the scaffolding created by ‘script/generate scaffold_resource Foo’ to be very helpful from UI to database including a starting point for building out our tests. We used nested resources and custom actions. We used content-type/format detection to return different views for the same actions.
If you are going to use REST on Rails you have to understand Rails routes. Of all aspects of the Rails framework, understanding request routing and the finer details of the generated helper methods caused us the most head scratching moments. Writing unit tests for our routes.rb file was a helpful tool for answering questions.
Without a doubt, REST is the way to go in Rails. It creates a sense of context within your application and a taxonomy of the actions that can take place. The helper methods for generating paths simplify refactoring and reorganizing.
In general we found the quality of available plugins to be high and their functionality rich. Plugins provide a range of functionality from extensions to ActiveRecord, mixins, rake tasks, and generators for migrations, models and controllers. Things are changing fast so it is important to survey the available options. Sometimes the most discussed plugin is not the best to use – just the one that has been available the longest.
We used Piston to manage our plugin dependencies. The standard Rails practice of using svn:externals linked to the trunk of a plugin’s development seemed sketchy to us. In fact, the trunk of the restful_authentication plugin had recent changes that caused it to fail in anything but Edge Rails. We used Piston to lock the plugin to the last working version. It’s strange to me that plugin developers do not seem to use tags and branches to manage releasing their plugins to the world.
The has_many_polymorphs plugin extends Rails’ associations with advanced support for polymorphic associations. We used this plugin to implement tagging features in place of the older, less flexible acts_as_taggable plugin. The plugin also provides a generator for specifically implementing tagging.
The userstamp plugin automatically updates ‘created_by’ and ‘updated_by’ attributes of model objects with the logged in user.
The acts_as_versioned plugin provides versioning of a subset of our model objects. We have a complex hierarchy for research information that can change through time. Research from a point in time needs to match the hierarchy at that time. The acts_as_versioned plugin manages a table of versions for each model being versioned with mixed-in methods on the model objects for accessing historical versions.
The restful_authentication plugin provides user authentication. The authorization plugin provides role and instance-based authorization. The authorization plugin implements a lightweight DSL – a technique that seems to be favored by many Ruby developers. The authorization plugin exposed us to the practice of using static class members as thread-local instances. We couldn’t quite stomach having User.current_user statically defined so implemented it with a thread-local instance:
class User < ActiveRecord::Base def self.current_user=(user) Thread.current[:user] = user end def self.current_user Thread.current[:user] end end
We wrote our own acts_as_notable plugin using the core plugin generator and the HOWTO on the Rails wiki. Our plugin mixes-in the ability to add notes to any model object. This was our first exposure to the powerful meta-programming tools ‘class_eval’ and ‘instance_eval’. We later realized that we could have used the has_many_polymorphs plugin for this but the exercise implementing this plugin was valuable anyway.
We used the core Rails support for unit, functional and integration tests though, honestly, we didn’t write any integration tests. We did not use RSpec or Mocha so did not get into behavior-based testing for Rails.
In general we found the testing support in Rails to be great. Being able to make assertions on your HTML output with ‘assert_select’ is great. Even simply having exceptions raised in output rendering when we broke a view with a refactor was great. Coming from the Java world where JSP makes it very difficult to get HTML output outside of the container, controller (functional) testing was a breath of fresh air.
Rails data fixtures are pretty cool but also a pain in the butt. In Java land, we use DBUnit to create data fixtures that model an application scenario for a suite of tests. With the core Rails support for fixtures, you are stuck maintaining a monolithic fixture context with related information spread across multiple files. Our application is a small one but even so our fixtures quickly became hard to manage and understand. Adding a version dimension to our model data didn’t help.
One approach we have seen for maintaining fixtures in to write a suite of tests that assert the integrity of your fixture data. Keep your fixtures as lean as possible while representing all possible application states. Yuck. We are interested in using fixture scenarios on our next project.
Randy dropped a database.rake file into lib/tasks/ to give us some utilities for managing our databases. While in early development, we would occasionally modify a historical migration and recreate our database from scratch using db:reset.
We also created a project rake file that defined tasks for importing data into our system. The import could have been a migration but seemed better as a task that could be done or redone at any time.
We like migrations a lot. We like the Rails DSL for defining migrations, using model objects in migrations, and explicit schema versioning. In fact, we liked migrations so much that we implemented a similar strategy on our Java projects over a year ago.
We did debate a couple issues. First, is it ever okay to modify historical migrations, for example, to add a column to a model. I felt that if a build has been pushed to our staging server, we should never modify a historical migration. We should have to migrate our staging data forward just like our production data. In that way we get to stage our migrations too. In reality, it turned out that it was only really important to be disciplined about this once we had a production system with data that we could not lose.
The other issue we debated was how much time to spend on our ‘down’ migrations. There are plenty of scenarios where you could roll back the schema but lose data. What do you do in this case? In reality, our deployment architecture and roll out plans were so simple that down migrations did not really seem relevant for this application. In the end we did test that our migrations could go up and down and preserved data where it was easy but did not spend much time on it. Do people implement migration tests?
From the discussion above:
Also, we like Ruby. It is terse yet semantically expressive. We often found ourselves writing six lines of code that we would reduce down to one intuitive and easy-to-read line. Ruby has a great shape-changing ability that makes it easy to create lightweight DSLs for different contexts. Rails makes great use of this ability in database migrations and RJS templates. We liked mixins and found them far more useful than inheritance for our purposes.
As mentioned above, we used Piston to manage our plugin dependencies. Where possible we also installed Gems in vendor/. In many cases, you need Gems to be built and installed for your system so you cannot check them in.
We ran into one issue where the plugins we we needed for our continuous integration server had gem dependencies that caused our application to not load in any environment, even if we did not need those plugins or gems for that environment. The issue was the way that rake tasks in the plugins loaded their dependencies. We ended up having to modify the plugins to not load if the gems were not available.
This dependency on system-installed software was foreign and awkward coming from Java land. Rails applications often depend on other system software like cron and ImageMagik. You can imagine multiple Rails applications running on one server having gem or software version conflicts. It seems that virtualization solutions could be very helpful here as it is for LAMP-based applications. We worked on a Python project that included a VMWare image of the entire development environment checked in to source control. It turned out to be a slick solution to the system dependency problem.
Rails does not have built in support for foreign keys. In your migrations, you have to execute SQL to create and remove foreign keys. Plenty of folks in the Rails community seem to think they are a good idea but there are issues. One issue we ran in to was that ‘db:test:prepare’ which replicates your database schema for testing does not replicated foreign keys so you can create scenarios that succeed in tests but fail in production.
On the other hand, not having foreign keys when testing made fixtures a bit easier to manage since load order did not matter.
We ended maintaining that foreign keys are important but that not having them in test was okay.
Coming from Hibernate, we found the query-building facilities of ActiveRecord to be limited. Very quickly you end up doing a bunch of string-munging to build dynamic queries. has_finder looks like it may address some of these issues.
After years using IDEA for Java development, it was a bit odd to be back in the world of search-and-replace for rename refactors. The IDEA Ruby plugin support for refactoring is improving but still needs work to be generally useful.
The most important lesson we learned in regards to refactoring is to be diligent about naming classes, methods and variables to be consistent with the model names and inflections of your application. Don’t abbreviate. Avoid model objects where the plural is the same as the singular because renaming it later will be harder than if the forms are different.
After using a good refactoring tool to work on Java applications going on four and five years old, I am bit worried about what its going to be like to work on Rails applications that have been around that long. Hopefully the tool support will be there but it will never be as good for a scripting language as it is for Java. Basic refactors like renaming are only going to take longer as a Rails application grows. Keep your test coverage high and refactor regularly or you may find yourself in the regrettable circumstance where the cost of refactoring prevents you from doing it.
On our next project we are looking forward to: