Rails Association — Part 2
An elaboration of has_many
, has_many-:through
& HABTM associations.
Prerequisite:
Table of Contents
↦ has_many
↦ has_many — :through
↪ one-to-many
↪ many-to-many
The belongs_to
, has_one
and has_one-:through
associations are discussed in my previous article. In this article, we will discuss, has_many
, has_many-:through
and has_and_belongs_to_many
associations.
has_many
has_many
association links declaring models’ each instance to zero or more instances of another model.
In other words, it sets a one-to-many relationship between instances of different models. Usually, the opposite side of the has_many
model will have a belongs_to
association which together build a has_many
-belongs_to
association.
Let’s understand this with an example:
“A school has many students and a student belongs to a school.”
We build 2 tables in the database, first of school and second of students which would generate corresponding models, and in those model files we would set associations as follows:
# app/model/school.rb
class School < ApplicationRecord
has_many :students
end# app/model/bank_detail.rb
class Student < ApplicationRecord
belongs_to :school
end
Note:
has_many
method takes a plural name of the model class we want to link to the declaring model.
After setting up associations, we create some data for both models through the rails console:
$ sc = School.create(name:"Al Wadi Al Kabir", city: "Muscat", country:"Oman")
=> #<School id: 1, name: "Al Wadi Al Kabir", city: "Muscat", country: "Oman, created_at: "##", updated_at: "##">
Creating new students of that school:
$ sc.students.create(name: "Vaishaki", age: 8, grade: 3)
$ sc.students.create(name: "Dean", age: 12, grade: 7)
$ sc.students.create(name: "Arun", age: 13, grade: 8)
Visualizing the above data created:
Observing the above table we can say that Al Wadi Al Kabir school has 3 students.
We can get a list of all students of a school by:
$ sc.students
=> [#<Student id: 1, name: "Vaishaki", age: 8, grade: 3, school_id: 1, created_at: "##", updated_at: "##">, #<Student id: 2, name: "Dean", age: 12, grade: 7, school_id: 1, created_at: "##", updated_at: "##">, #<Student id: 3, name: "Arun", age: 13, grade: 8, school_id: 1, created_at: "##", updated_at: "##">]
The school_id
(foreign key) in the students
table represents which school it refers to in the schools
table.
has_many-:through
has_many-:through
association links the declaring models’ instance to zero or more instances of another model by proceeding through a third model.
Very similar to the has_many
association, but instead of 2 models, we need 3 models, similar to the has_one-:through
association we saw in the previous article which sets a one-to-one relationship.
This association however sets relationship in 2 ways, one-to-many
and many-to-many
. It is the declaration of a belongs_to
method in models and a foreign key in the table that differentiates these relationships. Let's take a look at an example of each relationship:
One-to-Many
Data: “An airport has many flights taking off where each flight has many passengers in it.”
The association structure is similar to a has_one-:through
relationship, but here, instead of has_one
we set has_many
. So we have our association as follows:
# app/model/airport.rb
class Airport < ApplicationRecord
has_many :flights
has_many :passengers, through: :flights
end
# app/model/flight.rb
class Flight < ApplicationRecord
belongs_to :airport
has_many :passengers
end
# app/model/passenger.rb
class Passenger < ApplicationRecord
belongs_to :flight
end
We have 2 tables that include foreign keys which helps link instances with other tables.
Inputting some data to our tables:
$ a = Airport.create(name: "Chennai", sort:"International", city:"Chennai", country:"India")$ a.flights.create(name: "Continental Airlines, INC", carrier_code: "COA", rating:95, seats:150)$ a.flights.create(name: "Air France", carrier_code: "AFR", rating:90, seats:225)$ a.flights
=> <Flight id: 1, name: "Continental Airlines, INC", carrier_code: "COA", rating: 95, seats: 150, airport_id: 1, created_at: "##", updated_at: "##">, #<Flight id: 2, name: "Air France", carrier_code: "AFR", rating: 90, seats: 225, airport_id: 1, created_at: "##", updated_at: "##">$ f = a.flights.second
=> [#<Flight id: 2, name: "Air France", carrier_code: "AFR", rating: 90, seats: 225, airport_id: 1, created_at: "##", updated_at: "##">]$ f.passengers.create(name:"Anik Barwa", group:"economy", seat_no:29)$ f.passengers.create(name:"Prithvi Raj", group:"premium", seat_no:176)$ a.passengers
=> [#<Passenger id: 1, name: "Anik Barwa", group: "economy", seat_no: 29, flight_id: 2, created_at: "##", updated_at: "##">, #<Passenger id: 2, name: "Prithvi Raj", group: "premium", seat_no: 176, flight_id: 2, created_at: "##", updated_at: "##">]
Visualizing the data we created:
The Chennai Airport has 2 flights, named Continental Airlines & Air France, of which Air France has 2 passengers, Anik Barwa & Prithvi Raj. Or we could say that the following passengers are on an Air France flight which will take off from Chennai airport.
Many-to-Many
Data: “A teacher takes exams of many students, where each student has many exams to give of different teachers.”
Here, we would have 3 tables, teachers
, students
& exams
and set their associations as follows:
# app/model/teacher.rb
class Teacher < ApplicationRecord
has_many :exams
has_many :students, through: :exams
end# app/model/exam.rb
class Exam < ApplicationRecord
belongs_to :teacher
belongs_to :student
end# app/model/student.rb
class Student < ApplicationRecord
has_many :exams
has_many :teachers, through: :exams
end
Here a single table, exams
has 2 foreign keys which link to 2 different tables, where exams
tables’ each instance will give us data of that exam for a particular student & of that particular teacher.
Inputting some data into our tables by creating new teachers & students:
$ t1 = Teacher.create(name: "Shweta")
$ t2 = Teacher.create(name: "Raghani")$ s1 = Student.create(name: "Anik Barwa")
$ s2 = Student.create(name: "Prithvi Raj")
1st way to create exam instances through the teacher:
$ t1.exams.create(marks: 82, student_id: 1)
2nd way to create exam instances through a student:
$ s1.exams.create(marks: 97, teacher_id: 2)
Visualizing the data:
Anik Barwa gave 2 exams from 2 different teachers, and their primary keys are set as foreign keys in exams
table. The combination of foreign keys should always be unique for each instance in exams
table.
Extracting exam and student data of a teacher:
$ t1.exams
=> [#<Exam id: 1, marks: 82, teacher_id: 1, student_id: 1, created_at: "##", updated_at: "##">]$ t1.students
=> [#<Student id: 1, name: "Anik Barwa", created_at: "##", updated_at: "##">]
Extracting exam and student data of a student:
$ s1.teachers
=> [#<Teacher id: 1, name: "Shweta", created_at: "##", updated_at: "##">, #<Teacher id: 2, name: "Raghani", created_at: "##", updated_at: "##">]$ s1.exams
=> [#<Exam id: 1, marks: 82, teacher_id: 1, student_id: 1, created_at: "##", updated_at: "##">, #<Exam id: 2, marks: 97, teacher_id: 2, student_id: 1, created_at: "##", updated_at: "##">]
Extracting data through exams:
$ e = Exam.first$ e.student.name
=> "Anik Barwa"$ e.teacher.name
=> "Shweta"$ e.marks
=> 82
The key difference to set the one-to-many or many-to-many relationships in the has_many-:through
association is in the model file and schema file.
To set one-to-many, we give 2 belongs_to
in 2 different model files and their corresponding 2 foreign keys in 2 different tables through migrations.
To set many-to-many, we give 2 belongs_to
in a single model file and their corresponding 2 foreign keys in a single table through migration. Sometimes there might be more than 2 foreign keys in an application.
In this example, we declared 3 different tables explicitly with primary keys, however, in the next association, one of the tables will be without a primary key.
has_and_belongs_to_many
This HABTM association, which creates many-to-many relationship, links to zero or more instances of declaring model to another model through a table without the primary key.
Let's thoroughly understand this with an example.
Data: “A Medium Publication has many writers, where a writer can belong to many different Medium Publications.”
We first create 2 tables, medium_publications
and writers
and then set the association as follows:
# app/model/medium_publication.rb
class MediumPublication < ApplicationRecord
has_and_belongs_to_many :writers
end# app/model/writer.rb
class Writer < ApplicationRecord
has_and_belongs_to_many :medium_publications
end
We then create a third table that will only hold the combination of foreign keys of both tables and nothing else, not even a primary key, because it won't be a different entity or model, it's only for binding the 2 other models.
We run the following migration with a migration
task instead of model
and a migration name as CreateJoinTable
which will invoke the create_join_table
method followed by the foreign key attributes we want in the table:
rails g migration CreateJoinTable medium_publication:references writer:references
This will create the following migration file:
The arguments passed to the create_join_table
method is what creates the name of the table in the database. So in this example, it will create a table named medium_publications_writers
. It will create the table name in the same order as arguments are passed to create_join_table
method.
It's important to note that model names passed to create_join_table
method should be in ascending order meaning rails will expect the initial letters of model names in alphabetical order. In our case, ‘m’ for medium_publications
will be given first followed by ‘w’ for writers
, since ‘m’ comes before ‘w’. For more info on this, visit.
If they’re not passed in alphabetical order, then edit it appropriately. And then run the migration file.
Note: create_join_table
method implicitly sets id
to false
, so we have no primary key in our table.
Initializing some data:
First, let's create data for publications and their writers:
$ m1 = MediumPublication.create(username: "geekculture")$ m1.writers.create(username: "juzer-shakir")$ m1.writers
=> [#<Writer id: 1, username: "juzer-shakir", created_at: "##", updated_at: "##">]
Now we create a new writer and assign him to an existing publication:
$ w = Writer.create(username: "lew-brown")$ w.medium_publications << MediumPublication.find_by(username: "geekculture")$ w.medium_publications
=> [#<MediumPublication id: 1, username: "geekculture", created_at: "##", updated_at: "##">]$ m2 = MediumPublication.create(username: "betterprogramming")
Visualizing our data:
The above data tells us that the publication geekculture has 2 writers, juzer-shakir and lew_brown, or in other words these writers belong to geekculture publication. These writers can also belong to betterprogramming publication and many others where their relationships would be captured by the medium_publications_writers
table.
The combination of foreign keys in the medium_publications_writers
table should always be unique because the table captures the relationship between each instance of writer
and publication
table with their primary key, which is always unique for each instance of a table.
has_many-:through
or has_and_belong_to_many
?
These associations are very similar and at times it will confuse us to choose which association and when. Rails official documentation clears this confusion by thoroughly explaining it.
Relationships
Below is a chart that shows us which association falls into which relationship.
All of the associations that fall into these relationships must be used in combination of belongs_to
association.
We pluralize the name of the model we are relating for one-to-many & many-to-many relationships and give a singular form of model name to the one-to-one relationship.
In my next article, Rails Association Part 3, I will discuss conventional & Unconventional Foreign-keys, what is association name, and the different methods it generates.