What is a join model? Or has_and_belongs_to_many versus has_many through

Figuring out the associations between your models can be one of the hardest things to wrap your head around.

Rails’ ActiveRecord library presents your database as a series of objects; the relations of a relation database converted into associations. Which, on the surface, makes dealing with databases a piece of cake. But in reality it can lead to confusion.

What’s the difference between a has_many, a has_many through and a has_and_belongs_to_many?

When should you use belongs_to and when should you use has_one?

And what is a join model?

Last time we ran through a brief introduction to how relational databases work – and how Rails uses foreign keys and relations to define belongs_to, has_many and has_one associations.

But that leaves has_many through and has_and_belongs_to_many – words which leave many newbies utterly confused.

Returning to our previous example, we have people performing workouts.

Let’s say that each workout consists of multiple exercises – Strength Training involves bench presses, deadlifts and squats, Power Building involves deadlifts, leg presses, and pulldowns, Fat shredding involves deadlifts, pull ups and bench presses.

We could represent exercises as so:

If you look carefully each workout consists of many exercises. So our first guess would be that Workout has_many :exercises and Exercise belongs_to :workout. And our exercise table would need a workout_id foreign key so that Rails can link the two.

However that’s not quite right. Workouts do indeed have many exercises. But look at Strength Training and Power Building – exercises can belong to many workouts. Has many and belongs to many – sound familiar?

So our Rails representation would actually look like this:

We would then use it like so:

Now you may have noticed something here … if we’d gone with a has_many association we would have needed a workout_id foreign key on the exercises table so that Rails could store details about the association. But we’ve not added anything. How does Rails store this association data?

What we need here is a join table (continued below).

Do you know what to do but not how it works?

Ever wanted to understand why Rails views work the way that they do? Why variables from your controllers are visible inside your views?

Sign up below to get a free 5 part email course on how Ruby on Rails view rendering works and gain a deep understanding of the Rails magic.

We will send you the course, plus the occasional update from this web-site. But no spam, we promise, and it's easy to unsubscribe

In Rails, the join table is named after the two models involved in alphabetical order. In this case it would be called exercises_workouts and would need foreign keys to both the workouts table and the exercises table. This means that each exercise can be related to many workouts, each workout can be related to many exercises, as shown here:

Also remember that we said that primary keys did not need to be integers. In this case, as we’ll never refer to an ExerciseWorkout (they only exist in the context of an existing Workout or Exercise) we don’t need an individual primary key.

This is great – workouts and exercises linked together. And for many cases this would be enough.

But something’s missing. A superior workout would specify the order you do the exercises in – start with something easy and gradually build up as you go. The trouble with a has_and_belongs_to_many association is it lets you group models together, but you can’t specify any information about how they are grouped together. What we really need is a way of specifying the order of exercises within our workout.

So let’s scrap our has_and_belongs_to_many association and start again. We’ve got our three main tables and models – people, workouts and exercises. But we need some way of specifying that workouts have many exercises, exercises have many workouts and when a workout and exercise are linked together they should be in a particular order.

Instead of a mere join table, we need a join model.

So let’s add a new model into the mix. I’m going to call it WorkoutExercises and it’s going to be created like this:

And the code will look like this:

We have our new “join model” that sits between exercises and workouts – it belongs to both, meaning that you can use WorkoutExercises to relate multiple workouts to multiple exercises; we’ve got ourselves into the same position as using a has_and_belongs_to_many association but just with more code.

But we’ve also got a position field on WorkoutExercise – and a scope defined that lets us see that ordering @strength_training.workout_exercises.in_order will list those workout exercises in the order specified.

Adding exercises to a workout is a bit more involved now…

We create a new instance of our join model, attached to the workout and setting both the exercise and position.

And now we fulfil all our requirements – workouts have many exercises, exercises belong to many workouts and we can choose which order exercises are performed in for a given workout.

Just one thing left – every time we want to get a list of exercises associated with a given workout we have some slightly unwieldy code to work through:

And this is where the mysterious has_many through steps in. We already know that a Workout has many WorkoutExercises and a WorkoutExercise belongs to an Exercise. Because we can follow the associations in this way, we can ask Rails to shortcut things for us.

Now our list of exercises becomes:

So to recap:

  • If you want to link many of model A with many of model B and don’t really care how they’re grouped then use has_and_belongs_to_many and create a join table
  • If you want to link many of model A with many of model B but there’s information you need to store specific to that association then use multiple has_many associations with a join model inbetween
  • If you have two models that are indirectly related with another model in-between then you can specify a has_many through association to short-cut access across the models.

Do you know what to do but not how it works?

Ever wanted to understand why Rails views work the way that they do? Why variables from your controllers are visible inside your views?

Sign up below to get a free 5 part email course on how Ruby on Rails view rendering works and gain a deep understanding of the Rails magic.

We will send you the course, plus the occasional update from this web-site. But no spam, we promise, and it's easy to unsubscribe