Rails One to Many Relationships

Let's build a relationship between models.

We are going to make an app to visualize data for temperatures that belong to locations.

Each location will have many temperatures




Roadmap

  • Scaffold two independent models
  • Set up a one-to-many relationship between the models
  • Use nested routes
  • Design the api endpoints



SETUP

Create the top-level directory called temperatures that will house both our rails api and the frontend.

The app will have a one-to-many relationship between Locations and Temperatures.

For each location we can log changes in the climate.

Create the Rails API:

$ rails new temperatures_api --api -d postgresql --skip-git

cd into the Rails directory and create the database.





Generate App

Scaffold Locations with lat and lng as decimals, and also a name column.

Click for Full Command
$ rails g scaffold location lat:decimal lng:decimal name

Scaffold Temperatures with average_high_f and average_low_f as integers and month as a string

Click for Full Command
$ rails g scaffold temperature average_high_f:integer average_low_f:integer month

This has added boilerplate files and code to

  • db/migrate
  • app/models
  • config/routes.rb
  • app/controllers

Check that the migration files are correct.

screenshot

screenshot

We have two fully-formed but independent resources: Locations and Temperatures. What we need to do next is relate them together.





Add foreign key

One-to-many relationship

Let's generate a migration to add the foreign key for our one-to-many relationship.

If Locations have many Temperatures, and

A Temperature belongs to a Location ...

Which model should have the foreign key?

Answer: The foreign key always goes in the many. In this case there will be many temperatures. Each temperature will reference its single location via its foreign key.

Locations table: screenshot

Temperatures table: screenshot

screenshot

Inside the migration we want to add a column that is a foreign key connecting to temperatures called location_id that is an integer.

Click for Migration Code

screenshot

We have three migrations pending, let's run them and generate our schema.

schema.rb

screenshot





ActiveRecord Relations

Now we need to clarify that the Location model has_many :temperatures and the Temperature model belongs_to :location.

Click for Model Code

models/location.rb

screenshot

models/temperature.rb

screenshot

Note Rails's plural / singular conventions.



Seed data

Next, let's add some seed data:

seeds.rb

Location.create([
  { lat: 40.7128, lng: 74.0059, name: 'New York City' },
  { lat: 78.2232, lng: 15.6267, name: 'LongYearByen' }
])

Temperature.create([
  { average_high_f: 39, average_low_f: 26, month: 'January', location_id: 1 },
  { average_high_f: 42, average_low_f: 29, month: "February", location_id: 1 },
  { average_high_f: 50, average_low_f: 35, month: 'March', location_id: 1 },
  { average_high_f: 61, average_low_f: 45, month: 'April', location_id: 1 },
  { average_high_f: 71, average_low_f: 54, month: 'May', location_id: 1 },
  { average_high_f: 79, average_low_f: 64, month: 'June', location_id: 1 },
  { average_high_f: 84, average_low_f: 69, month: 'July', location_id: 1 },
  { average_high_f: 83, average_low_f: 68, month: 'August', location_id: 1 },
  { average_high_f: 75, average_low_f: 61, month: 'September', location_id: 1 },
  { average_high_f: 64, average_low_f: 50, month: 'October', location_id: 1 },
  { average_high_f: 54, average_low_f: 42, month: 'November', location_id: 1 },
  { average_high_f: 48, average_low_f: 43, month: 'December', location_id: 1 },
  { average_high_f: -6, average_low_f: -12, month: 'January', location_id: 2 },
])

Then run the seed file in Terminal.





Rails console

Open the Rails console.

Use ActiveRecord to see all temperatures belonging to a location:

screenshot

Use ActiveRecord to see a temperature's associated location:

screenshot





↩ 🚐 🚛 ROUTES 🛣 🔀 ↪

Design considerations

What do you want your API to do?

Locations

  • I don't want anyone to be able to add or edit locations on my API.
  • I do want there to be a list of locations (index), and information for each location (show).

