The “has_and_belongs_to_many” association and its close friend the “has_many through” association is one of the things that consistently puzzles and confuses newcomers to Rails.
And there’s a simple reason for this … whilst in Rails it is a single association, in the database it represents two database relations and a secret, hidden database table. Couple that with the fact that many experienced Rails developers treat “has_and_belongs_to_many” as the deprecated, weaker sibling of “has_many through” when they actually represent similar but different things and it’s no wonder people get frustrated.
The best way to understand what’s going on under the hood is to think in terms of relations and take an actual look at the database. So let’s run through an example:
Our application is going to represent people assigned to teams. Each team has many people assigned to it and each person can be assigned to many teams. But, importantly, we only care that someone is assigned to a team, we don’t care what they’ll actually be doing on that team.
To start with, we need a people table:
id | name |
---|---|
1 | Hannah |
2 | James |
Pretty simple, huh?
We also need our list of teams:
id | name |
---|---|
1 | Team A |
2 | Team B |
Imaginative names in this organisation!
Relational databases, as a matter of course, only contain “simple” data types; strings, integers and dates etc. Ideally we would model team assignments as a pair of arrays; a person has an array of teams to which they belong, a team has an array of people who are assigned to it. But we can’t do that directly; instead, Rails cheats, using an intermediary table:
person_id | team_id |
---|---|
1 | 1 |
1 | 2 |
2 | 2 |
If you look at this carefully you may spot that Hannah is assigned to Team A and Team B, while James is just on Team B.
This third table will be called people_teams (the names of the two tables in question in alphabetical order) and it doesn’t have its own ID column (which you can specify by passing id: false
in your migration).
Our models then look like this:
1 2 3 4 5 6 7 |
class Person < ActiveRecord::Base has_and_belongs_to_many :teams end class Team < ActiveRecord::Base has_and_belongs_to_many :people end |
And Rails goes out of its way to make these look like arrays; to add a person to a team you would do this: @team_a.people << @james
or @james.teams << @team_a
(whichever makes more sense in your context) and both team and person will be updated accordingly.
So, to recap, relational databases don’t usually have a native array type. To get over this, ActiveRecord simulates one, letting you associate models with other models as if they were connected through arrays.
Contrast that to “has_many through” which is similar but represents a fundamentally different concept.