Skip to main content Link Search Menu Expand Document (external link)

Active Record Associations

Although Ruby on Rails makes associating models very simple, there are some practices you should be aware of as you use the framework to create progressive online applications. This article will show you how to use Active Record Associations in the Ruby on Rails framework.

Table of contents

  1. What is Associations?
  2. Why Associations?
  3. Relationships
    1. One to One
    2. One to Many
    3. Many to Many
    4. Polymorphic
  4. Types of Associations
    1. belongs_to
    2. has_one
    3. has_many
    4. has_many :through
    5. has_one :through
    6. has_and_belongs_to_many
  5. Create Comments
  6. Nested Resources
  7. Create Categories
  8. Soft Kill
  9. dependent

What is Associations?

Active Record in Ruby on Rails offers a connection between tables in a relational database. The relationship between models is defined via association. Additionally, it serves as the link between two Active Record models.

Why Associations?

Associations allow us to create relationships between any two ActiveRecord::Base models. We can greatly improve the design of our code by telling Rails that two models are related in some way. Associations make common operations in your code simpler and easier.

Relationships

When using Associations, you must understand the relationships between your models. Is it a one-to-one, one-to-many, or many-to-many relationship? Knowing the type of relationship can be extremely useful later on.

One to One

This relationship is related to one instance of another model. One of the models in this relationship will have a has_one method invocation and another will have a belongs_to.

One to Many

This is one of the most common Rails relationships. One to many means that there are zero or more instances of another model. This relationship consists of two models: has_many and belongs_to.

Many to Many

When two models share a has_many association, it indicates a many-to-many relationship. We have has_and_belongs_to_many and has_many: through for this relationship.

Polymorphic

A polymorphic association is an Active Record association that can connect multiple models. It is the most advanced association we can have. Having a model that can belong to more than one other model, polymorphic associations allow for single associations.

Types of Associations

To know what type of association between models we are going to use, we must first learn the different types of associations. Rails support six types of associations: belongs_to, has_one, has_many, has_many :through, has_one :through and has_and_belongs_to_many.

belongs_to

A belongs_to association connects one model to another, having each instance of the declaring model belongs to one instance of the other model.

has_one

A has_one association indicates that this model is referenced by another model. This association can fetch or access the other connected model.

has_many

A has_many association is similar to a has one association, but it represents a one-to-many relationship with another model. This association is frequently found on the other side of a belongs_to association, and it indicates that each model instance has one or more instances of another model.

has_many :through

A has many:through association is frequently used to establish a many-to-many relationship with another model. This association indicates that by passing through a third model, the declaring model can be matched with zero or more instances of another model.

has_one :through

A has one:through association establishes a one-to-one relationship with another model. This association indicates that by passing through a third model, the declaring model can be matched with one instance of another model.

has_and_belongs_to_many

A has and belongs to many association forms a direct many-to-many connection with another model without the use of an intermediary model. This relationship indicates that each instance of the declaring model refers to one or more instances of another model.

Create Comments

For our project we will make a post that has many comment. Let’s generate a comment model with a content attribute. Open your project container then run rails generate model Comment content:string post:references.

 root@0122:/usr/src/app# rails generate model Comment content:string post:references
  invoke  active_record             
  create    db/migrate/xxxxxxxxxxxxxx_create_comments.rb
  create    app/models/comment.rb 

You will have a comments migration file that will look like this:

# db/migrate/xxxxxxxxxxxxxx_create_comments.rb

class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.string :content
      t.references :post, null: false, foreign_key: true

      t.timestamps
    end
  end
end

Read Carefully:

This migration will create a post_id column. The references is a method that simplifies creating columns, indexes, foreign keys, and even polymorphic association columns.

On the other hand, the generated comment model will have a belongs_to association and will be like this:

# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :post
end

Generate the database table, rails db:migrate.

 root@0122:/usr/src/app# rails db:migrate
== xxxxxxxxxxxxxx CreateComments: migrating ===================================
-- create_table(:comments)              
   -> 0.0128s                           
== xxxxxxxxxxxxxx CreateComments: migrated (0.0129s) ==========================

