- Use Active Model associations in the controller
- Render nested JSON data based on model associations
In this section, we're going to be building out an API for an exciting new business: AirBudNB, a website for renting dog houses. We are using two models to represent our application's data: dog houses and reviews. A dog house has many reviews, and each review belongs to one dog house.
DogHouse -< Review
The code for our models is already in place, so we can focus on getting the controller to return the right data. To get set up, run:
$ bundle install
$ rails db:migrate db:seed
Our designers have created a mockup of one of the pages of our application for displaying details about one individual dog house. It will look like this:
Our API will need to serve up the data for this page as efficiently as possible. Ideally, that means we'll be able to have just one request that returns the data about the dog house as well as a list of all its reviews.
Start up your Rails server and make a GET request to /dog_houses/1
. Check
out the controller action for this request:
# app/controllers/dog_houses_controller.rb
def show
dog_house = DogHouse.find(params[:id])
render json: dog_house
end
Currently, this route returns only the data about the dog house, not its reviews:
{
"id": 1,
"image": "https://assets.petco.com/petco/image/upload/f_auto,q_auto/1563564-right-1",
"name": "Cozy Studio in Historic District",
"city": "Denver",
"price": 90,
"favorite": false,
"latitude": "39.7433",
"longitude": "-104.98322"
}
Note: the seed file creates seed data randomly, so the data you see in your browser will be different.
Based on our models, we know each dog house has many reviews associated with it, and we can use Active Record to access that data:
class DogHouse < ApplicationRecord
has_many :reviews
end
You can verify this by running rails c
:
DogHouse.first.reviews
# => #<ActiveRecord::Associations::CollectionProxy [#<Review id: 1...>, #<Review id: 2...>]
So we need some way to include this review data in the response from our controller!
Thankfully for us, Rails gives us some additional serialization options when
converting Active Record objects to JSON data. In this case, the include
option will let us nest associated data in our response. Let's update the code
in our controller:
render json: dog_house, include: :reviews
Using include: :reviews
will call the .reviews
method that is provided with
the has_many :reviews
macro, and will serialize the reviews as a nested array
of JSON data. Try making that same GET /dog_houses/1
request again, and you
should now see the reviews listed along with the dog house they belong to.
Again, your data will be different, but it should be structured as follows:
{
"id": 1,
"image": "https://assets.petco.com/petco/image/upload/f_auto,q_auto/1563564-right-1",
"name": "Cozy Studio in Historic District",
"city": "Denver",
"price": 90,
"favorite": false,
"latitude": "39.7433",
"longitude": "-104.98322",
"reviews": [
{
"id": 1,
"username": "emory_rolfson",
"comment": "Listicle diy messenger bag food truck yuccie pug thundercats.",
"rating": 4,
"dog_house_id": 1
},
{
"id": 2,
"username": "willena",
"comment": "Brunch aesthetic williamsburg taxidermy.",
"rating": 1,
"dog_house_id": 1
}
]
}
Now, our API returns all the data we need to handle this view on the frontend with just one request.
One of the other pages our frontend will need is a page to list out all of the top reviews, along with their associated dog house:
Again, we'd like to make just one request to get all of the data to populate
this view. Currently, a GET
to /reviews
returns an array of all review
data, but it's missing the data we need for the associated dog house:
[
{
"id": 3,
"username": "alton",
"comment": "Meh polaroid letterpress occupy freegan.",
"rating": 5,
"dog_house_id": 1
},
{
"id": 4,
"username": "malorie.grant",
"comment": "8-bit 3 wolf moon tattooed blog +1.",
"rating": 5,
"dog_house_id": 1
},
{
"id": 10,
"username": "mary.hodkiewicz",
"comment": "Fixie art party cronut pug tattooed.",
"rating": 5,
"dog_house_id": 2
}
]
We can see that each review has an associated dog house based on the
dog_house_id
attribute, but it'd be quite the ordeal to make individual
requests for each dog house to get the associated data! Again, we can leverage
the power of our Active Record associations, and serialize the dog house along
with each review:
class ReviewsController < ApplicationController
def index
reviews = Review.all.order(rating: :desc)
render json: reviews, include: :dog_house
end
end
With this include: :dog_house
option in place, we now get a nested object
under each review representing the dog house that the review belongs to:
[
{
"id": 3,
"username": "alton",
"comment": "Meh polaroid letterpress occupy freegan.",
"rating": 5,
"dog_house_id": 1,
"dog_house": {
"id": 1,
"image": "https://assets.petco.com/petco/image/upload/f_auto,q_auto/1563564-right-1",
"name": "Cozy Studio in Historic District",
"city": "Denver",
"price": 90,
"favorite": false,
"latitude": "39.7433",
"longitude": "-104.98322"
}
},
{
"id": 4,
"username": "malorie.grant",
"comment": "8-bit 3 wolf moon tattooed blog +1.",
"rating": 5,
"dog_house_id": 1,
"dog_house": {
"id": 1,
"image": "https://assets.petco.com/petco/image/upload/f_auto,q_auto/1563564-right-1",
"name": "Cozy Studio in Historic District",
"city": "Denver",
"price": 90,
"favorite": false,
"latitude": "39.7433",
"longitude": "-104.98322"
}
},
{
"id": 10,
"username": "mary.hodkiewicz",
"comment": "Fixie art party cronut pug tattooed.",
"rating": 5,
"dog_house_id": 2,
"dog_house": {
"id": 2,
"image": "https://loveincorporated.blob.core.windows.net/contentimages/gallery/e7fd2f69-8c5b-4865-8add-d3ae27693f45-bowwowhaus.jpg",
"name": "Mid Century Studio in Lively Uptown",
"city": "Houston",
"price": 88,
"favorite": false,
"latitude": "29.750588",
"longitude": "-95.364063"
}
}
]
Now we can retrieve all the data for our reviews page with just one request!
You may notice that making the request for this data is a bit slow compared to some of the other endpoints we've been working on. This is due to how Active Record is accessing data for the associated dog house for each individual review.
If you open the Rails server log after making this request, you'll see why:
there are a lot of SQL queries being fired off at our database! This is an
example of the N+1 problem. First, we load all reviews with
Review.all
; then, for each review returned by Review.all
, we make a separate
query for each dog house associated with that review. This is definitely not
ideal! We'll learn about a solution to this problem in a future lesson, but for
now, keep an eye out for slow queries and look at the SQL code being executed in
your Rails server to identify where these issues arise.
When developing APIs with our frontend needs in mind, it's best to structure our
data to minimize the number of requests needed for the frontend to retrieve that
data. We can take advantage of Active Record associations using has_many
and
belongs_to
relationships, and serialize JSON data between related models using
the include
option.
Before you move on, make sure you can answer the following question:
- What does the
include
option do for us and how do we use it?