- Establish the many-to-many (or has many through) association in Active Record
In the previous lesson, we saw how to create a one-to-many association between two models using Active Record by following certain naming conventions and using the right foreign key on our tables when generating the migrations.
In the SQL section, we learned about one other kind of relationship: the many-to-many, also known as the has many through, relationship. For instance, in a domain where a cat has many owners and an owner can also have many cats, we needed to create another table to join between those two tables:
In this lesson, we'll learn how to create a many-to-many relationship in Active Record. We'll continue working on our games and reviews domain, but this time we'll add a third model into the mix: a users model. We'll be setting up these relationships:
- A game has many reviews
- A game has many users, through reviews
- A review belongs to a game
- A review belongs to a user
- A user has many reviews
- A user has many games, through reviews
Once we're done setting up the database tables, here's what the ERD will look like:
To get started, run bundle install
, then follow along with the code.
Right now, we've got code for the Game
model (and the games
table), along
with the code for the Review
model (and the reviews
table) from the previous
lesson.
To start, let's add the code we'll need for the User
model as well. We'll start
by generating the migration:
$ bundle exec rake db:create_migration NAME=create_users
Let's create the users
table with a name
column:
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
We'll also need to modify the reviews
table and add a foreign key to refer to
our users
table. Remember, each review now belongs to a specific user. Any
time we create a belongs to relationship, we need a foreign key to establish
this relationship. Let's go ahead and write a migration to update the reviews
table:
$ bundle exec rake db:create_migration NAME=add_user_id_to_reviews
We'll use the add_column
method to update the reviews
table and add a
user_id
foreign key:
class AddUserIdToReviews < ActiveRecord::Migration[6.1]
def change
add_column :reviews, :user_id, :integer
end
end
With that, our migrations are good to go! Run the new migrations to update the database and schema:
$ bundle exec rake db:migrate
Run the seed file as well to populate the games
and reviews
tables:
$ bundle exec rake db:seed
Now that we've updated the database, we can start working on updating our Active
Record models. The first one we'll work on is the Review
model. We want our
model's code to reflect the change we made in the database, so that we can
easily access data about which user left a review. Our Review
model currently
looks like this:
class Review < ActiveRecord::Base
belongs_to :game
end
We now also want our review to know which user it belongs to, so let's add that code as well:
class Review < ActiveRecord::Base
belongs_to :game
belongs_to :user
end
Now, we can create a new Review
instance and associate it with a User
and a Game
. Run rake console
and try it out:
# Get a game instance
game = Game.first
# Create a User instance
user = User.create(name: "Liza")
# Create a review that belongs to a game and a user
review = Review.create(score: 8, game_id: game.id, user_id: user.id)
Just like in the previous lesson, we can access data from the review instance about the associated game; but now, we can also access data about the associated user:
review.game
# => #<Game:0x00007ff71a25f5d0 id: 1, title: "Diablo", genre: "Visual novel", ...>
review.user
# => #<User:0x00007ff71a26fe58 id: 1, name: "Liza", ...>
In Active Record parlance, we refer to this Review
class as a "join" class,
because we use it to join between two other classes in our application: the
Game
class and the User
class. We need this association set up first before
we'll be able to access data about the users directly from their games, and
access data about the games directly from their users.
Let's start with the Game
class. Here's what it looks like right now:
class Game < ActiveRecord::Base
has_many :reviews
end
As a reminder, when we use the has_many
macro, Active Record generates
an instance method #reviews
that we can call on a Game
instance to access
all the associated reviews:
game = Game.first
game.reviews
# => [#<Review:0x00007ff71926dac8 id: 1, ...>, #<Review:0x00007ff71926d960 id: 2, ...>
However, if you'll recall, we updated our tables to support another relationship:
- A game has many users, through reviews
What this means for us in code is that it might be convenient to access a list of all the users who left reviews for a specific game from the game instance itself. In other words, it would be nice to be able to use a method like this to see all the users associated with a specific game:
game.users
# => [#<User>, #<User>]
Writing the SQL out to access this relationship would be a bit of a pain; we'd
need to join the reviews
table in order to access the correct users for a
specific game:
SELECT "users".*
FROM "users"
INNER JOIN "reviews"
ON "users"."id" = "reviews"."user_id"
WHERE "reviews"."game_id" = 1
Luckily for us, Active Record's has_many
macro also can be used to establish
this relationship and write that SQL for us! Here's how we can use it:
class Game < ActiveRecord::Base
has_many :reviews
has_many :users, through: :reviews
end
By adding this second has_many
macro, and using the through:
option, we're
now able to use that #users
instance method with our games. Try it out
(remember to exit your console and re-start it after updating your Game
class):
game = Game.first
game.users
# => [#<User:0x00007f96813a5d58 id: 1, name: "Liza", ...>]
We can now use Active Record to go through the join model, Review
, from
the Game
model, to return the associated users, all without writing any SQL
ourselves. Pretty cool!
There are a couple important things to note when using the has_many
macro with
the through:
option. Order matters — you must place the first has_many
that
references the join table above the second has_many
that goes through that
join table. This code won't work:
class Game < ActiveRecord::Base
has_many :users, through: :reviews
has_many :reviews
end
Active Record won't know how to go through
the reviews
table until you
create the has_many :reviews
association.
Also, these are still just Ruby methods, so it might help to see them written out with parentheses to understand the syntax:
class Game < ActiveRecord::Base
has_many(:reviews)
has_many(:users, through: :reviews)
end
When calling has_many
, we're passing in a first argument of a symbol that
refers to the table name in our database (:users
). In the second argument,
we're passing a key-value pair, where the key is the through
option, and the
value is the :reviews
symbol, which refers to the #reviews
method from the
first has_many
. Phew!
While we're at it, we can also set up the inverse relationship:
- A user has many reviews
- A user has many games, through reviews
This will give us the ability to access all reviews for a particular user, as
well as all the games a particular user has reviewed. The code will look similar
to what we added to the Game
model. Update the User
class in
app/models/user.rb
with the following code:
class User < ActiveRecord::Base
has_many :reviews
has_many :games, through: :reviews
end
Now, in the console, you can access a review for a user, as well as a list of the games they have reviewed:
user = User.first
user.reviews
# => [#<Review:0x00007fc2a2ac01b8 id: 147, score: 8, ...>]
user.games
# => [#<Game:0x00007fc2a2b53710 id: 1, title: "Diablo", genre: "Visual novel", ...>]
Success! All of our models are now associated correctly, and have methods available that make it convenient for us to access data across multiple database tables using the primary key/foreign key relationship. Our Ruby code now reflects the associations we established:
The power of Active Record all boils down to understanding database relationships and following certain naming conventions. By leveraging "convention over configuration", we're able to quickly set up complex associations between multiple models with just a few lines of code.
The one-to-many and many-to-many relationships are the most common when working with relational databases. You can apply the same concepts and code we used in this lesson to any number of different domains, for example:
Driver -< Ride >- Passenger
Doctor -< Appointment >- Patient
Actor -< Character >- Movie
The code required to set up these relationships would look very similar to the code we wrote in this lesson.
By understanding the conventions Active Record expects you to follow, and how the underlying database relationships work, you have the ability to model all kinds of complex, real-world concepts in your code!