Skip to main content

Ruby on Rails: Reducing clutter in actions by placing common code in filters

This is a tiny but useful tip, that saved many lines of repeated code in my controllers, hence why not share it :)

Do Not Repeat Thyself?

If you've looked at the controller code that's generated by scaffolding, you'll find something like this:

 
  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def destroy 
    User.find(params[:id]).destroy
  end

Sure, in this case all we are doing repeatedly is instantiating the user by a potentially available parameter value. What if instead we placed these common fetches in a controller filter, which would simply set an instance variable for us? Hell, we could even handle exceptions (such as invalid ID) in only one place this way! What not to like.

When dealing with a more complicated route that has been defined, this instantiation may become quite a bit more elaborate, and the case for a filter is even more justified.

Consider the case of building a collaboration system where you have projects and individual contributions under that project, as well as a producer of the project. We might want to support all project operations under a URL that looks kind of like this:

/user/kigster/projects/MyVacation/contribution/view/34 with a corresponding route in the config/routes.rb file:
# project management route
map.connect '/user/:username/project/:project_name/contribution/:action/:id',
    :controller     => "contribution"

Based on the route defined, Rails would create params[:username], params[:project_name] and params[:id] for contribution id, in addition to the standard action/controller pair.

Now imagine that the controller we are writing has many actions, such as add, edit, view, list, append, preview, post, comment, etc. All of them could use a handle on the project, it's producer and contribution instances. Ideally - in @project, @producer_user and @contribution respectively.

If we followed the scaffolding example, we'd simply add appropriate lookups at the beginning of each action. But that's a lot of repeated code!

So let's use filters instead, to get our common lookups under control.

class ContributeController < ApplicationController
  before_filter :setup_project

  def home
     # do stuff
  end

  private

  def setup_project
    @producer_user = 
      User.find_by_username(params[:username])
    @project = 
      @producer_user.produced_projects.detect do |p|
      p.name.downcase.eql?(params[:project_name].downcase)
    end
    @contribution = 
        @project.contributions.find_by_id(params[:id])

    return true

  rescue Exception => e
    logger.info "can't show project:", e
    flash[:error] = e.message if e
    redirect_to :controller => "home" and return
  end
end    

This is short and elegant, and now every action in this controller (which is not excluded for setup_project filter) will have access to our instance variables. Groovy!

Issues

Of course nothing is free, and in this case we are potentially loosing performance. Perhaps some actions don't need to know the @producer_user. We'll be fetching it all the time, which saves time for us - developers. If performance problems occur because of extra unused fetches the filter can be broken up or optimized. Until then - it sure saves me a headache.

That's it!

Thoughts, comments?

Comments

Anonymous said…
You can pass in the :only command on the filter like so:

before_filter :setup_project, :only => [:show, :edit, :update, :destroy]
brucek said…
In the RoR Wiki (http://wiki.rubyonrails.org/rails/pages/TipsAndTricks at the bottom)
they say:
"There’s a growing trend to abuse before_filter in controllers to load data for multiple actions. This is not what before_filter is for, and using it this way makes code harder to read and maintain as well as negating the benefits of controller action caching (and possibly other current or future Rails features).

As a rule, use before_filter only for actual filtering, and always use before_filter for code that does do filtering. When you want to factor out common initialization code, simply call the shared initialization method at the start of each action method that needs it."

Who's right?
could you please change the voting block on your blog, it shows an incorrect results in total percentage,


Votes results below

Mac OS-X: 19 (54%)

Windows: 11 (31%)

Linux: 17 (48%)

FreeBSD: 1 (2%)

Total: 48 (135%)

Popular posts from this blog

Car or Auto Make-Model-Year Database : For Breakfast

Make Model What?If you like me were tasked with loading a database of recent car makes/models/years, you would start by looking on the web and seeing if someone else just has it out there, readily available, hopefully for free, but perhaps for a tiny nominal fee.?If only it was that simple... I looked and looked, and couldn't find anything that would fit the above requirements. So I thought, who would know about US car models better than Kelly Blue Book? So I went on their site, and sure enough they have a javascript file that lists all known to them makes and models of used cars. Since the file is public, I figured it's not really "evil" if I scrape and parse it for my own benefit. Disagree? Have a better source? Then leave a comment.Anyway, to cut the long story short, I'm hoping to save a day or so to someone else who may, like me, be looking for this information. The ruby module shown below retrieves and parses the javascript from KBB site into a Ruby da…

Rails3 and The Inevitable Fragmentation

I remember one of the early talks at the Canada On Rails in 2006 had a slide showing how many books one needs to read to be able to write web applications in Rails, versus Java.Of course Java side had about 10 books: Java SE, Java EE, Hibernate, Struts/Tiles/JSF, WebServices, Ant, Maven, Eclipse, JUnit, etc, etc.The Rails slide proudly showed the now hopelessly outdated "Agile Web Development With Ruby on Rails", 1st edition. Those were the times. Back then, during my work for Blurb.com myself and three other engineers managed to learn ruby, rails and build a fully functional e-commerce site in about 3 months. I was blown away by the productivity gains compared to Java, where months could be spent laying out the project, and creating all necessary infrastructure for builds, deployment, automated testing with or without the database, etc.Fast-forward to 2010. We are on a brink of Rails3 release, and oh boy, has the landscape changed since back then. I would argue that in s…

Why I Like PostgreSQL

Today I gave a short presentation at work about PostgreSQL, and why I much prefer it to MySQL.

PostgreSQL vs MySQL: Eternal Battle
I may be misreading this, but it seems that there is a recent trend within startups to move away from MySQL, probably thanks to folks like Heroku on one side (who use PostgreSQL to the extreme, and help and contribute to it's development), vs folks like Oracle on the other side, tainting the "open source pureness" of MySQL :)

At my work we currently use a mid-sized MySQL 5.1 Percona instance, which is holding up quite well I must admit. Both PostgreSQL and MySQL have definitely converged to cover most features that people want, but my leaning is still towards PostgreSQL. I just agree with it's focus on data integrity, recovery, constraints, extensibility, while some of the early decisions in MySQL's design do not agree with me at all (like truncating long strings, 1/0 instead of booleans, ambiguous group by, etc). I think that data …