pure class
Posted by jcarroll
Jul 17
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):
1 2 3 4 5 6 7 8 9 10 11 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 |
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.
Comments on this post
Jul 17
Derek said,
So… you said you’re not exactly enamored with any of the methods you described above. Which design choice did you ultimately decide on?
Jul 17
Gaius said,
So what syntax would you love? There’s always the option of defining the method on the class using the association:class User < ActiveRecord::Base def good_reviews ... end endbut that doesn’t seem any nicer than option #1 above (though it does have the benefit of reducing chaining, and thus allowing refactoring more easily).
Jul 17
jare care said,
I ended up just defining an instance method #good_reviews on
Userand decidedlooked better than
Jul 17
James Moore said,
I think it’s interesting that what you present as the third variant (and don’t like very much) is what I think of as the correct/standard way to do this. The advantage is that it’s available to anyone who has_many of the thing with the class method:class Affiliate < ActiveRecord::Base has_many :reviews endAnd with the def self.good method, both user.reviews.good and affiliate.reviews.good share the same code.
You left out another way to do your other options – add another association, rather than a method:class User < ActiveRecord::Base has_many :reviews has_many :good_reviews, :conditions => ['rating = ?', Review::GOOD] endJul 17
Eric Mill said,
I think Jared’s objection to using defining
Review.goodis that its only use is to call it indirectly, throughuser.reviews.good. At least this is what you’ve said previously.Jul 17
Mr eel said,
I like the last option the most. That it’s not called directly isn’t an issue for me. In fact I consider that a useful side-effect — I might want to grab all good reviews with Reviews.good.
Jul 18
jare care said,
James,
Good point on the fact that the 3rd way is DRYer is some circumstances e.g. when you have a many-to-many with a join model such as:
That would allow me to put that #good method in 1 place and be usable from both a
User’s andSong’s reviews association.Also good point on the fact that I could of let
ActiveRecordwrite the #good_reviews instance method for me by calling #has_many again.Jul 19
Tammer Saleh said,
I know that the Review.good method is just for illustration, but I would tend to take it a step further and make it a find_good() method:
This gives you more freedom, and flexibility. Also, I prefer the hash syntax for :conditions.
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Come “ride the toad” on Hoptoad, the app error app.
Thunder Thimble: Brand monitoring for social media.
Widgetfinger: Simple content management for simple websites.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our beginning or advanced training.
Archives