Objective |
---|
Understand how to create one-to-many and many-to-many relationships in rails. |
Understand how to modify migrations to add foreign keys to tables |
Understand how to create a join table |
Understand how to create model instances when they have associations. |
Relationship type | Abbreviation | Description | Example |
---|---|---|---|
One-to-One | 1:1 | Parent model is associated with one child | One driver has one driver's license. |
One-to-Many | 1:N | Parent model is associated with many children from another model | One owner has_many pets. |
Many-to-Many | N:N | Two models. Both can be associated with many of the other. | Libraries and books. One library can have many books, while one book can be in many libraries. |
Before we move into the actual syntax, let's work together for 10 minutes to come up with some examples of both one-to-one and one-to-many relationships.
Example:
One owner has_many
pets and...
A pet belongs_to
one owner (this model will have a foreign key [FK] owner_id
)
Always remember! : Whenever there is a belongs_to
in the model, there should be a FK in the matching migration!
### So to set this up, we'll need two models:
rails generate model Pet name:string
rails generate model Owner name:string
Then, we'll need to add the following to our models:
class Owner < ActiveRecord::Base
has_many :pets
end
class Pet < ActiveRecord::Base
belongs_to :owner
end
Note that in this case, belongs_to
uses the singular form of the class name (:owner
), while has_many
uses the pluralized form (:pets
).
But if you think about it, this is exactly how you'd want to say this in plain English. For example, if we were just discussing the relationship between pets and owners, we'd say:
- "One owner has many pets"
- "A pet belongs to an owner"
Now, as mentioned, we have to add a foreign key in our migration, so in our pets
migration file we should add:
t.integer :owner_id
But wait, we could also do something more rail-sy and say
t.references :owner
This makes the association a bit more semantic and readable and has a few bonuses:
- It defines the name of the foreign key column (in this case,
owner_id
) for us. - It adds a foreign key constraint which ensures referential data integrity in our Postgresql database.
But wait, there's more...
We can actually get even more semantic and rail-sy and say:
t.belongs_to :owner
This will do the same thing as t.references
, but it has the added benefit of being super semantic for anyone reading your migrations later on.
First things first, we need to create our models, run our migrations, do all things necessary to setup our database.
Let's all run:
rake db:create
rake db:migrate
Now, let's jump into our rails console by typing rails c
at a command prompt, and check out how these new associations can help us define relationships between models:
Pet.count
Owner.count
fido = Pet.create(name: "Fido")
lassie = Pet.create(name: "Lassie")
nathan = Owner.create(name: "nathan")
nathan.pets
fido.owner
nathan.pets << fido # Makes "fido" one of my pets
nathan.pets << lassie # Makes "lassie" another one of my pets
nathan.pets.size
nathan.pets.map(&:name)
nathan.pets.each {|x| puts "My pet is named #{x.name}!"}
fido.owner
# What's going to be returned when we do this?
fido.owner.name
Remember: We just saw that in Rails, we can associate two model instances together using the <<
operator.
If you were to make the mistake of running rake db:migrate
before adding a foreign key to the table's migration, it's ok. There's no need to panic. You can always fix this by creating a new migration...
rails generate migration NameOfMigrationHere
...and then modifying it to include the following:
change_table :pets do |t|
# You ONLY need to add ONE OF THESE THREE to your new migration
t.integer :owner_id
# OR...
t.references :owner
# OR...
t.belongs_to :owner
end
Let's jump over to in-class exercise #1 where you'll all work in groups on a solution.
Let's think about the example of students and courses. A student can take many courses and a course will have many students. If you think back to our SQL discussions, you'll recall that we used a join table to create this kind of association.
Remember that a join table has two different FKs, one for each model it is associating. In the example below, 3 students have been associated with 4 different courses.
student_id | course_id |
---|---|
1 | 1 |
1 | 2 |
1 | 3 |
2 | 1 |
2 | 4 |
3 | 2 |
3 | 3 |
Let's make sure we understand how this join table works before moving on.
We use the has_many :relatedModel, :through => :joinTableName
method.
We'll start by creating 3 models
rails generate model Student name:string
rails generate model Course name:string
rails generate model Enrollment enrollment_date:date
Note that "Enrollment" is our join table.
After generating our models, we need to open them in sublime and edit them so they include the proper associations.
Your models should look as follows once you've finished making the necessary changes:
# models/course.rb
class Course < ActiveRecord::Base
has_many :enrollments
has_many :students, :through => :enrollments
end
# models/student.rb
class Student < ActiveRecord::Base
has_many :enrollments
has_many :courses, :through => :enrollments
end
# models/enrollment.rb
class Enrollment < ActiveRecord::Base
belongs_to :course
belongs_to :student
end
We'll also want to modify the migration for the enrollment
model before we run db:migrate. Make your enrollment model look like this:
class CreateEnrollments < ActiveRecord::Migration
def change
create_table :enrollments do |t|
t.date :enrollment_date
t.timestamps null: false
# The foreign keys for the associations are defined below
t.references :student
t.references :course
# or we can do this instead
t.belongs_to :student
t.belongs_to :course
end
end
end
Make sure your save the file before running rake db:migrate
.
(Also, just a reminder, if you're using postgresql, be sure to run rake db:create
first!)
Open the rails console by running rails c
in terminal.
At the command prompt, let's create some students:
nathan = Student.create(name: "Nathan")
travis = Student.create(name: "Travis")
justin = Student.create(name: "Justin")
Now we can create some courses too:
algebra = Course.create(name: "Algebra")
science = Course.create(name: "Science")
english = Course.create(name: "English")
french = Course.create(name: "French")
Now, because we've used :through
, we can create our associations in the same way we do for a 1:N association. If you're the curious type, you can try just using has_many
without through
(outside of class time please!!) and then using the console to experiment and figure out how you'd associate, and then access various instances together.
nathan.courses << algebra
nathan.courses << french
travis.courses << science
travis.courses << english
travis.courses << french
# Here's a little trick: Use an array to associate multiple courses with a student in just one line of code.
justin.courses << [english, algebra]
Once you've done all of this, try the following and see if your output matches the below in irb.
nathan.courses.map(&:name)
# Outputs: => ["Algebra", "French"]
travis.courses.map(&:name)
# Outputs: => ["Science", "English", "French"]
justin.courses.map(&:name)
# Outputs: => ["Algebra", "English"]
To do this exercise, let's head over to our many-to-many exercise file and work together in groups of two.
For your reference. If you'd like to experiment with these, go nuts! (after class please...)