We have updated the content of our program. To access the current Software Engineering curriculum visit curriculum.turing.edu.
Nested Resources in Rails
Learning Goals
- Understand what nested resources are
- Understand the routes and requests needed to create nested resources
Warm up
Discuss with a partner:
In SetList, when we fill out the form to create a new Artist and click the submit button, what does the resulting request look like? What information does it contain? To what path is it sent? What verb does it use?
Nested Resources
Sometimes in our applications we will have resources that are essentially tied to each other, meaning one cannot exist without the other. We refer to these as Nested Resources. In our SetList app, our Song model says that it belongs_to
an Artist. That means that it has to have an Artist. We say that Songs
are nested under Artists
. So, for some actions we want to take on a Song, we have to know which Artist it belongs to.
In SetList, we want users to be able to create new songs, but we have to know which artist the song is being created for. So, when we send a request to create a new song, we will use these routes:
GET /artists/:artist_id/songs/new #new song form
POST /artists/:artist_id/songs #create a song
Writing the test
# spec/features/songs/new_spec.rb
require 'rails_helper'
RSpec.describe "creating a new song" do
it "can create a song" do
artist = Artist.create(name: "Journey")
title = "Don't Stop Believin'"
length = 231
play_count = 7849
visit "/artists/#{artist.id}/songs/new"
fill_in "title", with: title
fill_in "length", with: length
fill_in "play_count", with: play_count
click_on "Create Song"
new_song = Song.last
expect(current_path).to eq("/songs/#{new_song.id}")
expect(page).to have_content(title)
expect(page).to have_content(length)
expect(page).to have_content(play_count)
expect(page).to have_content(artist.name)
end
end
Discuss with the person next to you:
- What route are we visiting?
- What should happen when we click submit?
When we run the test, we see that no route exists for our new song form.
Creating the Nested Routes
Let’s add that nested route now:
# config/routes.rb
get '/artists/:artist_id/songs/new', to: 'songs#new'
Run rake routes
and examine what routes this generated for us.
Creating the Form
Running the test gives us an ActionNotFound
error.
Create the new
action:
#songs_controller.rb
def new
end
Next we’ll get a missing template error, so go create the view:
touch app/views/songs/new.html.erb
Now the tests are telling us that it can’t find the form fields.
Creating the Form
First, let’s think about where we want this path to submit. Thinking back to our discussion at the beginning, what verb/path combo should we use?
<%= form_with url: "/artists/#{@artist_id}/songs", method: :post, data: {turbo: false} do |form| %>
<% end %>
Notice that we have used @artist_id
in the path, so let’s add that instance variable to our action. Where does that information come from? From the path! Take a look back at routes.rb
to remind yourself what the route to show this form looks like.
def new
@artist_id = params[:artist_id]
end
If we run the test again, we’ll still get our error for missing fields, so let’s add those fields:
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :length %>
<%= f.text_field :length %>
<%= f.label :play_count, "Play Count" %>
<%= f.number_field :play_count %>
<%= f.submit "Create Song"%>
Now when we run the test, we’ll see no route matches when we submit the form.
Creating the New Song
Let’s add the route to create a song:
post '/artists/:artist_id/songs', to: 'songs#create'
Let’s add our create action:
# songs_controller.rb
def create
end
Running the test will give us:
NoMethodError:
undefined method `id' for nil:NilClass
Follow the stack trace for this error to figure why this is happening.
We haven’t actually created our Song, so let’s do that in our Songs controller. As always with handling form data, we are going to use strong params:
private
def song_params
params.permit(:title, :length, :play_count)
end
Let’s try to create our Song with these strong params:
def create
song = Song.create!(song_params)
end
Notice how we are using create!
rather than create
. The bang (!
) will give us an error if our creation is unsuccessful, which is useful when developing. It is always a good idea to start with create!
first to make sure everything is working correctly.
Run the test and, sure enough, our Song was not created successfully. Our error should be
ActiveRecord::RecordInvalid:
Validation failed: Artist must exist
Finally, we are seeing the implications of our nested resources. A Song can’t exist on its own, it needs an artist. This is why we’ve been passing the artist_id
in our path:
def create
artist = Artist.find(params[:artist_id])
song = artist.songs.create!(song_params)
end
Run the test again and we no longer get an error that our Song couldn’t be created, so it looks like that is working.
Now we get a failure in our test saying the path is wrong, so all that’s left to do is redirect the request:
def create
artist = Artist.find(params[:artist_id])
song = artist.songs.create!(song_params)
redirect_to "/songs/#{song.id}"
end
It is important to note that the way we’ve set up the relationship is not the only way to do it. For instance, we don’t have to find the Artist object to set up the relationship. We could manually use the artist’s id to associate it with the song:
def create
song = Song.new(song_params)
song.artist_id = params[:artist_id]
song.save!
redirect_to "/songs/#{song.id}"
end
In this case, we have to first do Song.new
, then change the artist_id, then save the song. create
will do a new/save
all at once.
As usual, there are many ways of doing something in Rails. You should be comfortable with using either of these two options we’ve shown here.
Checking our work in Development Mode:
- Be sure you have at least one artist in your database (use
rails console
if you need to) - Run
rails s
- Visit
/artists/1/songs/new
- See if it works!
Independent Practice
Write a test and implement the code for an Artist’s Song index page. This page will show all the songs for a particular artist. It should use this route: get /artists/:artist_id/songs
.
Checks for Understanding
Turn and talk to your neighbor and discuss:
- What is a nested resource?
- What does the route look like for a nested resource?
- Consider the following features we could add in our app:
- show a song
- show all songs
- delete a song
- show a particular artist’s songs
- create a song
- update a song
Which of them would require a nested route?