Therefore the only routes I need are index and show for Locations. In Express, this is easy, I just write them in and I'm done. In Rails, there is a more specific procedure.

Limit location routes only to index and show

config/routes.rb

screenshot

Temperatures

  • I want my API to send an index of temperature records associated with a location.
  • I want my user to be able to add temperature data to the API for a given location.

All I need are index and create for Temperatures.

Limit temperature routes only to index and create

config/routes.rb

screenshot

screenshot

screenshot

The only keeps it nice and tidy.





Controller Actions

Locations controller

We want only an index and a show for Locations. Let's remove everything else except the boilerplate set_location method, and edit the before_action call just to have [:show]:

screenshot

Run the server with rails s and check out the index and show routes in the browser.




Locations show

Currently, our locations routes deliver data for locations, but there is no temperature data included.

Why not have our Locations show route also deliver the Temperatures for that location? It would be convenient for a front-end developer to query:

locations/1

And receive JSON for the location that includes the temperatures for that location:

screenshot

The frontend developer would get location data from result and temperatures with result.temperatures.

We can format our data this way with the .to_json method that takes a hash as an argument that we can use to include the temperatures.

render json: @location.to_json(include: :temperatures)

screenshot





Temperatures controller

We want to have an index and a create in our temperature routes. Let's remove everything else. Remove the before_action call and the set_temperature method, too, since we won't be needing them.

screenshot

Temperatures create

BRAIN BUSTER

When we create a Temperature:

  • Do we want a Temperature to exist without belonging to a Location?
  • At which point do we associate a Temperature with a Location?
  • Where would the Location even come from?



First Part of The Answer

We want the location to come in from a param. The user will decide which location when they make the request to the server.

A hypothetical request from the client-side would look like:

fetch('/locations/1/temperatures', {
	method: 'POST',
	data: this.formdata
})

The user wants a temperature added for location 1.

In Express this location id would come in as req.params.id.

How do we get it in Rails? There is no params hash in rails routes for our temperatures#create action.




Second Part of The Answer

Nested routes

To resolve this issue, we will nest our create action inside the locations routes:

Rails.application.routes.draw do
  resources :temperatures, only: [:index]
  resources :locations, only: [:index, :show] do
    resources :temperatures, only: [:create]
  end
end



When we run rails routes, we will get this:

               Prefix Verb URI Pattern                                    Controller#Action
         temperatures GET  /temperatures(.:format)                        temperatures#index
location_temperatures POST /locations/:location_id/temperatures(.:format) temperatures#create
            locations GET  /locations(.:format)                           locations#index
             location GET  /locations/:id(.:format)                       locations#show

Our create action URI has changed to reflect that we are creating a Temperature only in relation to a Location. The param we receive is location_id.

We will want to add the incoming location_id to our new temperature record:

@temperature.location_id = params[:location_id]

Finally, we will remove location: @temperature, because it will try to force a redirect and may give us errors in Postman if it stays.

screenshot

temperatures_controller.rb

screenshot

  • Here we create a new Temperature using temperature_params
  • On the new temperature, we set the id column to the location_id from the url
  • If save is successful, we send a 201
  • If unsuccessful, we send a 422



Test Create Route With Postman

screenshot

We want to send the following temperature object to the API:

{ average_high_f: 46, average_low_f: 39, month: 'January' location_id: 2 }

How would we write this as form-data in a Postman request?

We will add this new temperature record for location 2.

POST http://localhost:3000/locations/2/temperatures

Successful Postman Request

screenshot

Note that the temperature was saved with a location_id as intended.

Location 2 in the browser should now have the new temperature:

screenshot




Under the hood: params hash again

Whenever we make a CREATE request with Postman, we should see the following in the Terminal tab running rails s:

screenshot

This is like the request object in Express.

Our req.body is within temperature, and our req.params is within location_id. That's just the way Rails formats it. Body and params go into the params hash.