Yes, for profit! This is exciting. You see, Famous Ryan just made a post about extending ActiveRecord associations. Right here. He uses a classic example, way classic: find_active.
For reference, here it is (but really, read his post):
class Organization < ActiveRecord::Base has_many :people do def find_active find(:all, :conditions => ["active = ?", true]) end end end
Ryan then shows us his new superpower: organization.people.find_active (where organization is something normal like Organization.find(:first)). Makes sense, right? You can just slap whatever methods you want onto an association to make them More Powerful™.
So, that’s a pretty good use. Custom finder and all that. But we can get crazier than just finders.
Take, oh I dunno, tags. Let’s say you have a normal blog which has and belongs to many tags. Comma separated tags.
class Post < ActiveRecord::Base has_and_belongs_to_many :tags end
Now I can do Post.find(:first).tags to get an array of tags. Nothing special. How would you get a string of tags in a view?
Maybe:<%= @post.tags.map(&:name).join(', ') %>
That’s one way. But as you and I are on this skinny controller fat model kick, we know the better solution is to add a method like tags_string to our Post model.
class Post < ActiveRecord::Base has_and_belongs_to_many :tags def tags_string tags.map(&:name) * ', ' end end
Now, our view:
<%= @post.tags_string %>
Nice. A clean, comma separated list of tag names. The acts_as_taggable plugin includes a tags_list method by default which does this same thing (albeit space separated).
But wait. Wait. What <%= %> does, behind the scenes, is call to_s on what’s passed to it. Knowing that, we can control how objects are <%= %>‘d by overriding their to_s. You’d never want to <%= @post.tags %> because it would just print a bunch of debug crap. Hrm… it sure reads nicely, though…
What if we did this…
class Post < ActiveRecord::Base has_and_belongs_to_many :tags do def to_s map(&:name) * ', ' end end endNow we can just do this in our view to get a comma separated list of tags:
<%= @post.tags %>Or, in script/console:
>> @post.tags.class => Array >> @post.tags.size => 2 >> @post.tags.to_s => "bikes, site"
Hot. Extending associations ain’t just for finders no more.
Update: Wow, sloppy. For the record it’s &:name, not :&name. Fixed it up.
Ah, that’s awesome!
But one question.. What is this: :&name
Thank you! It seems to be a useful idea.
Thaks for the post. Overriding to_s that way has a real “wow factor” into it, but don’t you feel that you’re moving into the model display logic that belongs in the view? If you want your tags semi-colon separated tomorrow you are gonna have to mess with the model and that’s pretty counter-intuitive. I’d rather go with a “display_tags(@post.tags)” kind of helper. What do you think?
Now, doesn’t this completely fall down and crash once you want to display a comma separated list of linked tags? Now to_s would have to know about URLs, which is a no-no in the model. So you’d probably pass a Proc or something that spits out links. Crazy! Fat model and all, I think that kind of stuff still belongs into the V of our little MVC. Even if it means more partials and whatnot.
Burnin.
Hendrik: Sure, if you need links etc then go right ahead and write a helper method, but I think I always end up needing to_s’s for most stuff in the app for various purposes (e.g. link ‘title’s, etc).
Johannes, its how to use the to_proc method that’s a newish addition to Rails
In the map example, it gives you a nice shortcut. Instead of writing: You can just write:I just don’t get extending ActiveRecord associations. I understand what its doing, but it seems like every example is something that should go in a model anyway. What if somewhere in your app you want to get a list of all the tags used on your site and then use your <%= %> trick? Wouldn’t you want your to_s def in the model? Same with find_active. I’m just having trouble envisioning a case where I would ONLY want new functionality available through a specific association (or a few specific associations).
Also, shouldn’t tags_string be a function of tags instead of posts. That feels a little off to me.
mort: My model knows how to turn a string of tags into an array of tags by breaking on the comma (as opposed to on a semi-colon), so it’s okay if it knows how to rebuild a string of tags from an array. Maybe there needs to be a Tag.separator attribute which is set to comma? Yeah, there does.
All: Keep your links out of the model, please. This to_s trick is just a shortcut for a really common scenario. Not every scenario. You still need view helpers. Listen to Tim.
Rube: Yeah, find_active should probably go in the Tag model. I think if you do Post.find(:all).tags.find_active once you’ve defined the method on Tag the association will auto-scope the call to post_id for you, but that’s not the point. The point is you can. In this case, tags_string works on an array of tags—how can you put array logic into a model? You can’t, you need to put it on the array. The association array. to_s is right where it belongs.
Hey, here are two more ideas if you’re on board with Tag being in charge.
Chris, thanks for answering. Actually my point was more about putting display logic in the model, same as Hendrick’s but he put it better. As Tim and you have made clear now we all basically agree. And anyway, reservations aside, overriding to_s that way is undoubtely nifty.
Isn’t it tags.map(&:name), instead of tags.map(:&name) ?
Ok, how about combining this post with your previous one
Would allow you to do:
Unfortunately, with_scope appears to get ignored. Any ideas why?
Brandon: If you just define find_active on Person, you will be able to do organize.people.find_active(:all) just fine.
Minor detail but map(:&name) * ’, ’ should be map(&:title) * ’, ’ I think.
John: Good catch on my typo.
Perhaps the map could be replaced with to_sentence(:skip_last_comma => true)
Chime in.