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 withall_queries
set totrue
and now we add another nested scope, but the object queries inside this nested block should not apply the scope. For this, we passall_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
, anArgumentError
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 thecomments
made byauthor_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 becausescoping
only applies the scope to that particular model, in our case thePost
model.
Hope this article helped you understand the ActiveRecord scoping
method and its uses.
Thank you for reading! ❤️