dynamic has_many association (for lack of a better name)

Yesterday I had one of those moments in Rails where I knew there must be a really good way to do something, there had to be.  But all I could think of was the brute force way.

Technically speaking, I wanted to create collections of child objects on a parent object where the membership of each collection depended on whether the parent and the child shared an attribute in common.

In simpler terms, imagine you have a group blog.  Each post in the blog is written by one of several authors.  A Post belongs to an Author.

Then imagine that each Post can have many Comments.  Once again a comment belongs to an Author.

Each Post could have many Comments.  Some of the comments will be owned(written) by the same Author as the Post and other Comments will be owned by other Authors.  In your application, you may find it valuable to know the difference, and, moreover, to be able to access each group of comments for each Post as a collection.

I’m sure there are many working ways to do tis, but here’s the way I stumbled upon last night and I think it’s sahweeeet.

First, let’s look at our Post model before adding these collections:

<br />
class Post &lt; ActiveRecord::Base<br />
  belongs_to :author<br />
  has_many :comments<br />
end<br />

And the Comment model:

<br />
class Comment &lt; ActiveRecord::Base<br />
  belongs_to :post<br />
  belongs_to :author<br />
end<br />

I won’t show the Author model. All that is important about it is that it has an id and that it can own Posts or Comments or both.  Now say I want to actually differentiate between comments created by the Author of the Post and comments created by other Authors, but I don’t want a separate model for each type of comment.  I just want the one Comment model.  Here’s what I mean:

For a given Post, I want to access all the Comments that are owned by the same Author that owns the Post.  Let’s call these AuthorComments,

and

for a given Post, I want to access all the Comments that are owned by someone other than the Author of the Post.  Let’s call these ReaderComments.

As I mentioned, there are probably several valid ways to do this.  A few that come to mind are 1) Actually having two seperate model classes to do it. 2) Using find by methods on the post. 3) Using named scopes on Comments and retrieving the lists via @post.comments.byauthor or @post.comments.byreader

I chose a road less traveled.  In the name of less code means less to maintain (as long as it’s readable), I searched high and low for a way to use ‘has_many’ to accomplish this job.  I ended up finding it in this other blog posting: http://www.dweebd.com/ruby/has_many-with-arguments/ by a guy who’s name might be Duncan Beever.  He was trying to do something a little different, but the nugget I needed was in there.

Below is the updated Post model with the magic.  These are litterally the only two lines I had to add to achieve the functionality I’ve been describing:

</p>
<p>class Post &lt; ActiveRecord::Base<br />
belongs_to :author<br />
has_many :comments<br />
#note: learned out to create associations below<br />
#from http://www.dweebd.com/ruby/has_many-with-arguments/<br />
has_many :author_comments,<br />
         :class_name=&gt;'Comment',<br />
         :conditions=&gt; 'author_id != #{author.id}'</p>
<p>has_many :reader_comments,<br />
         :class_name=&gt;'Comment',<br />
         :conditions=&gt;'author_id = #{author.id}'<br />
end<br />

As Duncan says in his blog, “But all is not lost! A poorly-documented feature of the association methods is the lazy evaluation of conditions.”  The single quotes on the condition clause tell the interpreter to wait to evaluate the contents until it needs to. When the conditions clause is evaluated, the author method will be called on the comment.

When this worked the first time I tested it, I couldn’t believe it.  So I double checked the SQL generated when accessing the list of AuthorComments for a Post, and this is what I saw:

</p>
<p>SELECT * FROM &quot;comments&quot; WHERE (&quot;comments&quot;.post_id = 15 AND (author_id = 44))</p>
<p>

It’s getting all the comments that belong to a particular post that shares the same author_id as the comments it’s getting!  My advice to anyone out there that is looking to try this is to only use this technique if there is no internal difference in behavior between the objects in the two collections.  If the objects within the two lists need to be different in any way other than their name, then they should really have their own model classes.

Thanks for reading.

–Jon Christensen

Advertisements

One Response to “dynamic has_many association (for lack of a better name)”

  1. sms na święta na nowy rok

    Hello! Someone in my Facebook group shared this website with us so
    I came to look it over. I’m definitely loving the information. I’m bookmarking and will be
    tweeting this to my followers! Terrific blog and terrific style and design.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s