- Differentiate between a one-to-many and a many-to-many relationship
- Describe the role of a join table in a many-to-many relationship
- Create a Model to represent a join table
- Use the
has_many :throughassociation to connect two models via a join model in Rails - Use a many-to-many relationship to implement a feature in a Rails application
When we think back on what we have learned so far in this unit, we now have the ability to model real world entities and their relationships, and we have built web applications that have persisted data within these models.
Q. Reviewing what we have learned about relational databases and ActiveRecord, what has been the predominate relationship we have used so far?
Up to this point, we have focused on domains that only have two models, i.e. Artists, and Songs, which in turn have a strict one-to-many relationship. At its core, we expressed these relationships with ActiveRecord methods, and linked the tables via a foreign key on the child table.
This type of relationship is probably the most common, but today we will be looking at another widely used and very useful relationship that will help build out additional features within our Rails apps.
Put simply, Many-to-Many relationships arise when one or more records in a table, has a relationship with one or more records in another table.
Many-to-many relationships are fairly common in applications. Some examples include:
Postscan be sorted into multipleCategories,Categoriescontain manyPosts.Theaterscan show manyMovies, andMoviesmay appear in manyTheaters.Playlistscontain manySongs,Songscan be on multiplePlaylists.Doctorscan have manyPatients, aPatientcan have more than oneDoctor
Unlike one-to-many relationships, we can't just add a foreign key to one of the
two tables (the belongs_to table) to store these associations. We'd run into a
problem where the column would need to store multiple ids, rather than just the
one id in a one-to-many relationship.
Instead, we must create a new table, a join table to store these associations.
A join table is a separate intermediate table in our database whose job is to store information about the relationship between our two models of the many-to-many. For each many-to-many relationship, we'll need one join table.
Why are they called "join tables"? On a database level, join tables are created using SQL methods like
INNER JOINandOUTER JOIN. Learn more about them here.
Each join table should have, at minimum, two foreign_key columns. Each foreign key will represent one of the tables it's joining. In the example of Doctors and Patients, we would create a new join table that has a doctor_id column and a patient_id column.
We can also add columns as needed to store additional information about the relationship. For example, we may choose to add a date_of_visit column, which stores a datetime value representing when the appointment is, and could be different for each doctor + patient visit.
In order to do many-to-many relationships in Rails, convention says to create a new model to represent our join table. The name can technically be anything we want, but the model name should be as descriptive as possible, and indicate that it represents an association.
In pairs, spend 10 minutes answering the following questions in this issue for the below pairs of models...
- Should the relationship between these two models be represented using a many-to-many relationship?
- What would be a descriptive name for their resulting join table?
- What would be a useful additional column to include in the join table (e.g.,
order)?
Models
- Authors and Books
- Students and Courses
- Users and Groups
- Blog Posts and Categories
- Reddit Posts and Votes
Note: You are encouraged to not code along during this section -- just sit back and enjoy the ride! You will have the chance to implement this during in-class exercises with Tunr.
In order to see how to implement an example of a common many-to-many relationship in Rails, I'm going to build an event tracking application. For this application, I am only going to focus on the ability for a user to attend an event.
Q. What should the three models in our application be?
Let's call them: User, Event, and Attendance
For our domain's purposes, let's create a new model Attendance to represent the many-to-many relationship between our other two models: User and Event.
$ rails new attendance-tracker -d postgresql
$ cd attendance-tracker
$ rails db:drop db:create
$ rails g model User username:string age:integer
$ rails g model Event title:string location:stringWe generate the model just like any other. If we specify the attributes (i.e., columns on the command line) Rails will automatically generate the correct migration for us.
Onto the model files...
$ touch app/models/attendance.rb# models/attendance.rb
class Attendance < ApplicationRecord
# Associations to come later...
endNow the migration...
$ rails g migration create_attendances# db/migrate/*****_create_attendances.rb
class CreateAttendances < ActiveRecord::Migration[5.0]
def change
create_table :attendances do |t|
t.integer :num_guests, null: false
t.references :user, index: true, foreign_key: true, null: false
t.references :event, index: true, foreign_key: true, null: false
t.timestamps
end
end
endWhat is
t.references? It creates a column for referencing rows in another table (foreign key).
This will generate an Attendance table with user_id, event_id and num_guest columns. Take a look at it using psql in the Terminal.
For the in-class exercises you will be adding a "favoriting" feature to Tunr. In this version of Tunr, a user should be able to favorite a song.
To get started:
- Clone down this repo
- Checkout to the
favorites-starterbranch - Run
$ bundle install - Run
$ rails db:drop db:create db:migrate db:seed
Note: Make sure to work off the
favorites-starterbranch.
Then:
- Create a model and migration for
Favorite.- It should have
song_idanduser_idcolumns.
- It should have
A
Usermodel and user authentication functionality has already been provided for you. Because of this, you may see some code in here -- particularly inmodels/user.rbandroutes.rbthat was added by the gem Devise
Once we create our join model, we need to update our other models to indicate the associations between them. Let's visualize these associations with an ERD.
Board: Diagram Attendance Tracker ERD
For example, in our Users/Events example, we should have this...
# models/attendance.rb
class Attendance < ApplicationRecord
belongs_to :event
belongs_to :user
end
# models/event.rb
class Event < ApplicationRecord
has_many :attendances
has_many :users, through: :attendances
end
# models/user.rb
class User < ApplicationRecord
has_many :attendances
has_many :events, through: :attendances
endWe're essentially defining Attendance as an intermediary model/table between Event and User. An event has many users through Attendance and vice versa.
Now it's time to create the schema.
$ rails db:migrateTake 5 minutes to update the Song, User and Favorite models to ensure we have the correct associations.
It's a good idea to use the rails console to test creating our associations.
Here's an example of using the association of users / events...
george = User.create({username: "George", age: 18})
lorraine = User.create({username: "Lorraine", age: 17})
prom = Event.create({title: "Enchantment Under The Sea: 1955 Prom", location: "Hill Valley High School"})
after_party = Event.create({title: "Betty's Awesome After-party", location: "Super Secret!" })
brunch = Event.create({title: "Brunch!", location: "Lou's Cafe" })
# We can create the association directly
george_going_to_the_prom = Attendance.create(user: george, event: prom, num_guests: 1)
# Or using helper functionality
george.attendances.create(event: after_party, num_guests: 0)
# Or the other way
brunch.attendances.create(user: lorraine, num_guests: 10)
prom.attendances.create(user: lorraine, num_guests: 1)
# To see who's going to an event
prom.users
after_party.users
brunch.users
# To see a user's events
george.events
lorraine.eventsSo we've been able to generate associations between our models via the rails console. But what about our end users? How would somebody go about creating/removing a favorite on Tunr?
At a high level, what type of code do we need to add to support our new favoriting feature for Tunr?
We need to add functionality by modifying our controller, view and routes.
Before we add anything new, let's do a quick recap of the code we are starting from...
Let's take a look at songs_controller.rb...
- What do we currently have in here?
- Can we use any of these actions to handle adding/removing songs? Or do we need to add something new?
class SongsController < ApplicationController
# index
def index
@songs = Song.all
end
# new
def new
@artist = Artist.find(params[:artist_id])
@song = @artist.songs.new
end
# create
def create
@artist = Artist.find(params[:artist_id])
@song = @artist.songs.create(song_params)
redirect_to artist_song_path(@artist, @song)
end
#show
def show
@song = Song.find(params[:id])
end
# edit
def edit
@artist = Artist.find(params[:artist_id])
@song = Song.find(params[:id])
end
# update
def update
@song = Song.find(params[:id])
@song.update(song_params)
redirect_to artist_song_path(@song.artist, @song)
end
# destroy
def destroy
@song = Song.find(params[:id])
@song.destroy
redirect_to songs_path
end
private
def song_params
params.require(:song).permit(:title, :album, :preview_url, :artist_id)
end
endWe have CRUD functionality for the songs themselves, but that's about it.
- We need to add some actions to our controller that handle this additional functionality. You'll do that for Tunr in the next exercise.
- These will not correspond to RESTful routes.
There's more to this than just updating the Songs controller, we also need to make sure that our application has routes to support "favoriting" and "unfavoriting". For the sake of convenience, we have already defined the desired routes for you...
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
root to: 'artists#index'
resources :artists do
resources :songs, except: [:index, :show]
end
resources :songs, only: [:index, :show] do
# The member block creates two custom routes for songs that correspond with controller actions of the same name.
member do
post 'add_favorite'
delete 'remove_favorite'
end
end
endRead more about
memberroutes here: Rails Routing - Adding More RESTful ActionsHint: check out the output of
rails routesto see what those lines generated!
Great, now that we know we have the necessary routes defined, we need a way for the user to actually interact with our Web app so they can favorite a song.
We've gone ahead a provided some starter code in app/views/artists/show.html.erb, so let's look at the interface to how the user will favorite a song...
<h3>Songs <%= link_to "(+)", new_artist_song_path(@artist) %></h3>
<ul>
<% @artist.songs.each do |song| %>
<li>
<%= link_to "#{song.title} (#{song.album})", song_path(song) %>
# If this song has already been favorited, set the link to remove favorite.
<% if song.users.include? current_user %>
<%= link_to "♥".html_safe, remove_favorite_song_path(song), method: :delete, class: "fav" %>
# If the song has not been favorited, set the link to add favorite.
<% else %>
<%= link_to "♥".html_safe, add_favorite_song_path(song), method: :post, class: "no-fav" %>
<% end %>
</li>
<% end %>
</ul>Create the add_favorite and remove_favorite actions in the songs controller. Look at the artists/show.html.erb view to see how we route to these actions.
Below are some line-by-line instructions on how to implement add_favorite and remove_favorite. We encourage you not to look at the solution unless you are stuck!
Start out by logging into the application using the "Sign Up" feature. It should be visible in the top-right corner on the home page. Once you've done that, tackle the controller actions.
add_favorite should...
- Save the song which you will be favoriting in an instance variable.
- Create a new Favorite instance that...
a. Belongs to the song.
b. Belongs to the user who is creating the favorite. - Redirect to the show page for the artist once the song is added.
remove_favorite should...
- Save the song you will be un-favoriting in an instance variable.
- Delete the Favorite instance that references the song that is being un-favorited.
- Redirect to the show page for the artist once the song is added.
Because we are using Devise to handle user authentication, it gives us access to a current_user method that, when called, returns the user who is currently logged in. Conceptually, think of it as running something like User.find_by(logged_in: true).
This means that in your controller you can write code like Favorite.create(user: current_user).
...you can take a peek at it here.
-
Why do we need to have Many-to-Many relationships? Give examples.
-
What extra feature(s) do we need to add to our schema/model in order to implement Many-to-Many?
-
How do we add non-standard routes inside our
resourcesdirective?
If there's time left, spend the remainder of class working on Scribble. If you have completed the required steps, try implementing a many-to-many relationship between Posts and Categories using a Tags join table. This will require creating some new classes.