Ruby on Rails Rails Best Practices Beware of default_scope

Help us to keep this website almost Ad Free! It takes only 10 seconds of your time:
> Step 1: Go view our video on YouTube: EF Core Bulk Extensions
> Step 2: And Like the video. BONUS: You can also share it!

Example

ActiveRecord includes default_scope, to automatically scope a model by default.

class Post
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

The above code will serve posts which are already published when you perform any query on the model.

Post.all # will only list published posts 

That scope, while innocuous-looking, has multiple hidden side-effect that you may not want.

default_scope and order

Since you declared an order in the default_scope, calling order on Post will be added as additional orders instead of overriding the default.

Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC

This is probably not the behavior you wanted; you can override this by excluding the order from the scope first

Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC

default_scope and model initialization

As with any other ActiveRecord::Relation, default_scope will alter the default state of models initialized from it.

In the above example, Post has where(published: true) set by default, and so new models from Post will also have it set.

Post.new # => <Post published: true>

unscoped

default_scope can nominally be cleared by calling unscoped first, but this also has side-effects. Take, for example, an STI model:

class Post < Document
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

By default, queries against Post will be scoped to type columns containing 'Post'. But unscoped will clear this along with your own default_scope, so if you use unscoped you have to remember to account for this as well.

Post.unscoped.where(type: 'Post').order(updated_at: :desc)

unscoped and Model Associations

Consider a relationship between Post and User

class Post < ApplicationRecord
  belongs_to :user
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

class User < ApplicationRecord
  has_many :posts
end

By getting an individual User, you can see the posts related to it:

user = User.find(1)
user.posts
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]

But you want to clear the default_scope from the posts relation, so you use unscoped

user.posts.unscoped
SELECT "posts".* FROM "posts"

This wipes out the user_id condition as well as the default_scope.

An example use-case for default_scope

Despite all of that, there are situations where using default_scope is justifiable.

Consider a multi-tenant system where multiple subdomains are served from the same application but with isolated data. One way to achieve this isolation is through default_scope. The downsides in other cases become upsides here.

class ApplicationRecord < ActiveRecord::Base
  def self.inherited(subclass)
    super

    return unless subclass.superclass == self
    return unless subclass.column_names.include? 'tenant_id'

    subclass.class_eval do
      default_scope ->{ where(tenant_id: Tenant.current_id) }
    end
  end
end

All you need to do is set Tenant.current_id to something early in the request, and any table that contains tenant_id will automatically become scoped without any additional code. Instantiating records will automatically inherit the tenant id they were created under.

The important thing about this use-case is that the scope is set once per request, and it doesn't change. The only cases you will need unscoped here are special cases like background workers that run outside of a request scope.



Got any Ruby on Rails Question?