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 #has_many
and #belongs_to
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.