This lesson assumes you have forked and cloned the following:
By the end of this, developers should be able to:
scaffold
.inverse_of
relationship.training
, for your work.training
branch.Previously we created a single model, Book
.
Then we created a second model Author
and linked it to Book
.
Now we are going to add a third model: Borrower
, then we are going to create
a fourth model, Loan
, which is going to act as a link between the
Borrower
s and Book
s.
This Loan
model will connect Borrower
and Book
together. Earlier, when we
were working with a one-to-many
relationship books
belonged to an author
.
When we created a migration:
class AddAuthorToBooks < ActiveRecord::Migration
def change
add_reference :books, :author, index: true, foreign_key: true
end
end
...we added an author
reference column to the books
table which is able to
store a reference to an author of a particular book.
With Borrowers
, we know this is a two way street however: Books
can have
many Borrowers
and Borrowers
can have many Books
. In order to make this
a two way street we are going to need a join table
.
A join table
is a special model which holds references to two or more models.
Let's see what this association might look like:
In the above example, the loans
model is the join table
. You can see
it has both a book_id
attribute and a borrower_id
attribute. Both of these
attributes store references to their respective models.
You can also see an attribute called date
. You are allowed to add
other attributes on to your join table
, but you do not necessarily have to. In this
case it makes sense, in some cases it may not, use your judgement.
Let's make a borrower resource with scaffolding:
bin/rails generate scaffold borrower given_name:string family_name:string
Let's check to see if our scaffolded code is correct.
Now let's migrate that in so we don't confuse ourselves if we have to rollback:
bin/rails db:migrate
We want to create a new association between doctor
and patient
.
However, there are a few things to consider:
With this in mind, we can think of...: a doctor can have many types of patients and a patient can have different types of doctors. Think: primary care recipient and primary care physician.
In order to keep the work we've done so far, we'll want to modify our model associations to keep the relationship intact.
But where do we start? The Rails Guide has all the answers.
Looks like we need to add a class_name
attribute to both models and a foreign_key
attribute to the patient
model.
In models/doctor.rb
:
class Doctor < ApplicationRecord
has_many :primary_care_recipients, class_name: 'Patient'
validates :given_name, presence: true
validates :family_name, presence: true
end
In models/patient.rb
:
class Patient < ApplicationRecord
belongs_to :primary_care_physician,
class_name: 'Doctor', foreign_key: 'doctor_id'
validates :name, presence: true
validates :born_on, presence: true
end
Now rename the associations for the recipe
and ingredient
models.
We're going to use the generators that Rails provides to generate a loan
model
along with a loan
migration that includes references to both borrower
and
book
.
bin/rails generate scaffold loan borrower:references book:references date:datetime
Along with creating a loan
model, controller, routes, and serializer, Rails
will create this migration:
class CreateLoans < ActiveRecord::Migration
def change
create_table :loans do |t|
t.references :borrower, index: true, foreign_key: true
t.references :book, index: true, foreign_key: true
t.datetime :date
t.timestamps null: false
end
end
end
So our Loan
model now has the following attributes: id, borrower_id, book_id, and date.
Let's run our migration with bin/rails db:migrate
The following command let's us take a peek at our database and see how this model looks:
bin/rails db
Once we have our prompt, rails-api-library-demo_development=#
, we'll type:
\d loans
Now we see all the columns contained in the loans
table.
We're going to use the generators that Rails provides to generate an appointment
model along with an appointment
migration that includes references to both
patient
and doctor
.
bin/rails generate scaffold appointment doctor:references patient:references date:datetime
Along with creating an appointment
model, controller, routes, and serializer,
Rails will create this migration:
class CreateAppointments < ActiveRecord::Migration
def change
create_table :appointments do |t|
t.references :doctor, index: true, foreign_key: true
t.references :patient, index: true, foreign_key: true
t.datetime :date
t.timestamps null: false
end
end
end
So our appointment
model now has the following attributes: id, doctor_id,
patient_id, and date.
Let's run our migration with bin/rails db:migrate
Let's take a peek at our database and see how this model looks. Simply type:
bin/rails db
If your prompt looks like this rails-api-clinic-code-along_development=#
type:
\d appointments
You will be able to see all the columns contained in the appointments
table.
Create a join table, recipe_ingredients
, that represents the association between the recipe
and ingredient
models.
Note: Naming things is hard and if you absolutely can't come up with a good associative name, then mash the two model names together.
While we can see that in the loan
model some some code was added for us:
class Loan < ActiveRecord::Base
belongs_to :borrower
belongs_to :book
end
But we need to go into our models (borrower
, book
, and loan
) and add some
more code to finish creating our associations.
Let's go ahead and add that code starting with the book
model:
# Book Model
class Book < ActiveRecord::Base
belongs_to :author
has_many :borrowers, through: :loans
has_many :loans
end
In our borrower model we will do something similar:
# Borrower Model
class Borrower < ActiveRecord::Base
has_many :books, through: :loans
has_many :loans
end
Finally in our loan
model we're going to update it to:
class Loan < ActiveRecord::Base
belongs_to :borrower, inverse_of: :loans
belongs_to :book, inverse_of: :loans
end
What is inverse_of
and why do we need it?
When you create a bi-directional
(two way) association, ActiveRecord does not
necessarily know about that relationship.
I say necessarily because in future versions of Rails this is/may be resolved
Without inverse_of
you can get some strange behavior like this:
author = Author.first
book = author.books.first
author.given_name == book.author.given_name # => true
author.given_name = 'Lauren'
author.given_name == book.author.given_name # => false
Rails will store a
and b.author
in different places in memory, not knowing to
change one when you change the other. inverse_of
informs Rails of the
relationship, so you don't have inconsistancies in your data.
For more info on this please read the Rails Guides
While we can see that in the appointment
model some some code was added for us:
class Appointment < ActiveRecord::Base
belongs_to :doctor
belongs_to :patient
end
But we need to go into our models (patient
, doctor
, and appointment
) and
add some more code to finish creating our associations.
Let's go ahead and add that code starting with the patient
model:
# Patient Model
class Patient < ActiveRecord::Base
has_many :doctors, through: :appointments
has_many :appointments
end
In our doctor model we will do something similar:
# Doctor Model
class Doctor < ActiveRecord::Base
has_many :patients, through: :appointments
has_many :appointments
end
Finally in our appointment
model we're going to update it to:
class Appointment < ActiveRecord::Base
belongs_to :doctor, inverse_of: :appointments
belongs_to :patient, inverse_of: :appointments
end
What is inverse_of
and why do we need it? Recall the example we discussed
with author
and book
.
Go ahead and set up the three models with the appropriate associations.
Now that we can see some data it's time to update our serializers or these relationships will not be as useful as they can.
Let's add the borrowers
attribute to our attributes list in our book
serializer.
Our finished serializer should look like this:
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :borrowers
end
Let's do the same in our borrower
serializer, it should look like this once,
we're done.
class BorrowerSerializer < ActiveModel::Serializer
attributes :id, :family_name, :given_name, :books
end
Now that we can see some data it's time to update our serializers or these relationships will not be as useful as they can.
Let's add the patients
attribute to our attributes list in our doctor
serializer.
Our finished serializer should look like this:
class DoctorSerializer < ActiveModel::Serializer
attributes :id, :given_name, :family_name, :patients
end
Let's do the same in our patient
serializer, it should look like this once,
we're done.
class PatientSerializer < ActiveModel::Serializer
attributes :id, :family_name, :given_name, :doctors
end
Your turn! Add the appropriate attribute to the 'recipe' and 'ingredient' serializers.
Now, let's test this using curl. To connect books
and borrowers
we are going
to post to the join table:
curl --include --request POST http://localhost:4741/loans \
--header "Content-Type: application/json" \
--data '{
"loan": {
"borrower_id": "2",
"book_id": "2",
"date": "2016-11-22T11:32:00"
}
}'
Using this curl request as our basis, we will Create
, Read
and Update
the loans
table.
The same result could be achieved in the Rails console using Ruby. How might we write that command?
Now, let's test this using curl. To connect doctors
and patients
we are going
to post to the join table:
curl --include --request POST http://localhost:4741/appointments \
--header "Content-Type: application/json" \
--data '{
"appointment": {
"doctor_id": "2",
"patient_id": "2"
}
}'
Using this curl request as our basis, we will Create
, Read
and Update
the appointments
table. DO NOT DELETE
Now it's your turn to test the recipe_ingredients
join table. Use the above curl scripts as examples to achieve your task.
Say we wanted to delete a book or an borrower. If we delete one we proably want to
delete the association with the other. Rails helps us with this with a method
called dependent destroy
. Let's edit our book
and borrower
model to inclde it
so when we delete one, reference to the other gets deleted as well.
Let's update our models to look like the following:
# Book Model
class Book < ActiveRecord::Base
belongs_to :author
has_many :borrowers, through: :loans
has_many :loans, dependent: :destroy
end
class Borrower < ActiveRecord::Base
has_many :books, through: :loans
has_many :loans, dependent: :destroy
end
Let's test this out by using curl request to construct relationships then remove them.
curl --include --request DELETE http://localhost:4741/borrowers/2
How could we write the same command in the Rails console using Ruby?
Say we wanted to delete a patient or an doctor. If we delete one we proably want to
delete the association with the other. We'll use the dependent destroy
to help us achieve this goal. Let's edit our patient
and doctor
model to inclde it
so when we delete one, reference to the other gets deleted as well.
Let's update our models to look like the following:
# Doctor Model
class Doctor < ActiveRecord::Base
has_many :patients, through: :appointments
has_many :appointments, dependent: :destroy
end
class Patient < ActiveRecord::Base
has_many :doctors, through: :appointments
has_many :appointments, dependent: :destroy
end
Let's test this out by using curl request to construct relationships then remove them.
Go ahead and setup the dependent destroy method on the recipe
and ingredient
models.
Don't forget to test with curl requests!