Rails Association — Part 1

Juzer Shakir
Nerd For Tech
Published in
9 min readSep 11, 2021

--

What, Why, How & 3 Types of Associations discussed.

Prerequisite:

Following topics to be covered with examples in this article

What is Association?

Association is one of the features of Active Record which enables us to link between instances of 2 or more tables through model or Active Record Object.

Why Association?

This tool comes in handy when we want to extract data of one or more instances of a table B through another table A.

Let's consider a situation where a person has a single or multiple bank accounts. We create 2 different tables for it, first, which contains details of people, let's call that table users and another table as bank_details which contains details related to their bank.

Now, If we had many users, where each user had one or more bank accounts and we wanted to extract data of single users’ bank details, rails wouldn't know which bank details to extract of that particular user since we haven’t set any relationship between tables.

Not possible to link without a foreign key and association between tables.

If you’re curious which association we would use for this example, it would be has_many-belongs_to type of association.

How to set up connections between models:

Without Association:

However, there’s a way to extract the bank details of a user without explicitly declaring association in model files, by providing a foreign key to bank_details table with thereference method is given through migration.

Why foreign-key on bank_details table? Because bank_details instances provide more data about a particular instance of users table.

I have explained the foreign key definition and declaration in my previous article.

So, after setting up a foreign key, lets input bank data in the bank_details table:

Our model files:

# app/model/user.rb
class User < ApplicationRecord
end
# app/model/bank_detail.rb
class BankDetail < ApplicationRecord
end

In the terminal, first, we load the required user:

user = User.find_by(name: "Juzer Shakir")

then set that users’ bank details:

bank_detail = BankDetail.create(name: "ABC Bank", user_id: user.id)

The user_id is a foreign key attribute set through the reference method in the migration file. We create a new bank detail for the user Juzer Shakir by setting the name of the bank as ABC Bank and set foreign keys’ value explicitly as the primary key value of Juzer Shakir in users table.

Without association, we can link one or many bank details of a user through a foreign key.

This way now we can extract all the bank details of an initialized user using a foreign key.

bank_details = BankDetail.where(user_id: user.id)

What if we want to delete a particular user from our table users and its corresponding bank details from the bank_details table? Well, first, we would initialize that user and extract its bank data, (following the above steps) and then:

bank_details.each do | bank_detail |
bank_detail.destroy
end
user.destroy

We loop through users’ list of bank accounts, and then manually delete each of them and then delete that user. This ensures that all the bank details of a user are deleted along with the user.

Now, let's see how this is done when given an association:

With association:

After declaring the appropriate association in the model file, we invoke several useful model methods (discussed in Part 3) in our application with which we can manipulate data in the table.

# app/model/user.rb
class User < ApplicationRecord
has_many :bank_details, dependent: :destroy
end
# app/model/bank_detail.rb
class BankDetail < ApplicationRecord
belongs_to :user
end

Now, to create a new bank account of an initialized user:

bank_detail = user.bank_details.create(name: "DCB Bank")

See how instead of explicitly setting the foreign key, we use the power of association which sets the value of the foreign key appropriately and creates a new instance in the bank_details table.

Extracting all the bank details of that user:

user.bank_details

And if we wish to delete the user along with its bank details, it's way easier:

user.destroy

The :dependent option provided to the User model helped delete all the bank details associated with that user. There are many other options provided by Rails for different types of associations (more on this in Part 3).

Types of Association

Active Record supports 9 types of Association and those are:

  1. belongs_to
  2. has_one
  3. has_one :through
  4. has_many
  5. has_many :through
  6. has_and_belongs_to_many
  7. Polymorphic
  8. Self-Join
  9. Single Table Inheritance

This article will cover the first 3 associations, #4–6 will be covered in Part 2 and the remaining associations have their separate articles.

So, now let’s begin with our first association, belongs_to which is the most simple and used association of all.

belongs_to

‘belongs_to’ sets up a relationship with another model such that each instances of declaring model (where belongs_to is declared) ‘belongs to’ exactly one instance of another model.

The definition is self-explanatory and the example we saw above exemplifies it. However, we can also set this as a one-directional association, meaning we set a relationship with another model from only one side and not both which means we declare belongs_to in just one model file. So taking the same example from above, our model files now will be:

# app/model/user.rb
class User < ApplicationRecord
end
# app/model/bank_detail.rb
class BankDetail < ApplicationRecord
belongs_to :user
end
The one-directional connection between 2 tables

A belongs_to declared in model file alone doesn’t ensure reference consistency but with the foreign key in the table which is declared through migration as:

create_table :bank_details do | t |
t.reference :user, foreign_key: true
#...
end

In a one-directional relationship, each instance of bank_details table knows its user, but each instance of the user table doesn’t know about their bank details.

To set up a bi-directional relationship, we can use has_one or has_many on a model in combination with belongs_to to an opposite model, as seen in the example above.

Note:
The table name declared to belongs_to method will always be in singular-snake-case.

has_one

