Rails in Depth

Active Record Associations

Why Associations? (1)

In Rails, an association is a connection between two Active Record models. Creating association between models make common operations simpler and easier in your code.

Consider the following example:

class Author < ApplicationRecord
end
 
class Book < ApplicationRecord
end

Why Associations? (2)

Without association, if we want to add a book to an existing author, we should do this:

@book = Book.create(published_at: Time.now, author_id: @author.id)

And when deleting an author, to ensure all his books are deleted as well, we should do this:

@books = Book.where(author_id: @author.id)
@books.each do |book|
  book.destroy
end
@author.destroy

Why Associations? (3)

With Active Record associations, we can streamline these operations to:

class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end
 
class Book < ApplicationRecord
  belongs_to :author
end

@book = @author.books.create(published_at: Time.now)

@author.destroy

Type of Associations

There are three kinds of associations:

  • One-to-one relationship
  • One-to-many relationship
  • Many-to-many relationship

One-to-One Relationship

For one-to-one relationship, Rails provides us with these macro-style calls:

  • has_one
  • has_one :through
  • belongs_to

One-to-Many Relationship

For one-to-one relationship, Rails provides us with these macro-style calls:

  • has_many
  • belongs_to

Many-to-Many Relationship

For one-to-one relationship, Rails provides us with these macro-style calls:

  • has_many :through
  • has_and_belongs_to_many

Polymorphic Associations

Polymorphic association is a slightly more advanced twist on associations. With polymorphic associations, a model can belong to more than one other model, on a single association.

Has One (1)

This is how you define a has_one association in Rails.

class Supplier < ApplicationRecord
  has_one :account
end

class Account < ApplicationRecord
  belongs_to :supplier
end

Has One (2)

The migration files may look like this.

class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
  end
end
 
class CreateAccounts < ActiveRecord::Migration[5.0]
  def change
    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps
    end
  end
end

Has One Through (1)

This is how you define a has_one through association in Rails.

class Supplier < ApplicationRecord
  has_one :account
  has_one :account_history, through: :account
end
 
class Account < ApplicationRecord
  belongs_to :supplier
  has_one :account_history
end
 
class AccountHistory < ApplicationRecord
  belongs_to :account
end

Has One Through (2)

The migration files may look like this.

class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
  end
end

class CreateAccounts < ActiveRecord::Migration[5.0]
  def change 
    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps
    end
  end
end

class CreateAccountHistories < ActiveRecord::Migration[5.0]
  def change
    create_table :account_histories do |t|
      t.belongs_to :account, index: true
      t.integer :credit_rating
      t.timestamps
    end
  end
end

Has Many (1)

This is how you define a has_many association in Rails.

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end

Has Many (2)

The migration files may look like this.

class CreateAuthors < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps
    end
  end
end

class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.belongs_to :author, index: true
      t.datetime :published_at
      t.timestamps
    end
  end
end

Has Many Through (1)

This is how you define a has_many through association in Rails.

class Doctor < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end
 
class Appointment < ApplicationRecord
  belongs_to :doctor
  belongs_to :patient
end
 
class Patient < ApplicationRecord
  has_many :appointments
  has_many :doctors, through: :appointments
end

Has Many Through (2)

The migration files may look like this.

class CreateDoctors < ActiveRecord::Migration[5.0]
  def change
    create_table :doctors do |t|
      t.string :name
      t.timestamps
    end
  end
end
 
class CreatePatients < ActiveRecord::Migration[5.0]
  def change
    create_table :patients do |t|
      t.string :name
      t.timestamps
    end
  end
end

class CreateAppointments < ActiveRecord::Migration[5.0]
  def change 
    create_table :appointments do |t|
      t.belongs_to :doctor, index: true
      t.belongs_to :patient, index: true
      t.datetime :appointment_date
      t.timestamps
    end
  end
end

Has and Belongs to Many (1)

This is how you define a has_and_belongs_to_many association in Rails.

class Food < ApplicationRecord
  has_and_belongs_to_many :tags
end
 
class Tags < ApplicationRecord
  has_and_belongs_to_many :foods
