- 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
has_many through
to connect two models via a join model in Rails - Use a many-to-many relationship to implement a feature in a Rails app
Many-to-many relationships are fairly common in apps. Examples might include:
- Posts can be sorted into multiple Categories, Categories contain many Posts.
- Groups can host many Photos, Photos may appear in many Groups.
- Playlists contain many songs, Songs will appear on multiple Playlists.
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 JOIN
andOUTER 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 Songs and Playlists, we would have a song_id
column and a playlist_id
column.
We can also add columns as needed to store additional information about the relationship. For example, we may choose to add an order
column which stores an integer representing what order that song appears on the playlist. A song may be first on one playlist, but 10th on another - that info would be stored on the join table.
In rails, we should always create a model to represent our join table. The name can technically be anything we want, but really the model name should be as descriptive as possible, and indicate that it represents an association.
In pairs, spend 5 minutes answering the following questions 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
- Doctors and Patients
- Blog Posts and Categories
- Reddit Posts and Votes
We will be using Attendances as the in-class example. We encourage you not to code along -- just watch. You will have the chance to implement this during in-class exercises with Tunr.
Attendances represent the many-to-many relationship between two models: Users and Events. Let's set them up in a brand new Rails application...
$ rails new attendance-tracker -d postgresql
$ rails g model User username:string age:integer
$ rails g model Event title:string location:string
We 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...
# models/attendance.rb
class Attendance < ActiveRecord::Base
# Associations to come later...
end
Now the migration...
$ rails g migration create_attendances
# db/migrate/*****_create_attendances.rb
class CreateAttendances < ActiveRecord::Migration
def change
create_table :attendances do |t|
t.integer :num_guests, null: false
t.references :user, index: true, foreign_key: true
t.references :event, index: true, foreign_key: true
t.timestamps null: false
end
end
end
What is
t.references
? It does the same thing as writing outbelongs_to :model
.
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.
5 minutes exercise. 5 minutes review.
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.
Here's some starter code. Make sure to work off the favorites-starter
branch.
Create a model and migration for Favorite
. It should have song_id
and user_id
columns.
A
User
model and user authentication functionality has already been provided for you. Because of this, you may see some code in here -- particularly inmodels/user.rb
androutes.rb
that you have not seen before. We will explain what it does in a later mini-lesson.
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.
For example, in our Users/Events example, we should have this...
# models/attendance.rb
class Attendance < ActiveRecord::Base
belongs_to :event
belongs_to :user
end
# models/event.rb
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, through: :attendances
end
# models/user.rb
class User < ActiveRecord::Base
has_many :attendances
has_many :events, through: :attendances
end
We're essentially defining Attendance
as an intermediary model/table between Event
and User
. An event has many users through Attendance
and vice versa.
Take 5 minutes to update the Song, User and Favorite models to ensure we have the correct associations.
If you finish early, go ahead and start testing out these new associations using the Rails console.
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...
bob = User.create({username: "Bob", age: 25})
carly = User.create({username: "Carly", age: 28})
prom = Event.create({title: "Under the Sea: 2015 Prom", location: "Greenville High School"})
after_party = Event.create({title: "Eve's Awesome After-party", location: "Super Secret!" })
brunch = Event.create({title: "BRUNCH!", location: "IHOP" })
# We can create the association directly
bob_going_to_the_prom = Attendance.create(user: bob, event: prom, num_guests: 1)
# Or using helper functionality
bob.attendances.create(event: after_party, num_guests: 0)
# Or the other way
brunch.attendances.create(user: carly, num_guests: 10)
prom.attendances.create(user: carly, num_guests: 1)
# To see who's going to an event
prom.users
after_party.users
brunch.users
# To see a user's events
bob.events
carly.events
# To delete an association
Attendance.find_by(user: bob, event: prom).destroy # will only destroy the first one that matches
Attendance.where(user: bob, event: prom).destroy_all # will destroy all that match
prom.attendances.where(user: bob).destroy_all
So we've been able to generate associations between our models via Pry. But what about our end users? How would somebody go about creating/removing a favorite on Tunr?
- We need to add that functionality by modifying our controller, view and routes.
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 = Song.new
end
# create
def create
@artist = Artist.find(params[:artist_id])
@song = Song.create!(song_params.merge(artist: @artist))
redirect_to artist_song_path(@artist, @song)
end
#show
def show
@song = Song.find(params[:id])
end
# edit
def edit
@song = Song.find(params[:id])
end
# update
def update
@song = Song.find(params[:id])
@artist = Artist.find(params[:artist_id])
@song.update(song_params.merge(artist: @artist))
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
end
We 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, but we've done some of the work for you...
# config/routes.rb
Rails.application.routes.draw do
devise_for :users
root to: 'artists#index'
get '/songs', to: 'songs#index'
resources :artists do
resources :songs
resources :genres
end
# This creates two custom routes for songs that correspond with controller actions of the same name.
resources :songs do
member do
post 'add_favorite'
delete 'remove_favorite'
end
end
end
This application uses a gem called Devise to handle User authentication. All you need to know about that for now is that it generates a User model.
Also, see how we've indented resources statements? That's called nested resources. It enables us to visit URLs like
http://localhost:3000/artists/1/songs/2
. You'll learn more about those later.
# app/views/artists/show.html.erb
<h3>Songs <%= link_to "(+)", new_artist_song_path(@artist) %></h3>
<ul>
<% @artist.songs.each do |song| %>
<li>
<%= link_to "#{song.title} (#{song.album})", artist_song_path(@artist, song) %>
# If this song has already been favorited, set the link to remove favorite.
<% if song.favorites.length > 0 %>
<%= 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>
Take 15 minutes to 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. At a high level, 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.
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.
More information is available on the Scribble repo.
Example: Deployed Scribble