Hi, thanks for taking the time to read my article. Visit aaronvb.com for more reads, and contact me below if you have questions.


You can find me on GitHub, Twitter, Instagram, Flickr, or email at bokhoven@gmail.com

A Polymorphic Join Table

Written Aaron Van Bokhoven on Nov 29, 2012

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: http://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.

Note join model that is also polymorphic. notable_id and notable_type is the polymorphic attributes used by ActiveRecord.

Next I add the model associations to Location and Checkpoint models.

Above I set the note_joins model as the polymorphic association :notable, which will use the notable_id and notable_type attributes to assign which model and ID the note join belongs to. Then I set the has_many 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.

Now for the Note model, it should belong to the notable polymorphic association, and also belong to a user(through the user_id attribute).

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 has_many locations and checkpoints. Notice I’m using the source and source_type 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

I’m using « to push location objects into the note.locations array, and ActiveRecord will handle the creation of the NoteJoin record. Pretty neat.