Scope
This page will help you how to use scope in your rails application.
Table of Contents
Why we need to use scope?
Scope may create custom queries that improve code better and clear, while also making your application easier to maintain and DRY (Dont Repeat Yourself).
The scope has two arguments the scope name
and -> {...}
. The scope name
is used to call the scope in your code, and -> {...}
is to implement the query.
Its look like this, our example here is post model.
# app/models/post.rb
class Post < ApplicationRecord
# ...
+ scope :recent, -> { order(created_at: :desc) }
+ scope :today, -> { where('created_at >= ?', Time.current.beginning_of_day) }
# ...
end
After calling the scopes, you will get an ActiveRecord:Relation
object. Which mean you can combine and chain scopes.
-> {...}
is a ruby syntax that equivalent tolambda{...}
orProc.new{...}
used to create a method object. TheActiveRecord:Relation
it represents in a specific row in your database and also allow you to to combine and chain queries together.
How to use scope?
Now let’s used the created scope in posts
controller under index action.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
def index
@posts = Post.includes(:user, :region, :province, :moods).page(params[:page]).per(5)
+ @today_posts = Post.today.recent
end
# ..
end
This @today_posts
is an instance variable , that may be used to share the data in the controller to views.
Lets adjust the index
under app/views/posts
.
<!-- app/views/posts/index.html.erb -->
<!-- ... -->
<%= paginate @posts %>
+ <hr>
+ <h1>Today Posts</h1>
+ <table>
+ <thead>
+ <td><%= Post.human_attribute_name(:title) %></td>
+ <td><%= Post.human_attribute_name(:content) %></td>
+ <td>created at</td>
+ </thead>
+ <% @today_posts.each do |post| %>
+ <tr>
+ <td><%= post.title %></td>
+ <td><%= post.content %></td>
+ <td><%= post.created_at.to_fs %></td>
+ </tr>
+ <% end %>
+ </table> <!--End table of today posts-->
<!-- ... -->
In this table we expect the result is only today, and sort by descending order.
We can also chain after the has_many
association. Example is the User
and Post
. Let’s say we already have users and posts, and we choose the last user, and select all the posts of the user with scope. Lets try in irb
.
root@0122:/usr/src/app# rails console
Loading development environment (Rails 7.0.4)
irb(main):001:0> User.last.posts.recent.today
User Load (0.4ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
Post Load (0.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`deleted_at` IS NULL AND `posts`.`user_id` = 112 AND (created_at >= 'xxxx-xx-xx xx:xx:xx.xxxxxx') ORDER BY `posts`.`created_at` DESC
=>
[#<Post:0x000055f4bc903f38
id: 138,
title: "title 2",
content: "content 2",
created_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
updated_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
deleted_at: nil,
user_id: 112,
comments_count: nil,
image: nil,
address: "",
address_region_id: 2,
address_province_id: 7>,
# ...
]
Lets try in without scope.
root@0122:/usr/src/app# rails console
Loading development environment (Rails 7.0.4)
irb(main):001:0> User.last.posts.order(created_at: :desc).where('created_at > ?', Time.current.beginning_of_day)
User Load (0.4ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
Post Load (0.4ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`deleted_at` IS NULL AND `posts`.`user_id` = 112 AND (created_at > 'xxxx-xx-xx xx:xx:xx.xxxxxx') ORDER BY `posts`.`created_at` DESC
=>
[#<Post:0x000055f4ba45ca18
id: 138,
title: "title 2",
content: "content 2",
created_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
updated_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
deleted_at: nil,
user_id: 112,
comments_count: nil,
image: nil,
address: "",
address_region_id: 2,
address_province_id: 7>,
# ...
]
So with or without scope there is the same output, but with scope
its more readable, clear and short code.
Scope with parameters
You can try another scope, we will try the scope region
, you can declare what region you need to show.
# app/models/post.rb
class Post < ApplicationRecord
# ...
scope :recent, -> { order(created_at: :desc) }
scope :today, -> { where('created_at >= ?', Time.current.beginning_of_day) }
+ scope :filter_by_region, -> (region_name) { where(region: {name: region_name } }
# ...
end
Let’s say we need to show all the Region V
and sort by descending order.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
def index
@today_posts = Post.today.recent
+ @region_posts = Post.includes(:region).filter_by_region('Region V').recent
end
# ..
end
Lets adjust again the index
under app/views/posts
.
<!-- app/views/posts/index.html.erb -->
<!-- ... -->
</table> <!--End table of today posts-->
<!-- ... -->
+ <hr>
+ <h1>Region V Posts</h1>
+ <table>
+ <thead>
+ <td><%= Post.human_attribute_name(:title) %></td>
+ <td><%= Post.human_attribute_name(:content) %></td>
+ <td>address</td>
+ </thead>
+ <% @region_posts.each do |post| %>
+ <tr>
+ <td><%= post.title %></td>
+ <td><%= post.content %></td>
+ <td><%= "#{post.region&.name} #{post.province&.name} #{post.address}" %></td>
+ </tr>
+ <% end %>
+ </table> <!--End table of region posts-->
<!-- ... -->
In this table we expect the result is all post of Region V, and sort by descending order.
Lets try in irb
.
root@0122:/usr/src/app# rails console
Loading development environment (Rails 7.0.4)
irb(main):001:0> Post.includes(:region).filter_by_region("Region V").recent
SQL (3.1ms) SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`content` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `posts`.`deleted_at` AS t0_r5, `posts`.`user_id` AS t0_r6, `posts`.`comments_count` AS t0_r7, `posts`.`image` AS t0_r8, `posts`.`address` AS t0_r9, `posts`.`address_region_id` AS t0_r10, `posts`.`address_province_id` AS t0_r11, `region`.`id` AS t1_r0, `region`.`code` AS t1_r1, `region`.`name` AS t1_r2, `region`.`created_at` AS t1_r3, `region`.`updated_at` AS t1_r4 FROM `posts` LEFT OUTER JOIN `address_regions` `region` ON `region`.`id` = `posts`.`address_region_id` WHERE `posts`.`deleted_at` IS NULL AND `region`.`name` = 'Region V' ORDER BY `posts`.`created_at` DESC
=>
[#<Post:0x000055f4bc9095f0
id: 122,
title: "Nulla et exercitationem aut.",
content: "Recusandae tenetur porro. Error illum vitae. Et dolor et.",
created_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
updated_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
deleted_at: nil,
user_id: 95,
comments_count: 18,
image: nil,
address: nil,
address_region_id: 6,
address_province_id: 45>,
#<Post:0x000055f4bc7d6368
id: 112,
title: "Dolor autem molestiae ipsum.",
content: "Quos maxime aut. Aliquam nisi sunt. Saepe blanditiis possimus.",
created_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
updated_at: xxx, xx xxx xxxx xx:xx:xx.xxxxxxxxx HKT +00:00,
deleted_at: nil,
user_id: 96,
comments_count: 18,
image: nil,
address: nil,
address_region_id: 6,
address_province_id: 1>,
# ...
]
As you can see, the expected result is all posts that are region is Region V
and sort by descending order.
We recommend to use scope with parameters, because you can see clearly what parameter are you going to display and you can change the parameter depending on what you need to display.