Finish the setup by adding association has_many comments to post model.

# app/models/post.rb

class Post < ApplicationRecord
  validates :title, presence: true
  validates :content, presence: true
+
+ has_many :comments
end

Try if the relationship you made have been correctly established. Open rails console. Create a comment object and associate it with post.

irb(main):001:0> post = Post.first
  Post Load (10.5ms)  SELECT `posts`.* FROM `posts` ORDER BY `posts`.`id` ASC LIMIT 1
=> #<Post:0x00005628c4400838 ... >
irb(main):002:0> post.comments
  Comment Load (0.4ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` = 1
=> []
irb(main):003:0> comment = Comment.new(content: 'comment of post 1', post_id: post.id)
=> #<Comment:0x00005628c40bb9e8 id: nil, content: "comment of post 1", post_id: 1, created_at: nil, updated_at: nil>
irb(main):004:0> comment.save
  TRANSACTION (0.3ms)  BEGIN
  Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1                                            
  Comment Create (0.5ms)  INSERT INTO `comments` (`content`, `post_id`, `created_at`, `updated_at`) VALUES ('comment of post 1', 1, 'xxxx-xx-xx xx:xx:xx.xxxxx', 'xxxx-xx-xx xx:xx:xx.xxxxxx')
  TRANSACTION (0.8ms)  COMMIT                                                                                                
=> true
irb(main):005:0> post.comments
=> []
irb(main):006:0> post.reload
  Post Load (0.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1
=> #<Post:0x00005628c4400838 ... >
irb(main):007:0> post.comments
  Comment Load (0.5ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` = 1
=> [#<Comment:0x00007f0c78660248 ... >]

Nested Resources

We can document parent/child relationships directly in our routes thanks to nested resources in rails. The parent (Post) and child (Comment) are taken from the two models Post and Comment. Nested routes are yet another method for capturing these relationships in your routing.

First, let’s generate our comment controller.

 root@0122:/usr/src/app# rails generate controller CommentsController --skip-routes --no-test-framework
      create  app/controllers/comments_controller.rb
      invoke  erb
      create    app/views/comments
      invoke  helper
      create    app/helpers/comments_helper.rb

Setup our nested comment routes to posts resources and remove the unnecessary comments.

# config/routes.rb

Rails.application.routes.draw do
-  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
-
- # Defines the root path route ("/")
  root 'welcome#index'

- resources :posts
+ resources :posts do
+   resources :comments
+ end
end

Check the routes it created.

 root@0122:/usr/src/app# rails routes
                 Prefix Verb   URI Pattern                                        Controller#Action
                   root GET    /                                                  welcome#index
          post_comments GET    /posts/:post_id/comments(.:format)                 comments#index
                        POST   /posts/:post_id/comments(.:format)                 comments#create
       new_post_comment GET    /posts/:post_id/comments/new(.:format)             comments#new
      edit_post_comment GET    /posts/:post_id/comments/:id/edit(.:format)        comments#edit
           post_comment GET    /posts/:post_id/comments/:id(.:format)             comments#show
                        PATCH  /posts/:post_id/comments/:id(.:format)             comments#update
                        PUT    /posts/:post_id/comments/:id(.:format)             comments#update
                        DELETE /posts/:post_id/comments/:id(.:format)             comments#destroy
                  posts GET    /posts(.:format)                                   posts#index
                        POST   /posts(.:format)                                   posts#create
               new_post GET    /posts/new(.:format)                               posts#new
              edit_post GET    /posts/:id/edit(.:format)                          posts#edit
                   post GET    /posts/:id(.:format)                               posts#show
                        PATCH  /posts/:id(.:format)                               posts#update
                        PUT    /posts/:id(.:format)                               posts#update
                        DELETE /posts/:id(.:format)                               posts#destroy

Notice that in your routes, it also takes up a post_id parameter. This is the basis of whose comments parent (post) we are trying to access.

Post Comments Index

Our CommentController#index will be like this.

# app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :set_post

  def index
    @comments = @post.comments
  end

  private

  def set_post
    @post = Post.find params[:post_id]
  end
end

Create view for comments index page.

<!-- app/views/comments/index.html.erb -->

<h1>Title <%= @post.title %></h1>
<h2>Comments</h2>
<ul>
<% @comments.each do |comment| %>
    <li><%= comment.content %>(<%= comment.created_at %>)</li>
<% end %>
</ul>

<%= link_to 'Post List', posts_path %>

Add comments link to our posts page.

<!-- app/views/posts/index.html.erb -->

  <!-- ... -->
  <td>
    <!-- ... -->
    <%= button_to 'Delete', post_path(post), method: :delete %>
+   <%= link_to 'Comments', post_comments_path(post) %>
  </td>
  <!-- ... -->

Post Comments New & Create

Create new and create actions for comments controller.

Read Carefully:

Another way of creating a related object is to create a child object directly from the parent object with the use of build method.

# app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  # ...
+ def new
+   @comment = @post.comments.build
+ end
+
+ def create
+   @comment = @post.comments.build(comment_params)
+   if @comment.save
+     flash[:notice] = 'Comment created successfully'
+     redirect_to post_comments_path(@post)
+   else
+     render :new
+   end
+ end
+
  private
  # ...
+
+ def comment_params
+   params.require(:comment).permit(:content)
+ end
end

Create comments new view, and add comments new link to posts index page.

<!-- app/views/comments/new.html.erb -->

<h1>Title: <%= @post.title %></h1>

<%= form_with model: @comment, url: post_comments_path(@post), data: { turbo: false } do |form| %>
  <% if @comment.errors.any? %>
    <ul>
      <% @comment.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  <% end %>
  <div>
    <%= form.label :content %>
    <%= form.text_field :content %>
  </div>
  <%= form.submit %>
<% end %>
<!-- app/views/posts/index.html.erb -->

  <!-- ... -->
  <td>
    <!-- ... -->
    <%= link_to 'Comments', post_comments_path(post) %>
+   <%= link_to 'New Comment', new_post_comment_path(post) %>
  </td>
  <!-- ... -->

Afterwards, add simple validation for comments content attribute.

# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :post
+
+ validates :content, presence: true
end

Post Comments Show

With the comments having only one attribute and already being displayed in comments index page, creating show view will be unnecessary. To remove the route, add the arguments except: :show in our resources :comments.

# config/routes.rb

Rails.application.routes.draw do
  root 'welcome#index'

  resources :posts do
-   resources :comments
+   resources :comments, except: :show
  end
end

Post Comments Edit & Update

Add edit and update actions in our CommentsController.

# app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :set_post
+ before_action :set_comment, only: [:edit, :update]
  # ...
+ def edit; end
+
+ def update
+   if @comment.update(comment_params)
+     flash[:notice] = 'Comment updated successfully'
+     redirect_to post_comments_path(@post)
+   else
+     render :edit
+   end
+ end
+
  private
  # ...
+
+ def set_comment
+   @comment = @post.comments.find(params[:id])
+ end
end

Create _form.html.erb partial and render to edit view. Replace also the form in new view.

<!-- app/views/comments/_form.html.erb -->

<%= form_with model: comment, url: path, data: { turbo: false } do |form| %>
  <% if comment.errors.any? %>
    <ul>
      <% comment.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  <% end %>
  <div>
    <%= form.label :content %>
    <%= form.text_field :content %>
  </div>
  <%= form.submit %>
<% end %>
<!-- app/views/comments/edit.html.erb -->

<h1>Title: <%= @post.title %></h1>
<h2>Edit Comment ID: <%= @comment.id %></h2>
<%= render partial: 'form', locals: { post: @post, comment: @comment, path: post_comment_path(@post, @comment) } %>
<!-- app/views/comments/new.html.erb -->

  <h1>Title: <%= @post.title %></h1>
-
- <%= form_with model: @comment, url: post_comments_path(@post), data: { turbo: false } do |form| %>
-   <% if @comment.errors.any? %>
-     <ul>
-       <% @comment.errors.each do |error| %>
-         <li><%= error.full_message %></li>
-       <% end %>
-     </ul>
-   <% end %>
-   <div>
-     <%= form.label :content %>
-     <%= form.text_field :content %>
-   </div>
-   <%= form.submit %>
- <% end %>
+ <%= render partial: 'form', locals: { post: @post, comment: @comment, path: post_comments_path(@post) } %>

Add edit link to comments index page.

<!-- app/views/comments/index.html.erb -->

<h2>Comments</h2>
<ul>
<% @comments.each do |comment| %>
- <li><%= comment.content %>(<%= comment.created_at %>)</li>
+ <li>
+   <%= comment.content %>(<%= comment.created_at %>)
+   <%= link_to 'Edit', edit_post_comment_path(@post, comment) %>
+ </li>
<% end %>
</ul>

Post Comments Destroy

Setup destroy action to CommentsController.

# app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :set_post
- before_action :set_comment, only: [:edit, :update]
+ before_action :set_comment, only: [:edit, :update, :destroy]
  # ...
+ def destroy
+   @comment.destroy
+   flash[:notice] = 'Comment deleted successfully'
+   redirect_to post_comments_path(@post)
+ end
+
  private
  # ...
end

Finally, add delete button to comments index page.

<!-- app/views/comments/index.html.erb -->

  <!--  ...  -->
  <li>
    <%= comment.content %>(<%= comment.created_at %>)
    <%= link_to 'Edit', edit_post_comment_path(@post, comment) %>
+   <%= button_to 'Delete', post_comment_path(@post, comment), method: :delete %>
  </li>
  <!-- ... -->

Create Categories

Another feature that we will make in our project is the categories. Our post has many categories, and a category has many posts.

Read Carefully:

In an instance of many-to-many relationship, you will have to create a pivot or joint table. For rails, the preferred naming practice is joining the two table names and adding Ship that stands for relationship. In this way, you can interpret it as joint relationship just by reading the table name.

For example, in our case its Post + Category + Ship, then the result would be PostCategoryShip. Now let’s start creating our Category with column name.

 root@0122:/usr/src/app# rails g model Category
      invoke  active_record
      create    db/migrate/xxxxxxxxxxxxxx_create_categories.rb
      create    app/models/category.rb
# db/migrate/xxxxxxxxxxxxxx_create_categories.rb

class CreateCategories < ActiveRecord::Migration[7.0]
  def change
    create_table :categories do |t|
+     t.string :name
      t.timestamps
    end
  end
end

Then create our joint table PostCategoryShip.

 root@0122:/usr/src/app# rails g model PostCategoryShip
      invoke  active_record
      create    db/migrate/xxxxxxxxxxxxxx_create_post_category_ships.rb
      create    app/models/post_category_ship.rb

Add references for post and category.

# db/migrate/xxxxxxxxxxxxxx_create_post_category_ships.rb

class CreatePostCategoryShips < ActiveRecord::Migration[7.0]
  def change
    create_table :post_category_ships do |t|
+     t.references :post
+     t.references :category
      t.timestamps
    end
  end
end

Migrate the files.

 root@0122:/usr/src/app# rails db:migrate
== xxxxxxxxxxxxxx CreateCategories: migrating =================================
-- create_table(:categories)
   -> 0.0095s
== xxxxxxxxxxxxxx CreateCategories: migrated (0.0096s) ========================
== xxxxxxxxxxxxxx CreatePostCategoryShips: migrating ==========================
-- create_table(:post_category_ships)
   -> 0.0140s
== xxxxxxxxxxxxxx CreatePostCategoryShips: migrated (0.0141s) =================

Set up the associations and category name validation.

# app/models/category.rb

class Category < ApplicationRecord
+ validates :name, presence: true
+
+ has_many :post_category_ships
+ has_many :posts, through: :post_category_ships
end
# app/models/post.rb

class Post < ApplicationRecord
  validates :title, presence: true
  validates :content, presence: true

  has_many :comments
+ has_many :post_category_ships
+ has_many :categories, through: :post_category_ships
end
# app/models/post_category_ship.rb

class PostCategoryShip < ApplicationRecord
+ belongs_to :post
+ belongs_to :category
end

Categories CRUD

Make the CRUD for Category. First generate controller for category.

 root@0122:/usr/src/app# rails g controller categories index new edit --skip-routes
      create  app/controllers/categories_controller.rb
      invoke  erb
      create    app/views/categories
      create    app/views/categories/index.html.erb
      create    app/views/categories/new.html.erb
      create    app/views/categories/edit.html.erb
      invoke  helper
      create    app/helpers/categories_helper.rb

Add route for category.

# config/routes.rb

Rails.application.routes.draw do
  root 'welcome#index'

  resources :posts do
    resources :comments, except: :show
  end
+ resources :categories, except: :show
end

Setup CategoriesController.

# app/controllers/categories_controller.rb

class CategoriesController < ApplicationController
  before_action :set_category, only: [:edit, :update, :destroy]

  def index
    @categories = Category.all
  end

  def new
    @category = Category.new
  end

  def create
    @category = Category.new(category_params)
    if @category.save
      flash[:notice] = 'Category created successfully'
      redirect_to categories_path
    else
      render :new
    end
  end

  def edit; end

  def update
    if @category.update(category_params)
      flash[:notice] = 'Category updated successfully'
      redirect_to categories_path
    else
      render :edit
    end
  end

  def destroy
    @category.destroy
    flash[:notice] = 'Category deleted successfully'
    redirect_to categories_path
  end

  private

  def set_category
    @category = Category.find(params[:id])
  end

  def category_params
    params.require(:category).permit(:name)
  end
end

Create _form.html.erb and edit the generated index, new and edit category views.

<!-- app/views/categories/_form.html.erb -->

<%= form_with model: category, url: path, data: { turbo: false } do |form| %>
  <%= form.label :name %>
  <%= form.text_field :name %>
  <%= form.submit %>
<% end %>
<!-- app/views/categories/index.html.erb -->

- <h1>Categories#index</h1>
- <p>Find me in app/views/categories/index.html.erb</p>
+ <h1>Category</h1>
+
+ <ul>
+   <% @categories.each do |category| %>
+     <li>
+       <%= category.name %>
+       <%= link_to :edit, edit_category_path(category) %>
+       <%= button_to :delete, category_path(category), method: :delete %>
+     </li>
+   <% end %>
+ </ul>
+ 
+ <%= link_to 'new', new_category_path %>
<!-- app/views/categories/edit.html.erb -->

- <h1>Categories#edit</h1>
- <p>Find me in app/views/categories/edit.html.erb</p>
+ <h1>category id: <%= @category.id %></h1>
+ <%= render partial: 'form', locals: { category: @category, path: category_path(@category) } %>
<!-- app/views/categories/new.html.erb -->

- <h1>Categories#new</h1>
- <p>Find me in app/views/categories/new.html.erb</p>
+ <h1>New category</h1>
+ <%= render partial: 'form', locals: { category: @category, path: categories_path } %>

Category For Post

In this section, we will add a feature wherein you can select a category for a post.

Let’s add a checkbox to our posts form.

<!-- app/views/posts/_form.html.erb -->

<%= form_with model: post do |form| %>
  <!-- ... -->
+ <div>
+    <%= form.collection_check_boxes :category_ids, Category.all, :id, :name %>
+ </div>
  <%= form.submit %>
<% end %>

And permit the category parameter to our PostsController.

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
   # ...
   private
   # ...
   def post_params
-    params.require(:post).permit(:title, :content)
+    params.require(:post).permit(:title, :content, category_ids: [])
   end
end

Display post categories in posts index page.

<!-- app/views/posts/index.html.erb -->

<h1>Post List</h1>

<table>
  <thead>
  <td>title</td>
  <td>content</td>
+ <td>category</td>
  <td>action</td>
  </thead>
  <% @posts.each do |post| %>
    <tr>
      <td><%= post.title %></td>
      <td><%= post.content %></td>
+     <td><%= post.categories.pluck(:name).join(',') %></td>
      <!-- ... -->
    </tr>
  <% end %>
</table>

<%= link_to 'New', new_post_path %>

If we leave things as it is, the post.categories will have an additional query or N+1.

Read Carefully:

The N+1 query problem occurs when a certain method executes N additional SQL statements to retrieve the same data as the primary SQL query. When executing a large number of additional queries, that takes time and slows down response time.

To prevent the N+1 problem, we can query (eager load) all the related categories to post by using includes method to our posts index query.

# app/controllers/posts_controller.rb

class PostsController < ApplicationController 
   before_action :set_post, only: [:show, :edit, :update, :destroy]

   def index
-     @posts = Post.all
+     @posts = Post.includes(:categories).all
   end
   # ...
end

Soft Kill

Read the article Soft Kill to find out how to make it.

dependent

Option dependent controls what happens to the associated objects when their owner is destroyed. We will use this option to our post_category_ships association in category. Rails includes a few method dependent options that allow us to further customize our apps namely: destroy, delete_all, nullify, restrict_with_error, and restrict_with_exception.

Why use dependent?

When a parent model instance is deleted, its related records or children still remains. This will cause several bugs whenever those related records were called because its foreign key attempted to load an associated record that no longer existed.

Reminder:

We will be using sandbox for this test. Type rails console --sandbox to enter the sandbox mode.

destroy

All associated objects are destroyed when you use argument :destroy.

# app/models/category.rb

class Category < ApplicationRecord
  validates :name, presence: true

- has_many :post_category_ships
+ has_many :post_category_ships, dependent: :destroy
  has_many :posts, through: :post_category_ships
end
irb(main):001:0> post = Post.new(title: 'Lorem Destroy', content: 'a content for destroy')
irb(main):002:0> post.categories.build(name: 'Testing Destroy')
irb(main):003:0> post.save
irb(main):004:0> category = post.categories.last
irb(main):005:0> category.destroy
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  PostCategoryShip Load (0.5ms)  SELECT `post_category_ships`.* FROM `post_category_ships` WHERE `post_category_ships`.`category_id` = 7
  PostCategoryShip Destroy (0.3ms)  DELETE FROM `post_category_ships` WHERE `post_category_ships`.`id` = 6
  Category Destroy (0.2ms)  DELETE FROM `categories` WHERE `categories`.`id` = 7
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1             
=> #<Category:0x00007f485c9cfe90 id: 7, name: "Testing Destroy", ... >

delete_all

All associated objects from the database (so callbacks will not execute) when you use argument :delete_all.

# app/models/category.rb

class Category < ApplicationRecord
  validates :name, presence: true

- has_many :post_category_ships, dependent: :destroy
+ has_many :post_category_ships, dependent: :delete_all
  has_many :posts, through: :post_category_ships
end
irb(main):001:0> post = Post.new(title: 'Lorem Delete All', content: 'a content for delete all')
irb(main):002:0> post.categories.build(name: 'Testing Delete All')
irb(main):003:0> post.save
irb(main):004:0> category = post.categories.last
irb(main):005:0> category.destroy
  TRANSACTION (0.3ms)  SAVEPOINT active_record_1
  PostCategoryShip Delete All (0.4ms)  DELETE FROM `post_category_ships` WHERE `post_category_ships`.`category_id` = 8
  Category Destroy (0.9ms)  DELETE FROM `categories` WHERE `categories`.`id` = 8
  TRANSACTION (0.3ms)  RELEASE SAVEPOINT active_record_1
=> #<Category:0x0000563bb643de18 id: 8, name: "Testing Delete All", ... >

nullify

The foreign key is set to ‘Null’ when you use argument :nullify. This is also the case for polymorphic associations. Using this will not trigger callbacks.

# app/models/category.rb

class Category < ApplicationRecord
  validates :name, presence: true

- has_many :post_category_ships, dependent: :delete_all
+ has_many :post_category_ships, dependent: :nullify
  has_many :posts, through: :post_category_ships
end
irb(main):001:0> post = Post.new(title: 'Lorem Nullify', content: 'a content for nullify')
irb(main):002:0> post.categories.build(name: 'Testing Nullify')
irb(main):003:0> post.save
irb(main):004:0> category = post.categories.last
irb(main):005:0> category.destroy
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  PostCategoryShip Update All (0.3ms)  UPDATE `post_category_ships` SET `post_category_ships`.`category_id` = NULL WHERE `post_category_ships`.`category_id` = 10
  Category Destroy (0.7ms)  DELETE FROM `categories` WHERE `categories`.`id` = 10
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1
=> #<Category:0x000055565595c558 id: 10, name: "Testing Nullify", created_at: Tue, ... >
irb(main):006:0> PostCategoryShip.last
=> #<PostCategoryShip:0x0000555656c458b8 id: 9, post_id: 46, category_id: nil, ... >

restrict_with_error

If there are any associated objects, :restrict_with_error adds an error to the parent record.

# app/models/category.rb

class Category < ApplicationRecord
  validates :name, presence: true

- has_many :post_category_ships, dependent: :nullify
+ has_many :post_category_ships, dependent: :restrict_with_error
  has_many :posts, through: :post_category_ships
end
irb(main):001:0> post = Post.new(title: 'Lorem Error', content: 'a content for error')
irb(main):002:0> post.categories.build(name: 'Testing Error')
irb(main):003:0> post.save
irb(main):004:0> category = post.categories.last
irb(main):005:0> category.destroy
  TRANSACTION (0.3ms)  SAVEPOINT active_record_1
  PostCategoryShip Exists? (0.4ms)  SELECT 1 AS one FROM `post_category_ships` WHERE `post_category_ships`.`category_id` = 11 LIMIT 1
  TRANSACTION (0.3ms)  ROLLBACK TO SAVEPOINT active_record_1
=> false
irb(main):006:0> category.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=restrict_dependent_destroy.has_many, options={:record=>"post category ships"}>]>

Our destroy action doesn’t have an alert if destroy fails since we don’t have a validation before because we were expecting the destroy to always succeed. So in our current code if @category.destroy fails, the users will still get a flash telling them that ‘Category is destroyed successfully’ even though it’s not the case.

  def destroy
    @post.destroy
    flash[:notice] = 'Category destroyed successfully'
    redirect_to posts_path
  end

But now, since we are using dependent: :restrict_with_error, we are expecting the destroy to fail if category owns a post. Thus, we should add a flash if destroy fails.

  def destroy
    if @category.destroy
      flash[:notice] = 'Category destroyed successfully'
    else
      flash[:alert] = @category.errors.full_messages.join(', ')
    end
    redirect_to posts_path
  end

Notice that we put @category.errors.full_messages.join(', ') as the flash message. We did this to get the errors from the model, :restrict_with_error has a default error message that we can access through errors method if it triggered an error.

restrict_with_exception

If there are any associated records, an exception will be raised with argument :restrict_with_exception. This is the option that we will be using here on our association.

# app/models/category.rb

class Category < ApplicationRecord
  validates :name, presence: true

- has_many :post_category_ships, dependent: :restrict_with_error
+ has_many :post_category_ships, dependent: :restrict_with_exception
  has_many :posts, through: :post_category_ships
end
irb(main):001:0> post = Post.new(title: 'Lorem Exception', content: 'a content for exception')
irb(main):002:0> post.categories.build(name: 'Testing Exception')
irb(main):003:0> post.save
irb(main):004:0> category = post.categories.last
irb(main):005:0> category.destroy
  Category Load (0.4ms)  SELECT `categories`.* FROM `categories` INNER JOIN `post_category_ships` ON `categories`.`id` = `post_category_ships`.`category_id` WHERE `post_category_ships`.`post_id` = 49 ORDER BY `categories`.`id` DESC LIMIT 1               
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1        
  PostCategoryShip Exists? (0.3ms)  SELECT 1 AS one FROM `post_category_ships` WHERE `post_category_ships`.`category_id` = 12 LIMIT 1
  TRANSACTION (0.1ms)  ROLLBACK TO SAVEPOINT active_record_1
/usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/associations/has_many_association.rb:16:in `handle_dependency': Cannot delete record because of dependent post_category_ships (ActiveRecord::DeleteRestrictionError)

We are now done with Active Record Associations.


Back to top

Copyright © 2020-2022 Secure Smarter Service, Inc. This site is powered by KodaCamp.