Week 2 had a WHERE. Week 3 had a foreign key: a column in one table that holds the id of a row in another. Code-along 5 is those two ideas in Ruby, and nothing more. A contact works at one company; the contacts table holds that company's id; and you find a contact's company, or a company's contacts, by looking the id up.
Two tables, one column between them
The rules under Contact in DONE.md:
## Contact
A contact can't be saved without a company.
I can find all the contacts at a company: Apple has three.
The prompt asked the agent for the model and its migration as well as the tests, so the migration came first:
class CreateContacts < ActiveRecord::Migration[8.1]
def change
create_table :contacts do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.integer "company_id"
t.timestamps
end
end
end
company_id is an integer because it holds another table's id. On the Data page, every contact's company_id is the id of a row in companies. That column is the whole relationship. There's no other link anywhere.
The tests
require "test_helper"
class ContactTest < ActiveSupport::TestCase
# One test for each sentence under Contact in DONE.md, named with that sentence.
test "a contact can't be saved without a company" do
contact = Contact.new
contact["first_name"] = "Tim"
contact["last_name"] = "Cook"
contact.save
assert_equal 0, Contact.all.count
end
test "I can find all the contacts at a company: Apple has three" do
apple = Company.new
apple["name"] = "Apple"
apple.save
amazon = Company.new
amazon["name"] = "Amazon"
amazon.save
for first_name in ["Tim", "Craig", "Lisa"]
contact = Contact.new
contact["first_name"] = first_name
contact["company_id"] = apple["id"]
contact.save
end
contact = Contact.new
contact["first_name"] = "Andy"
contact["company_id"] = amazon["id"]
contact.save
apple_contacts = Contact.where({ "company_id" => apple["id"] })
assert_equal 3, apple_contacts.count
end
end
On the first run, one fails and one passes. "Can't be saved without a company" fails because nothing enforces it yet. "Apple has three" passes on the empty model, because finding rows by a column comes with the table; where is not something you add to a model. Its test is still worth having: it says what "at a company" means (the company_id matches) and that Andy, at Amazon, is not counted.
One line makes the first pass:
class Contact < ApplicationRecord
validates :company_id, presence: true
end
From a company to its contacts
Contact.where({ "company_id" => apple["id"] })
A list of every contact whose company_id is Apple's id. It's the WHERE from week 2, written in Ruby, and the SQL it writes is in log/development.log:
SELECT "contacts".* FROM "contacts" WHERE "contacts"."company_id" = 1
where always returns a list, even when it finds one row, or none.
From a contact to its company
Company.find_by({ "id" => contact["company_id"] })
One row, the company whose id the contact holds. find_by returns a single row, or nil if there isn't one.
That's the script from class, scripts/list-contacts.rb, after it saves Apple, Amazon and four contacts:
# Each contact holds its company's id. Look the company up by that id.
for contact in Contact.all
company = Company.find_by({ "id" => contact["company_id"] })
puts "#{contact["first_name"]} #{contact["last_name"]} (#{company["name"]})"
end
Tim Cook (Apple)
Craig Federighi (Apple)
Lisa Jackson (Apple)
Andy Jassy (Amazon)
Every line is one lookup. When the thing you want is two tables away (an activity's contact's company, in lab 3), it's two lookups, following the ids one at a time.
Why not contact.company?
Rails can hide this lookup. With belongs_to :company in the model, contact.company does the find_by for you, and has_many :contacts gives you apple.contacts. It's shorter, and in a bigger app you'd use it.
This repo's AGENTS.md tells the agent not to, and the reason is the same as for tests first: so you can see what's happening. contact.company looks like the contact contains a company. It doesn't. It holds a number, and somebody runs a WHERE with it. Once that's obvious to you, the shorthand is a convenience. Before it is, it's a mystery, and the first time it returns nil you won't know where to look.
If your agent wrote has_many or belongs_to, or a contact.company, point it at AGENTS.md and ask for the where.
The rule of thumb
Which table holds the id? The many side. One company has many contacts, so each contact holds a company_id; the company holds nothing about its contacts. One salesperson logs many activities, so each activity holds a salesperson_id. To go from the one to the many, where on that column. To go from the many to the one, find_by on id.