Rails Association — Part 2

Juzer Shakir
9 min readSep 10, 2021

--

An elaboration of has_many, has_many-:through & HABTM associations.

Prerequisite:

Following topics to be covered in this article

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.

Source Code

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
Visual representation of the relationship between models.

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.

Source Code

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.

Source Code

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.

Source Code

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.

--

--