end

Has and Belongs to Many (2)

The migration files may look like this.

# No need to create migration for Food as we already have it

class CreateTags < ActiveRecord::Migration[5.0]
  def change
    create_table :tags do |t|
      t.string :name
      t.timestamps
    end
  end
end

class CreateFoodsTags < ActiveRecord::Migration[5.0]
  def change
    create_table :foods_tags, id: false do |t|
      t.belongs_to :food, index: true
      t.belongs_to :tag, index: true
    end
  end
end

Polymorphic Associations (1)

This is how you define a polymorphic association in Rails.

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Food < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Restaurant < ApplicationRecord
  has_many :pictures, as: :imageable
end

Polymorphic Associations (2)

The migration files may look like this.

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

Self Joins (1)

Sometimes we need to create a model that has a relation to itself. A model Employee, for instance, can have subordinates and managers that are themselves employees too.

class Employee < ApplicationRecord
  has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
  belongs_to :manager, class_name: "Employee"
end

Self Joins (2)

The migration files may look like this.

class CreateEmployees < ActiveRecord::Migration[5.0]
  def change
    create_table :employees do |t|
      t.references :manager, index: true
      t.timestamps
    end
  end
end

Detailed Association Reference

When we use associations, we get several methods added to our model. For instance, when we declare that a Book belongs to an Author, we can call a method "book.author" for any instance of class Book.

Now let's see what methods are available for each kind of association.

Belongs To

"belongs_to" association adds these methods to our model:

association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
# So, if we have a class named Book
# that belongs_to a class named Author,
# we can do:

book.author
book.author = Author.create(name: "Author 1")
book.build_author(name: "Author 1")
book.create_author(name: "Author 1")
book.create_author!(name: "Author 1")
book.reload_author

Has One

"has_one" association adds these methods to our model:

association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
# So, if we have a class named Supplier
# that has_one a class named Account,
# we can do:

supplier.account
supplier.account = Account.create(name: "Accont 1")
supplier.build_account(name: "Account 1")
supplier.create_account(name: "Account 1")
supplier.create_account!(name: "Account 1")
supplier.reload_account

Has Many (1)

"has_many" and "has_and_belongs_to_many" association adds these methods to our model:

collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {}, ...)
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload

Has Many (2)

"has_many" and "has_and_belongs_to_many" association adds these methods to our model:

# So, if we have a class named Author
# that has_many a class named Book,
# we can do:

author.books
author.books << Book.create(title: "Book 1")
author.books.delete(@book1)
author.books.destroy(@book1)
author.books = Book.create([{title: "Book 1"}, {title: "Book 2"}])
author.book_ids
author.book_ids = Book.where("title like 'Book%'").ids
author.books.clear
author.books.empty?
author.books.size
author.books.find(title: 'Book 1')
author.books.where("title like 'Book%'")
author.books.exists?(title: "Book 1")
author.books.build(title: "Book 1")
author.books.create(title: "Book 1")
author.books.create!(title: "Book 1")
author.books.reload

Exercise (1)

1. Create an MVC for model named Tag with requirements:

  • Tag has a many-to-many relationship with Food
  • When we create or edit a Food, there will be a checkbox to pick multiple Tags that a Food has

2. Create an MVC for model named Restaurant with requirements:

  • Restaurant's attributes are name and address
  • Restaurant has one-to-many relationship with Food
  • When we create or edit a Food, there should be a dropdown field to choose the Restaurant name
  • Your Store index page should now display list of foods grouped by restaurants

Exercise (2)

3. Create an MVC for model named Review with requirements:

  • Review's attributes are reviewer's name, title, and description
  • Restaurant has one-to-many relationship with Review
  • Food has one-to-many relationship with Review
  • From your Store index page, there should be a button "write review" in every row of Restaurant and Food, and when clicked the app will show Review form for selected Restaurant or Food
  • Your Restaurant's and Food's show page should display all the reviews for respective Restaurant or Food

Of course, write all of these features in TDD fashion!

Rails in Depth - Active Record Associations

By qblfrb

Rails in Depth - Active Record Associations

  • 351