Rails 3 Many to Many

28 Mar

Working on the Target Cancer site has given me a pretty good grip on creating 1-to-many relationships in rails. For each Song there are many comments and likewise for each Post.

I then wanted to add some functionality to allow for Users to be entered and for those users to be able to choose Songs in which to Perform. Each user could perform on many songs and each song would have several users (let’s call them Players) performing them.

That much I was able to visualize clearly, but actually putting that into Rails terms was fairly intimidating, and after a few sketches and some fits and starts, I finally had to start doing some hard research as to how, exactly, this would be implemented.

So I created a simple Rails app in which to model this relationship exclusively. I also found some good tutorials to get the models and migrations sorted out, my favorite of which can be seen here.

In order to give the Songs and Player models access to each other in a many-to-many fashion I chose to go the route (no pun intended) of has_many :through which, if you’ve read a bit on Rails relationships, you’ll know is a more flexible alternative to the method using has_and_belongs_to_many. The ‘has_many’ route involves building a 3rd model to mediate between the two. Instead of just a simple table to track the foreign keys, the ‘has_many’ approach lets you add columns to the table in addition to the foreign keys, which can be very useful for tracking information that relates to the relationship, if that doesn’t sound too strange. I put a ‘role’ column in there, and called the new model ‘Performances’, the purpose of ‘role’ being to store the role a given Player would play in the performance. So Player ‘John Doe‘ might have a Performance role of ‘bass and vocals’, While ‘Billy Zoom‘ may have the role of ‘blazing rhythm and lead guitar’.

So the tutorials got the ball rolling, but then left me still pretty clueless as to how to manipulate these objects usefully in my own application. Testing in the rails console, I found that I could quite easily add a Player object (selected using Player.find(:id)) to a song (selected in the same fashion) simply by doing the following:

song << player

I thought that was pretty neat, but then found it difficult to update the role which the give Player was playing in that song. Using the method above, the role remained ‘nil’.

After quite a bit of fumbling, I finally resolved to contact the Boston Ruby Group for assistance. My hat is off because the response time and detail with these folks is PHENOMENAL! One kind user clued me into the following:

song.performances.create(player: player, role:"guitar")

All in one line, save to the database and everything! This began to shed light on another problem I was having, which was getting Song and their associated Players (and roles) to display nicely on a view. I was trying the following code and getting errors:

<% @songs.each do |song| %>

      <%= song.title %><br/>
        <% for i in 1..song.players.length %>
          <%= song.players[i].name %>, 
          <%= song.performances[i].role %>
        <% end %>
      <% end %>

This was resulting in a “undefined method `name’ for nil:NilClass” error, and I knew in any case that my approach was wrong and there had to be an easier way. Sure enough there was, and after taking some time to digest the response I got from the BRG folks, I came up with the following:

<% @songs.each do |song| %>
      <u><%= song.title %></u><br />
      <% song.performances.each do |performance| %>
          <%= performance.player.name %> -
          <%= performance.role %><br />
      <% end %><br />
<% end %>

This spits out everything I wanted as neatly as can be:

Songs

Miserlou
Kent – guitar
John Doe – bass guitar

Louie Louie
Kent – tambourine
Sally – bazooka and lead vocal
John – drums and percussion

Love Me Do
John Doe – bass guitar and lead vocal

Happy me! Big thanks to the BRB for wisdom and guidance on this (and other soon-to-be-posted) issues. I’m amazed at how easily (once you know how) Rails lets you access all the columns in the join between the two tables, from either side (or the middle for that matter). This is still sinking in, but is a powerful lesson for me.

I don’t have the code for this on GitHub, as it’s just a sketch, but for the curious I’m putting the Model code here:


class Song < ActiveRecord::Base
  attr_accessible :title
  has_many :performances
  has_many :players, :through => :performances
end

class Player < ActiveRecord::Base
  attr_accessible :instrument, :name
  has_many :performances
  has_many :songs, :through => :performances
end

class Performance < ActiveRecord::Base
  # attr_accessible :title, :body
  attr_accessible :player, :song, :role
  belongs_to :player
  belongs_to :song
end

The schema code (Rails puts this in a single file very conveniently) is here:

ActiveRecord::Schema.define(:version => 20130328181909) do

  create_table "dogs", :force => true do |t|
    t.string   "name"
    t.integer  "age"
    t.string   "sex"
    t.string   "breed"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
  end

  create_table "performances", :force => true do |t|
    t.text     "role"
    t.integer  "player_id"
    t.integer  "song_id"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
  end

  create_table "players", :force => true do |t|
    t.string   "name"
    t.string   "instrument"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
  end

  create_table "songs", :force => true do |t|
    t.string   "title"
    t.datetime "created_at", :null => false
    t.datetime "updated_at", :null => false
  end

end

Never mind that ‘Dogs’ table behind the curtain. That was for troubleshooting another issue which will appear in another post!

Comments are welcome!