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
- What is Associations?
- Why Associations?
- Relationships
- Types of Associations
- Create Comments
- Nested Resources
- Create Categories
- Soft Kill
- 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 forrelationship
. In this way, you can interpret it asjoint 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 executesN
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.