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…

Getting RMagic and friends to work on OS-X Mountain Lion

Upgraded my ruby environment today to Mountain Lion.

Here is a quick checklist that I went through to get everything working.  The largest change was having to reinstall XCode and command line tools, and also download XQuarts in order to reinstall ImageMagick successfully. Without it, I was getting errors building RMagick of the following shape:
ld: file not found: /usr/lib/libltdl.7.dylib for architecture x86_64clang:
error: linker command failed with exit code 1
(use -v to see invocation)make: *** [RMagick2.bundle] Error 1

Quick checklist: Install Mountain Lion Install XCode 4.4 Install command line tools from XCode 4.4 Preferences dialog Install XQuartzIn terminal run brew update brew uninstall imagemagick brew install --fresh imagemagick wipe out your ~/.rvm folder reinstall RVM and install the latest ruby 1.9.3-p-194 run "bundle" in the project folder run "rake" and rejoice
References:

https://github.com/mroth/lolcommits/issues/65

Wanelo Tech Gems: we've been busy!

It's been quite some time that I've posted here, to my personal blog, so long in fact, that the publishing interface a'la Blogger now looks like MS Word. Or God forbid you remember, Word Perfect. (Yes, I am that old).

But that's not what I wanted to write about.

Wanelo, the team that I lead as a CTO, has been kicking so much ass lately, that I've just had no time documenting personal projects here, even though there have been plenty.

So to sort of catch up in one blog post at once, I wanted to put a few links to some of the great content on our technical blog, that's been recently migrated to Github Pages.

Without further ado:
Multi-process or multi-threaded design for Ruby daemons A pretty awesome blog post by our own Eric Saxby about thinking in terms of GIL (global interpreter lock in ruby), it's impact on production environments, especially in multi-threaded environments, and where long-running processes may have impact on your database transactions. Ver…