<body><script type="text/javascript"> function setAttributeOnload(object, attribute, val) { if(window.addEventListener) { window.addEventListener("load", function(){ object[attribute] = val; }, false); } else { window.attachEvent('onload', function(){ object[attribute] = val; }); } } </script> <iframe src="http://www.blogger.com/navbar.g?targetBlogID=7625526986034013157&amp;blogName=Tim%2C+the+Enchanter&amp;publishMode=PUBLISH_MODE_HOSTED&amp;navbarType=BLUE&amp;layoutType=CLASSIC&amp;searchRoot=http%3A%2F%2Ftim.theenchanter.com%2Fsearch&amp;blogLocale=en&amp;homepageUrl=http%3A%2F%2Ftim.theenchanter.com%2F" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" height="30px" width="100%" id="navbar-iframe" allowtransparency="true" title="Blogger Navigation and Search"></iframe> <div></div>

About

I'm a ruby developer passionate about developing clean code that makes for programming happiness. I'm also am passionate about freedom, liberty, and capitalism, and enjoy jamming out some good rock or jazz on the piano.

I'm a family man and a I'm a member of The Church of Jesus Christ of Latter Day Saints (AKA the "Mormons") and I wield a strong testimony of my Savior Jesus Christ (yes we're Christians).

I'm currently employed by:

How to has_many :through a has_many :through Thursday, October 2, 2008 |

So, I’ve got 3 models:

  User -< UserConversation >- Conversation -< Message

  (where -< is has many, >- is belongs to, ... etc)

I wanted, in this application, to be able to filter to all messages for a given user.

  User#messages.find(message_id)

So, naturally, I first reached for this:

  class User < ActiveRecord::Base
    ...
    has_many :user_conversations
    has_many :conversations, :through => :user_conversations
    has_many :messages, :through => :conversations
  end 

  class UserConversation < ActiveRecord::Base
    belongs_to :user
    belongs_to :conversation
  end

  class Conversation < ActiveRecord::Base
    has_many :user_conversations, :dependent => :destroy
    has_many :messages, :dependent => :destroy
  end

  class Message < ActiveRecord::Base
    belongs_to :conversation
  end

Which… is completly WRONG and yields results like the following:


  >> User.first.messages

  ActiveRecord::StatementInvalid: SQLite3::SQLException: no such
  column: conversations.user_id: SELECT "messages".* FROM "messages"
  INNER JOIN conversations ON messages.conversation_id =
  conversations.id    WHERE (("conversations".user_id = 1)) 

So, apparently ActiveRecord doesn’t support joins like that. Sure, I could resort to manually specifying the finder_sql in the join and skip the has_many :through business, but then I’d lose the that cool ability to apply additional scopes after this – major FAIL.

So, I came up with a clever way to get around it.

  class User < ActiveRecord::Base
    ...
    has_many :user_conversations
    has_many :conversations, :through => :user_conversations

    def messages
      Message.for_user(self)
    end
  end

  class Message < ActiveRecord::Base
    belongs_to :conversation
    
    named_scope :for_user, lambda { |user| user = user.id if user.is_a?(User); { :joins => {:conversation => :user_conversations}, :conditions => ["user_conversations.user_id = ?", user]}}
  end

And… now we can do things like:

  user.messages.find(1)
  user.messages.matching_subject("boogy man")

etc., etc. Hurray for named_scope. So.. there's still a few issues. Namely, it's not REALLY an association - no "create" or "build" methods.

Dear web: what do think about this? Got a better way to implement the above scenario?

Labels: