Many-to-many relationships in Rails can be confusing at first. There are a couple different ways to handle them, depending on what you need. Let’s say you want to organize your mp3 collection. Artists obviously have multiple songs, but songs also have multiple artists.
has_and_belongs_to_many
The simpler way to do this is using HABTM (has_and_belongs_to_many) to define your relationships. Here are the models:
# app/models/song.rb class Song < ActiveRecord::Base has_and_belongs_to_many :artists end # app/models/artist.rb class Artist < ActiveRecord::Base has_and_belongs_to_many :songs end
And here are the migrations:
# db/migrate/create_artists.rb
class CreateArtists < ActiveRecord::Migration
def self.up
create_table :artists do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :artists
end
end
# db/migrate/create_songs.rb
class CreateSongs < ActiveRecord::Migration
def self.up
create_table :songs do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :songs
end
end
# db/migrate/create_artists_songs.rb
class CreateArtistsSongs < ActiveRecord::Migration
def self.up
create_table :artists_songs, :id => false do |t|
t.integer :artist_id
t.integer :song_id
end
end
def self.down
drop_table :artists_songs
end
end
Notice a couple things about the migrations. First, I had to manually create a join table migration. Its name must be a combination of the two tables being joined, in alphabetical order. This is how Rails is able to figure out the join table’s name automatically. Also notice that I had to specify :id => false in the join table definition. Primary keys in join tables cause big problems.
Now, if I have an object song that is an instance of the Song class, I can say song.artists and get a list of the artists associated with this song. I can do the same for artists. Yay!
has_many :through
What if we wanted to track what instrument the artist played on a given song? This doesn’t go in the artists table, because it’s different for every song. It doesn’t go in the songs table either, because each artist probably played something different. It needs to go in the join table, but there’s a problem. There’s no way in your Rails code to access the information stored in the join table itself.
What we really have here is more than just a join table – it’s another model all its own. Let’s call it a contribution. Each artist’s contribution to a song is unique. I might play nose flute in a rendition of Herbie Hancock’s Watermelon Man, but I play the slide whistle when recreating the theme song from the Krusty the Klown show.
Recreating the models from scratch looks like this:
# app/models/artist.rb class Artist < ActiveRecord::Base has_many :contributions has_many :songs, :through => :contributions end # app/models/contribution.rb class Contribution < ActiveRecord::Base belongs_to :artist belongs_to :song end # app/models/song.rb class Song < ActiveRecord::Base has_many :contributions has_many :artists, :through => :contributions end
The migrations look like this:
# db/migrate/create_artists.rb
class CreateArtists < ActiveRecord::Migration
def self.up
create_table :artists do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :artists
end
end
# db/migrate/create_contributions.rb
class CreateContributions < ActiveRecord::Migration
def self.up
create_table :contributions do |t|
t.references :artist
t.references :song
t.string :instrument
t.timestamps
end
end
def self.down
drop_table :contributions
end
end
# db/migrate/create_songs.rb
class CreateSongs < ActiveRecord::Migration
def self.up
create_table :songs do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :songs
end
end
I didn’t have to manually create the join table this time. I created the three models calling script/generate, and the tables were created automatically. Now, with an object artist that is an instance of the Artist class, I can get a list of contributions with artist.contributions. I can get a list of songs just as easily with artist.songs, and Rails will find them through the contributions table. If I want the instrument the artist used on the first song, I can say artist.songs[0].contribution.instrument, and so on.
Deciding Which to Use
If two models have a many-to-many relationship and you need to track info about the relationship itself, use has_many :through. In fact, sometimes it’s wise to do this anyway, if you anticipate needing that later. HABTM should only be used when the relationship itself will never need to track data of its own.
Tags: developers, habtm, has_and_belongs_to_many, has_many, has_many :through, Rails, Ruby, ruby on rails
January 16, 2010 at 1:14 pm |
Interesting, however, wouldn’t you have a problem if you don’t know what someone played on an album? It looks to me like you basically made the join table more useful and put in detailed info about the album. But there still needs to be something in the contributions table regardless to link up everything.
So, basically, its the same thing isn’t it? There will be data in contributions regardless of whether or not you actually know what they played.
January 16, 2010 at 1:27 pm |
If you saw my post when it first came out, some of the model/migration definitions were a little messed up due to wordpress “sanitizing” some of the code.
You don’t have to fill in the instrument info for every record. And you’re right, the contributions table is still acts as a join table, albeit with more info. But if you want to *use* the extra fields in the join table, you have to use the has_many :through approach. Otherwise there’s no way to set or get the instrument info within Rails, aside from hard-coding SQL.
January 16, 2010 at 2:40 pm |
Also this is a little strange:
class CreateArtistsSongs false do |t|
t.integer :artist_id
t.integer :song_id
end
end
Wouldn’t it be:
class CreateArtistsSongs < ActiveRecord::Migration
def self.up
create_table :artists_songs do |t|
t.references :artists
t.references :songs
end
end
January 16, 2010 at 2:45 pm |
Yeah, that was the mess-up I mentioned before. It’s fixed now.
January 16, 2010 at 2:57 pm |
No no, in the first example. Wouldn’t you use t.references rather than integer?
create_table :artists_songs, :id => false do |t|
t.integer :artist_id
t.integer :song_id
end
But rather use t.references instead? And would you need to disable the :id still?
January 16, 2010 at 3:33 pm |
They’re interchangeable. Honestly, I’ll probably keep using “t.integer whatever_id” because it’s more explicit. All t.references does is isave you from typing “_id”.
January 16, 2010 at 3:59 pm |
Ok, but do you need to still do that :id => false thing? I mean, if there are t.references to the linking tables……
January 16, 2010 at 4:18 pm |
The *only* thing that “t.references :something” does is convert it to “t.integer :something_id” – it’s merely shorthand, and not that short. It doesn’t do anything else.
You still need “:id => false”, to keep the join table from having a primary key. If you leave the primary key in (which is the default) and you’re using a version of Rails older than 2.3.4, it will fail with a cryptic message several inserts in. As of 2.3.4, I actually contributed a change to the Rails core so that it tells you the primary key is the culprit.
Migrations merely define tables in a database – they don’t actually create relationships between models. That’s what the ActiveRecord methods (has_one, has_many, has_and_belongs_to_many, and belongs_to) are doing for you in the model itself. But the foreign keys do have to be in the right places, and all tables involved have to have the correct names and settings.
September 9, 2010 at 4:16 pm |
OK your article is nice. However I can not find anywhere how to set the extra field. E.g. artist.songs[0].contribution.instrument = “piano” does not return any error but the field stays nil.