Rails 7 enables scoping to apply to all queries


Rails 7 adds ability to pass all_queries: true option to the ActiveRecord #scoping method.

The passed option will create a global scope that gets applied to all queries for the duration of the block.

Before

Let’s say we have an Article model which belongs to the User.

Scoping method only worked on class queries (Article.create, Article.all, Article.update_all) and not on class objects (article.update or article.delete).

We can check this in the below example:

article = Article.find(1)
Article.where(user_id: article.user_id).scoping do
  Article.update_all(title: "hello world")
  article.update(title: "Rails 7")
end

# UPDATE "articles" SET "title" = ? WHERE "articles"."user_id" = ?  [["title", "hello world"], ["user_id", 1]]

# UPDATE "articles" SET "title" = ?, "updated_at" = ? WHERE "articles"."id" = ?  [["title", "Rails 7"], ["updated_at", "2021-04-07 11:19:07.187712"], ["id", 1]]

In the above SQL query, we can see that scope articles.user_id is being applied to the update_all query. But in case of article update, this scope is missing.

After

To fix the above issue, Rails 7 allows us to pass all_queries: true to the scoping method.

article = Article.find(1)
Article.where(user_id: article.user_id).scoping(all_queries: true) do
  article.update(title: "Rails 7")
end
# UPDATE "articles" SET "title" = ?, "updated_at" = ? WHERE "articles"."id" = ? AND "articles"."user_id" = ?  [["title", "Rails 7"], ["updated_at", "2021-04-07 11:28:18.188673"], ["id", 1], ["user_id", 1]]

We can see that scope articles.user_id is now applied to the update query inside the scoping block.

Limitations

Scoping only applies to the objects of the same class

Let’s take an example to understand it better.

Suppose we have a Category model which belongs to Article.

Article.where(user_id: 1).scoping(all_queries: true) do
  article.categories.update_all(title: "Rails 7")
end

# UPDATE "categories" SET "title" = ?, "updated_at" = ? WHERE "categories"."id" = ?  [["title", "Rails 7"], ["updated_at", "2021-04-06 07:25:46.996236"], ["id", 1]]

In the above example scope, articles.user_id = 1 is missing in the generated SQL query. This is because the scope is applied to the Article model and not on Category.

If a block is scoped to all_queries, it cannot be unscoped inside the same block

When we try to pass option all_queries: false, inside a block that already has all_queries: true, it will throw an ArgumentError.

Article.where(user_id: 1).scoping(all_queries: true) do
  Article.first
  Article.where(user_id: 1).scoping(all_queries: false) { }
end
# ArgumentError (Scoping is set to apply to all queries and cannot be unset in a nested block.)