DRY in Rails: Model Scopes

A common antipattern in Rails and MVC development in general is to clutter up an application's controllers with logic that could be better handled from inside the model. A common best-practice known as "Fat models, Skinny Controllers" has emerged to address this and has been repeated across blogs and presentations for quite some time.

RJ Pittman
#Development | Posted

A common antipattern in Rails and MVC development in general is to clutter up an application's controllers with logic that could be better handled from inside the model. A common best-practice known as "Fat models, Skinny Controllers" has emerged to address this and has been repeated across blogs and presentations for quite some time.

Rails in particular has implemented some functionality in ActiveRecord (the default base class for Rails models) that makes it very easy to avoid the the Fat Controller problem while simultaneously improving code readability. ActiveRecord scopes are class methods that can be used to provide a way to access specific filters and sorts on a particular model.

Rails ActiveRecord scopes can help DRY up your application by exposing methods for interacting with models.

An Example

Consider that we're building a simple blogging application. We want our blog to:

  1. Always hide posts that are not published
  2. Be able to list posts:
    • With a specific tag or tags
    • With a specific author or authors
    • Across both authors and tags via a search mechanism

First try: A swing and a miss

Take a look at what a first attempt without scopes from our Posts controller might resemble:

  1. class PostsController < ApplicationController
  2. # ...
  3. def search
  4. @posts = Post.includes(:tags).where(:tags => {:name => ['hey']})
  5. @posts = @posts.where(:published => 1)
  6. @posts = @posts.order(:title)
  7.  
  8. respond_to do |format|
  9. format.html # index.html.erb
  10. format.json { render json: @posts }
  11. end
  12. end
  13. # ...
  14. end

We've specified criteria in our search method that would need to be repeated in other parts of our application in order to meet the requirements we outlined. What that means is the same criteria will be re-written at least two more times based on our requirements.

Now DRY it up

A better approach would be to move both our filters and our sort into resuable named scopes in our model. We'll probably also want to include tags and authors of a post whenever we search by them for display and performance purposes. Let's see what that might look like in our Post model:

  1. class Post < ActiveRecord::Base
  2. attr_accessible :body, :published, :title, :tags, :tags_ids
  3. has_and_belongs_to_many :tags
  4. belongs_to :user
  5.  
  6. scope :published, where(:published => 1)
  7. scope :tagged, lambda{|tag_names| joins(:tags).where(:tags => {:name => tag_names}) unless tag_names.nil?}
  8. scope :authored_by, lambda{|user_ids| joins(:users).where(:user => {:id => user_ids}) unless user_ids.nil?}
  9. scope :sorted, order(:date => :desc)
  10. end

And in our controller...

  1. class PostsController < ApplicationController
  2. # ...
  3. def search
  4. @posts = Post.published.authored_by(params[:authors]).tagged(params[:tags]).sorted
  5.  
  6. respond_to do |format|
  7. format.html # index.html.erb
  8. format.json { render json: @posts }
  9. end
  10. end
  11. # ...
  12. end

Voila!

Rails scopes can make an application's codebase more concise and descriptive. In the example, we can see that the Post model's named scopes are very descriptive of the functionality they provide. All methods in the controller can easily reuse scopes instead of repeating the ActiveRecord filters and parameters they are comprised of. If the filters need to be updated, any changes can be made to the scopes which enforces consistency across the controllers that implement them.

RJ Pittman