The has_one association sets anyone instance of a table A to only one instance of another table B.

For example: “A user can have only one passport”. We would set its association as follows:

# app/model/user.rb
class User < ApplicationRecord
has_one :passport
end
# app/model/passport.rb
class Passport < ApplicationRecord
end

This sets a one-directional relationship. So the table, passed as a value to has_one should have a foreign key that refers to the model where has_one is declared. In our example, the passport table should have a foreign key that refers to the user table.

The one-directional connection between 2 tables

Typically, associations work in two directions, setting up a bi-directional association by requiring the declaration of association on two different models. To make our example bi-directional, we set a belongs_to association on another model (More on bi-directional in Part 3):

# app/model/user.rb
class User < ApplicationRecord
has_one :passport
end
# app/model/passport.rb
class Passport < ApplicationRecord
belongs_to :user
end

Now, to create a new user and its passport instance in the database, fire up your terminal, navigate to an appropriate directory, and run rails c or rails console

  • Create a new instance in the users table:
$ u = User.create(name: "Akshay")
=> #<User id: 1, name: "Akshay", created_at: "##", updated_at: "##">
  • Create a passport of that user, a new instance in passports table:
$ u.passport = Passport.create(number: 97645)
=> #<Passport id: 1, number: 97645, user_id: 1, created_at: "##", updated_at: "##">

Notice, user_id (aka. foreign-key) links to primary-key (id) of the user we created. Now to extract a user’s passport:

$ u.passport

Or to extract a user from passports table, first we load an instance from the passport table:

$ p = Passport.find(1)
=> #<Passport id: 1, number: 97645, user_id: 1, created_at: "##", updated_at: "##">
$ p.user
=> #<User id: 1, name: "Akshay", created_at: "##", updated_at: "##">

The instance methods, passport & user given to the class object are created by Active Record because we gave these values, :passport & :user, to our association methods (has_one / belongs_to) which link to the appropriate model class.

Source Code

has_one or belongs_to?

To set a one-to-one relationship between 2 models, we can use has_one and belongs_to combinations but how do we know exactly which model is to be declared with has_one or belongs_to?

The answer is in your data! So for instance, a person has only one smartphone, and we have a model named as Person and a Smartphone. So in Persons’ model, we declare has_one association because our data says that a person can have only one smartphone and since we know which model has has_one association, the other model will have belongs_to association. However, if we swapped the association, that would mean ‘a smartphone has only one person’, which is opposite to our example.

And once you know which model declares a belongs_to association, we can then set a foreign key to that corresponding table through migration.

has_one — :through

In this association, the concept of linking models is similar to the has_one association, but instead of 2, we need 3 models. So...

The has_one-:through association sets up 1-to-1 connection between 3 models where first models’ instance can link-up to third model via second model, where second & third model include a foreign-key.

To better understand this, let's take an example of “ a person having one Amazon pay account through its Amazon account”.

To implement the above example, we would need 3 tables in a database, and in their corresponding model files we would set the following associations:

# app/model/person.rb
class Person < ApplicationRecord
has_one :amazon_account
has_one :amazon_payment, through: :amazon_account
end
# app/model/amazon_account.rb
class AmazonAccount < ApplicationRecord
belongs_to :person
has_one :amazon_payment
end
# app/model/amazon_payment.rb
class AmazonPayment < ApplicationRecord
belongs_to :amazon_account
end

This is the visual representation of how models will link:

This association enables us to extract the data of a person’s Amazon payment directly instead of first extracting data from a person’s Amazon account and then from its Amazon payment. To get a better understanding, let's set up some data in our tables:

$ person = Person.create(name: "Shourya Roy", age: 25, gender: "Male", city: "New York")

Associations enable us to access persons’ Amazon account through .amazon_account method to a class object of the class Person.

$ person.amazon_account
=> nil

Since we haven’t set any data on that persons’ Amazon account, it will return value as nil, so let's set it up:

$ person.amazon_account = AmazonAccount.create(public_name: "SRoy", email: "abc@yahoo")
=> #<AmazonAccount id: 1, public_name: "SRoy", profile_photo: nil, email: "abc@yahoo", about: nil, person_id: 1, created_at: "##", updated_at: "##">

person_id links the instance of the Amazon account to the correct person. Now to set persons’ Amazon payment, we need to set it through an Amazon account.

$ acc = AmazonAccount.first$ acc.amazon_payment = AmazonPayment.create(upi: "12345@xyz", cashback: 14.00, balance: 29.00)
=> #<AmazonPayment id: 1, cashback: 14.0, upi: "12345@xyz", balance: 29.0, amazon_account_id: 1, created_at: "##", updated_at: "##">

Let's visualize the data we set above:

Extracting all Amazon payment details of a person:

person.amazon_payment

To extract only balance value of that user:

person.amazon_payment.balance
= 29.00

Source Code

The remaining associations has_many, has_many-:through and has_and_belongs_to_many are discussed thoroughly in Rails Association — Part 2. See you there!

--

--