GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

pure class

Specifying your domain model associations in Rails is all done in Ruby and looks nice.

Say in this app users can write reviews (it doesn’t matter what they’re reviewing, but we could say music):


class User < ActiveRecord::Base

  has_many :reviews

end

class Review < ActiveRecord::Base

  belongs_to :user

end

Now #hasmany and #belongsto are class methods available on ActiveRecord::Base subclasses that generate some instance methods for the subclass that provide support for the association.

I want a method on my User’s reviews association that gives me all a User’s Review s that have a rating of ‘good’. Something like:


  user.reviews.good

There’s a couple ways I can do this:

You can pass a block to #has_many:


class User < ActiveRecord::Base

  has_many :reviews do

    def good
      find :all,
        :conditions => ['rating = ?', Review::GOOD]
    end

  end

end

class Review < ActiveRecord::Base

  POOR, AVERAGE, GOOD = 0, 1, 2

  belongs_to :user

end

I’m not a huge fan of this because its syntactically ugly.

You can also put the extensions in a separate module and file and specify the module name as a parameter in the #has_many call:


module UserExtensions

  def good
    find :all,
      :conditions => ['rating = ?', Review::GOOD]
  end

end

class User < ActiveRecord::Base

  has_many :reviews, :extend => UserExtensions

end

class Review < ActiveRecord::Base

  POOR, AVERAGE, GOOD = 0, 1, 2

  belongs_to :user

end

This is syntactically cleaner but now your User model behavior is spread over 2 files, since I put the module in a separate file in say (lib/user_extensions.rb).

Those 2 techniques are well documented and known. However, this 3rd one I’ve run into accidentally.

Define the extensions as class methods on the associated class:


class User < ActiveRecord::Base

  has_many :reviews

end

class Review < ActiveRecord::Base

  POOR, AVERAGE, GOOD = 0, 1, 2

  belongs_to :user

  def self.good
    find :all,
      :conditions => ['rating = ?', Review::GOOD]
  end

end

Apparently what happens is that ActiveRecord wraps that call to Review#good in a block that’s passed to ActiveRecord::Base#with_scope in order to find only Review s for a specific User.

Something along the lines of:


User.with_scope(:find => { :conditions => ['user_id = ?', id] }) do
  Review.good
end

I’m not a fan of this 3rd way because I don’t like seeing a class method on Review that’s never directly called. It’s only indirectly called through a User’s Review association.