Here’s an interesting problem I ran into today.
Polymorphic associations in Ruby on Rails are actually quite easy to do, especially in Rails 3.2. If you need a refresher, there’s a great screencast over at Railscasts, which does require a subscription which I highly highly recommend: railscasts.com/episodes/154-polymorphic-association-revised.
However, my problem was a little different, and maybe a special case because I can’t think of many applications this would apply to.
Say I have a Location and a Checkpoint, and the Location and Checkpoint can have notes, posted by Users. I would use a polymorphic association for the Notes to the Location and Checkpoint. But, I also want a single Note, to be posted to many Locations or Checkpoints, which for a single model-to-model relationship I could simply use a join table.
Example, two Locations which are near each other, maybe they’re coordinates, could share a single Note describing the general area, and with the same Note model, one Note may describe multiple Checkpoints.
Solution: Make the join table polymorphic.
Notes model that contains the content and user_id, which I use to associate the User model with.
class CreateNotes < ActiveRecord::Migration def change create_table :notes do |t| t.text :content t.integer :user_id t.timestamps end end end
Note join model that is also polymorphic. notableid and notabletype is the polymorphic attributes used by ActiveRecord.
class CreateNoteJoins < ActiveRecord::Migration def change create_table :note_joins do |t| t.integer :note_id t.integer :notable_id t.string :notable_type t.timestamps end add_index :note_joins, [:notable_id, :notable_type] end end
Next I add the model associations to Location and Checkpoint models.
class Location < ActiveRecord::Base has_many :note_joins, as: :notable has_many :notes, through: :note_joins end
class Checkpoint < ActiveRecord::Base has_many :note_joins, as: :notable has_many :notes, through: :note_joins end
Above I set the notejoins model as the polymorphic association :notable, which will use the notableid and notabletype attributes to assign which model and ID the note join belongs to. Then I set the hasmany association on the Note model, through note_joins. This will let me use such methods as Location.first.notes to pull up all the notes that belong to that location. The same applies to Checkpoint.
In the NoteJoin model I need specify that it belongs to the notable polymorphic association and that it also belongs to a Note. I do so by adding the following.
class NoteJoin < ActiveRecord::Base belongs_to :notable, polymorphic: true belongs_to :note end
Now for the Note model, it should belong to the notable polymorphic association, and also belong to a user(through the user_id attribute).
class Note < ActiveRecord::Base attr_accessible :content, :user_id belongs_to :notable, polymorphic: true belongs_to :user has_many :note_joins end
At this point everything should work, and through the NoteJoin model I can have a single Note belong to many different Locations and even Checkpoints.
Let’s quickly test this in console.
> Location.first.notes.create(user_id: 1, content: "foobar") => #< Note id: 1, content: "foobar", user_id: 1, created_at: "2012-11-30 02:19:24", updated_at: "2012-11-30 02:19:24" > > Location.first.notes => [#< Note id: 1, content: "foobar", user_id: 1, created_at: "2012-11-30 02:19:24", updated_at: "2012-11-30 02:19:24" >]
Great it works, but now I want to see what Locations or Checkpoints this Note belongs to through NoteJoin. To do that I need to update my Note model to include a hasmany locations and checkpoints. Notice I’m using the source and sourcetype option, to pass my polymorphic association :notable, since that’s how we translate which model it belongs to.
And now I can find the locations which my note belongs to.
> Note.first.locations => [#< Location id: 1, name: "foobar", created_at: "2012-11-30 02:19:24", updated_at: "2012-11-30 02:19:24" >]
Another problem came up after this point. If I were to create a Note, how could I easily add many Locations to it? My first thought was to create a method that would update the join table, but I knew there had to be an easier way already built in ActiveRecord to do this.
I did some digging around and found a simple solution:
> note = Note.create(user_id: 1, content: "foobar2") => #< Note id: 2, content: "foobar2", user_id: 1, created_at: "2012-11-30 02:19:24", updated_at: "2012-11-30 02:19:24" > > Location.all.each do |location| note.locations << location end
I’m using << to push location objects into the note.locations array, and ActiveRecord will handle the creation of the NoteJoin record. Pretty neat.