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) }
endLoading 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 transactionThe 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 transactionScoping 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
truecannot be unset in the nested block
Let's say, we have a global scope withall_queriesset totrueand now we add another nested scope, but the object queries inside this nested block should not apply the scope. For this, we passall_queries: falseto 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 > endBut if we add nested scoping with
all_queries: false, anArgumentErrorwill 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 thecommentsmade byauthor_id: 1on 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 becausescopingonly applies the scope to that particular model, in our case thePostmodel.
Hope this article helped you understand the ActiveRecord scoping method and its uses.
Thank you for reading! ❤️