In Rails, multiple scopes can be created and chained together. What if we wish to apply a specific scope to a group of queries? Consider the following scenario: we have Post and Comment models and we want to perform few operations on public posts.

# app/models/post.rb

class Post < ActiveRecord::Base
  scope :public, -> { where(private: false) }
end
Loading development environment (Rails 7.0.0.alpha2)

3.0.0 :001 > Post.public.update_all(body: 'public post')
  Post Update All (4.1ms)  UPDATE "posts" SET "body" = ? WHERE "posts"."private" = ?  [["body", "public post"], ["private", 0]]

3.0.0 :002 > Post.public.pluck(:id, :title)
  Post Pluck (0.2ms)  SELECT "posts"."id", "posts"."title" FROM "posts" WHERE "posts"."private" = ?  [["private", 0]]

3.0.0 :003 > Post.public.where(author_id: 1)
  Post Load (0.6ms)  SELECT "posts".* FROM "posts" WHERE "posts"."private" = ? AND "posts"."author_id" = ? /* loading for inspect */ LIMIT ?  [["private", 0], ["author_id", 1], ["LIMIT", 11]]

We can either include the public scope in all queries or define a default scope. However, as an alternative, Rails Active Record provides a scoping method that allows us to apply a certain scope to all the queries in a block.

In this article, we will look at how scoping works, its various applications, and what is new w.r.t scoping in Rails 7. Let us rewrite the above example using the scoping block.

Loading development environment (Rails 7.0.0.alpha2)

3.0.0 :001 > post = Post.find 1
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]

3.0.0 :002 > Post.where(private: false).scoping do
3.0.0 :003 >   Post.update_all(body: 'public post')
3.0.0 :004 >   Post.pluck(:id, :title)
3.0.0 :005 >   post.update_attribute(:body, 'Post 1')
3.0.0 :006 > end
  Post Update All (0.3ms)  UPDATE "posts" SET "body" = ? WHERE "posts"."private" = ?  [["body", "public post"], ["private", 0]]
  Post Pluck (0.4ms)  SELECT "posts"."id", "posts"."title" FROM "posts" WHERE "posts"."private" = ?  [["private", 0]]
  TRANSACTION (0.1ms)  begin transaction
  Post Update (0.4ms)  UPDATE "posts" SET "body" = ?, "updated_at" = ? WHERE "posts"."id" = ?  [["body", "Post 1"], ["updated_at", "2021-10-06 05:51:05.023685"], ["id", 1]]
  TRANSACTION (1.7ms)  commit transaction

The private: false scope is applied to the 1st and 2nd query, but not to the 3rd query because scoping only works on class queries and not on object queries.

all_queries in Rails 7

Rails 7 introduces the ability to supply the all_queries option to the scoping method to address the above issue. By specifying all_queries: true, the scope will be applied to all queries (including objects) in a block.

Loading development environment (Rails 7.0.0.alpha2)

3.0.0 :001 > post = Post.find 1
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]

3.0.0 :002 > Post.where(private: false).scoping(all_queries: true) do
3.0.0 :003 >   Post.update_all(body: 'public post')
3.0.0 :004 >   Post.pluck(:id, :title)
3.0.0 :005 >   post.update_attribute(:body, 'Post 1')
3.0.0 :006 > end
  Post Update All (0.2ms)  UPDATE "posts" SET "body" = ? WHERE "posts"."private" = ?  [["body", "public post"], ["private", 0]]
  Post Pluck (0.2ms)  SELECT "posts"."id", "posts"."title" FROM "posts" WHERE "posts"."private" = ?  [["private", 0]]
  TRANSACTION (0.1ms)  begin transaction
  Post Update (0.4ms)  UPDATE "posts" SET "body" = ?, "updated_at" = ? WHERE "posts"."id" = ? AND "posts"."private" = ?  [["body", "Post 1"], ["updated_at", "2021-10-06 05:57:31.190539"], ["id", 1], ["private", 0]]
  TRANSACTION (1.8ms)  commit transaction

Scoping with .or clause

As we know, with the .or method, the two queries must be structurally compatible, which means the queries must be identical except for the where or having clause.

Loading development environment (Rails 7.0.0.alpha2)

3.0.0 :001 > Post.select(:id, :title).where(private: false)
3.0.0 :002 >   .or(Post.select(:id, :title).where(author_id: 1))
  Post Load (0.3ms)  SELECT "posts"."id", "posts"."title" FROM "posts" WHERE ("posts"."private" = ? OR "posts"."author_id" = ?) /* loading for inspect */ LIMIT ?  [["private", 0], ["author_id", 1], ["LIMIT", 11]]

Here we can see, the Post.select(:id, :title) clause is repeated.
The scoping method can be used to DRY our code by applying the same clause to all queries.

Loading development environment (Rails 7.0.0.alpha2)

3.0.0 :001 > Post.select(:id, :title).scoping do
3.0.0 :002 >   Post.where(private: false)
3.0.0 :003 >   .or(Post.where(author_id: 1))
3.0.0 :004 > end
  Post Load (1.3ms)  SELECT "posts"."id", "posts"."title" FROM "posts" WHERE ("posts"."private" = ? OR "posts"."author_id" = ?) /* loading for inspect */ LIMIT ?  [["private", 0], ["author_id", 1], ["LIMIT", 11]]

Limitations

  • all_queries once set to true cannot be unset in the nested block
    Let's say, we have a global scope with all_queries set to true and now we add another nested scope, but the object queries inside this nested block should not apply the scope. For this, we pass all_queries: false to nested scope as below:

    Loading development environment (Rails 7.0.0.alpha2)
    
    3.0.0 :001 > Post.where(title: 'Rails').scoping(all_queries: true) do
    3.0.0 :002 >   # queries on which scope will be applied
    3.0.0 :003 >   ...
    3.0.0 :004 >   Post.where(private: true).scoping(all_queries: false) do
    3.0.0 :005 >     # object queries on which scope should not be applied
    3.0.0 :006 >     post.update_attribute(:private, false)
    3.0.0 :007 >   end
    3.0.0 :008 > end
    

    But if we add nested scoping with all_queries: false, an ArgumentError will be raised.

    ArgumentError (Scoping is set to apply to all queries and cannot be unset in a nested block.)
    
  • Scoping doesn't get applied while querying on the associated table.
    Let's say, we need to fetch all the comments made by author_id: 1 on the public posts.

    Loading development environment (Rails 7.0.0.alpha2)
    
    3.0.0 :001 > Post.where(private: false).scoping(all_queries: true) do
    3.0.0 :002 >   Comment.joins(:post).where(author_id: 1)
    3.0.0 :003 > end
      Comment Load (1.7ms)  SELECT "comments".* FROM "comments" INNER JOIN "posts" ON "posts"."id" = "comments"."post_id" WHERE "comments"."author_id" = ? /* loading for inspect */ LIMIT ?  [["author_id", 1], ["LIMIT", 11]]
    

    Here we may expect the query to fetch all the comments from public posts with author_id: 1. But if we see the resultant query the public scope is missing because scoping only applies the scope to that particular model, in our case the Post model.

Hope this article helped you understand the ActiveRecord scoping method and its uses.

Thank you for reading! ❤️


References