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-gitcd 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 nameScaffold 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 monthThis has added boilerplate files and code to
db/migrateapp/modelsconfig/routes.rbapp/controllers
Check that the migration files are correct.


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:

Temperatures table:


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

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

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

models/temperature.rb

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:

Use ActiveRecord to see a temperature's associated location:

↩ 🚐 🚛 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

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



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]:

Run the server with rails s and check out the index and show routes in the browser.
Locations with related temperatures
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/1And receive JSON for the location that includes the temperatures for that location:

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)
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.

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
endWhen 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#showOur 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.

temperatures_controller.rb

- Here we create a new Temperature using
temperature_params - On the new temperature, we set the id column to the
location_idfrom the url - If save is successful, we send a 201
- If unsuccessful, we send a 422
Test Create Route With Postman

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

Note that the temperature was saved with a location_id as intended.
Location 2 in the browser should now have the new temperature:

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:

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.