Tuesday, September 25th, 2007...4:02 pm
Polymorphic, has_many :through join model
Jump to Comments
Here’s the problem I was trying to solve today. I’ve got a data model that includes users and messages. Each user can have many messages. Each user can have many friends. Generally, a user will want to display all of his friends’ and his own messages together on a single “board” of sorts. So far so good. That’s fairly simple to implement. Here’s the complexity, though:
On each user’s board, I want them to be able to hide certain messages (written by himself or by others), make others sticky so they never scroll off the board, and do the same for users (ignore some users and give the thumbs up to others). In this article I’ll describe how I did that, then DRY’ed up what I did and extracted it into a little module so that I can easily reuse the functionality if I need to.
Specs first
In the spirit of defining what I want to do before I actually set about doing it, here’s what I want to be able to do, represented by some unit tests. Yes, yes, I could have done it in rSpec, but I haven’t learned how to use rSpec yet, so that’s still over the horizon. Anyway, it’s the approach that counts, not the tool. So without further ado, here’s the gloriously simple expected behaviour:
end
Nice and simple, right? That’s the way it always should be when writing ruby code, with Rails or otherwise. There may be some optimisations required later to make sure that this actually performs under load, but those will depend on specific use cases. I’ll do a later article on optimisation.
On a brief positive note about test-first-development/behaviour-driven-development, before I wrote those tests I didn’t really picture how this interaction business would work, so this helped me put something to paper and aim for that. Thanks to all the BDD/TDD gurus out there for pushing this idea! It really does make life better.
Step 1: The model
The model took a little bit of thought and research to make sure I wasn’t designing this in a way that wouldn’t work - and at the same time to ensure I wasn’t too shy and didn’t just avoid very nice ActiveRecord constructs like has_many :through.
What it boils down to is: we need a join table between users and messages, and perhaps another join table between users and users. This needs to be a “rich” join, with extra values, as there are at least two different types of interactions involved in each join. In a pure database world, this would probably end up being 4 join tables. But we’re using Rails, and to use Rails properly you need to think in an object-centric way rather than a database-centric way.
Rails actually provides a neat shortcut to simplify this even further: polymorphic associations. So we only need one join table. It will join users to either other users or messages. Here’s the model for it:
script/generate model Interaction victim_id:integer victim_type:string user_id:integer interaction_type:string created_at:datetime updated_at:datetime
This results in the following migration being created:
create_table :interactions do |t|
t.column :victim_id, :integer
t.column :victim_type, :string
t.column :user_id, :integer
t.column :interaction_type, :string
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
drop_table :interactions
end
end
Essentially, I have tied the interaction to a user and to a victim (I like dramatic naming conventions). In addition, there’s an interaction_type string that can be set to things like “ignored”, “sticky”, etc, to mark the type of interaction.
Step 2: Make the model work for messages
This will seem very easy here, but actually it took me quite a long time because I had never used many-to-many joins in rails, and I hadn’t used polymorphic joins either. What it all boiled down to is that I needed to add a few things to the Interaction model and the User model, but didn’t need to do anything in particular to the Message model (as long, that is, as you don’t need to go back from messages towards users, which I don’t). Here’s my Interaction model:
end
And the User model (I’ve removed a lot of the irrelevant bits, obviously…):
end
I built the ‘ignored’ and ’sticky’ messages associations, and those worked well. The relevant tests passed with flying colours. However, before I could get started on the user associations (ignore user and highlight user), my DRY itch started bothering me. I was just about to create another 2 quasi-identical bits of code. Surely I should define some sort of new has_something_or_other to use in my own class(es) instead of copy-pasting that code.
Even more importantly, this is not very readable. This is important, because it’s actually not good to DRY things out if they’re only used in a single place (which this is, since it is only applicable to users). However, re-factoring for readability is generally not a bad idea. How is it hard to read? If you try to read this in English, you’ll see what I mean: “has many ignored messages through interactions with source victim with source type message on the condition that interaction type is ignored”.. phew, what a mouthful! How should it read? How about: “has interaction of type ignored with model message”?
Step 3: Re-factor into DRY state and reuse happily ever after
I took a leaf from Rails’ very own ActiveRecord (don’t you just love having access to all the source) and did exactly that. I created “has_interaction” and put it in a module in the lib directory, as follows:
base.extend(ClassMethods)
end
has_many (args[:type] + “_“ + args[:model].downcase.pluralize).to_sym,
:through => :interactions,
:source => :victim,
:source_type => args[:model],
:conditions => “interaction_type = ‘‘“
end
end
end
It’s simple and neat. The only not-so-neat bit is the bit where i construct the symbol. If anyone has any suggestions to simplify that, please do let me know!
With this, my User model is even simpler than before, and extremely readable. Moreover, I can create a lot of these interactions, and I can make them between users and any other objects, without having to copy boilerplate code everywhere. Here’s the new User object, with all four interactions set up. It passes the tests/specs, of course!
end
And that’s it for this one. If I’d found a tutorial like this one when I started researching this, I would probably have saved myself several hours of googling and reading “around” the subject. Unfortunately it seemed very difficult to locate those “more complicated” has_many tutorials! So I’ve put this one together for the benefit of anyone else who has to solve the same problem again. Thanks for listening in, please do leave a comment if you liked it, or have any suggestions as to how I should improve this code even further!
Actually…
Now that I think of it, I probably should not have extracted this has_interaction method into a module, since it is only used by users. However, I was not able to make it work in the intended way without putting it in a module. Again, if anyone has any suggestions…
Update:
Please check out the follow-up article, Polymorphic has_many :through enhanced
2 Comments
September 26th, 2007 at 5:39 pm
[…] Polymorphic, has_many :through join model […]
December 12th, 2007 at 5:42 am
Thanks… This actually was almost exactly what I was looking for. Made some modifications and implemented one of Josh Susser’s other methods “push_with_attributes” and it works great.
Thanks for posting!!!
-J
Leave a Reply