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

Scope

This page will help you how to use scope in your rails application.

Table of Contents

  1. Why we need to use scope?
  2. How to use scope?
  3. Scope with parameters

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 to lambda{...} or Proc.new{...} used to create a method object. The ActiveRecord: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.


Back to top

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