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 initializationAs 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 AssociationsConsider 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
.
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.