Creating a Rails Relationship the Hard Way
Currently we have a list of narrations the user can click on to go to the narration’s show page. From there, they can navigate back to the narrations list or view the original article in a new tab. Looking forward, I’d like to not only show who narrated the narration, but let the listener click on their name/avatar, and then go to their profile page. Each narrator will have many narrations. Each narration will have exactly one narrator. We could have duos some day, but we’ll table that until we need it. So let’s get started; beginning with the end in mind, I’d like to start from the Narration view. Let’s add a link to take us to a Narrator show page:
There’s some pseudo-code as a placeholder for now. I can envision the Narrator table having a first_name
and last_name
column, and I’ll need to use the Narrator controller to create a #full_name
method. But first, let’s define the route narrator_path
:
Here’s the error we get if we click on the link in this state:
Routing Error
uninitialized constant NarratorsController
So in case you were wondering whether we should create our model or controller next, now you know! Let’s make a new file called app/controllers/narrators_controller.rb
and give it a show method:
Here’s my next error in the browser:
ActionController::UnknownFormat in NarratorsController#show
NarratorsController#show is missing a template for this request format and variant.
Now let’s create app/views/narrators/show.html.haml
:
Now each Narration show page has a link that takes us to its Narrator show page, sort of. Our route is using the same id as the Narration. Let’s create our Narrator model now and add some data to shine some light, app/models/narrator.rb
:
Now, let’s create a migration, db/migrate/20170728110334_create_narrators.rb
, and similar to Twitter, to give personality and possible anonymity, let’s give the Narrator a username
:
Run rails db:migrate
:
== 20170728110330 CreateNarrators: migrating ==================================
-- create_table(:narrators)
-> 0.0961s
== 20170728110330 CreateNarrators: migrated (0.0964s) =========================
Now let’s create some seed data to work with. Here’s what I’ll drop into seeds.rb
and then run rails db:seed
:
I double checked that duplicate records weren’t created for Narrations
and confirmed using rails db
and select * from narrators;
that we have our table populated:
id | first_name | last_name | username
----+------------+-----------+-----------
1 | Aaron | Kelton | chemturion
2 | Morgan | Freeman | freeman
Now for the purpose of demonstration, how can we assign a narrator to a narration? Let’s suppose I narrated the “Life in Short” article, and Morgan Freeman narrated the “Drugs and the Meaning of Life” article. Generically, I the Narrator could have many narrations. Let’s define this relationship in each model file. First, app/models/narrator.rb
has_many
narrations:
And second, app/models/narration.rb
belongs_to
a narrator:
Now, conceptually this makes me think that only the Rails objects know about each other. I’m not sure that we’ve successfully linked the data in the tables together using a foreign key. Since we’ve already generated a migration to create the tables, let’s generate a migration to add the connections. Here’s the Rails Guide that helped me: http://edgeguides.rubyonrails.org/association_basics.html#updating-the-schema. Unfortunately their migration isn’t for after the table’s been created. Let’s do something comparable, so new file db/migrate/20170728114023_add_reference_to_narrations.rb
and using http://api.rubyonrails.org/v5.1.2/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference as guidance:
Running rails db:migrate
gives us:
== 20170728114023 AddReferenceToNarrations: migrating =========================
-- add_reference(:narrations, :narrator, {:foreign_key=>true})
-> 0.1256s
== 20170728114023 AddReferenceToNarrations: migrated (0.1258s) ================
Now let’s look inside running rails db
and select * from narrations;
. We should see a narrator_id
column, but we can also confirm by looking in our db/schema.rb
file:
Notice that the narrations
table has a column named narrator_id
with type bigint
, and an index on that column. Nothing changed for the narrator
table, but at the end of the schema.rb
file, we have the add_foreign_key
statement. Ok, back to the use case.
When a narrator creates a narration, their id will be plopped into the narration table as narrator_id. But for now using sample data, I think I’ll need to manually put that into my seeds.rb file and do the whole reset & load dance rails db:reset
.
After the reset, here’s my feedback:
Dropped database 'narify_development'
Dropped database 'narify_test'
Created database 'narify_development'
Created database 'narify_test'
-- enable_extension("plpgsql")
-> 0.0463s
-- create_table("narrations", {:force=>:cascade})
-> 0.0403s
-- create_table("narrators", {:force=>:cascade})
-> 0.0086s
-- add_foreign_key("narrations", "narrators")
-> 0.0162s
-- enable_extension("plpgsql")
-> 0.0468s
-- create_table("narrations", {:force=>:cascade})
-> 0.0248s
-- create_table("narrators", {:force=>:cascade})
-> 0.0130s
-- add_foreign_key("narrations", "narrators")
-> 0.0755s
Now, let’s jump back into the narrators_controller.rb
and assign the narrator in question to the @narrator
instance variable.
I’m not totally sure this is going to work. I think the params
dude is going to pass an :id
, but I’m not sure how it knows to pass the narration.narrator_id
, if that makes sense. Let’s add this little code to our narrator’s show view:
Unfortunately, my narrator’s id matches the narration id, so I can’t fully tell if it’s working. To test, let’s change the published value for narration id=3 to true, and see if the corresponding narrator route tries to pull 3 instead of 2… And it’s pulling the wrong id. So I think we should now modify our link_to
helper in app/views/narrations/show.html.haml
:
Now we can navigate to the narrator show page from the narration show page. Next thing we want to do is list all the narrations for the narrator when we go to their page, similar to seeing a user’s Tweets on their Twitter profile page.