149

Everywhere on the internet people mention that using the rails default_scope is a bad idea, and the top hits for default_scope on stackoverflow are about how to overwrite it. This feels messed up, and merits an explicit question (I think).

So: why is using the rails default_scope recommended against?

wrtsprt
  • 5,319
  • 4
  • 21
  • 30

5 Answers5

212

Problem 1

Lets consider the basic example:

class Post < ActiveRecord::Base
  default_scope { where(published: true) }
end

The motivation to make the default published: true, might be to make sure you have to be explict when wanting to show unpublished (private) posts. So far so good.

2.1.1 :001 > Post.all
  Post Load (0.2ms)  SELECT "posts".* FROM "posts"  WHERE "posts"."published" = 't'

Well this is pretty much what we expect. Now lets try:

2.1.1 :004 > Post.new
 => #<Post id: nil, title: nil, published: true, created_at: nil, updated_at: nil>

And there we have the first big problem with default scope:

=> default_scope will affect your model initialization

In a newly created instance of such a model, the default_scope will be reflected. So while you might have wanted to be sure to not list unpublished posts by chance, you're now creating published ones by default.

Problem 2

Consider a more elaborate example:

class Post < ActiveRecord::Base
  default_scope { where(published: true) }
  belongs_to :user
end 

class User < ActiveRecord::Base
  has_many :posts
end

Lets get the first users posts:

2.1.1 :001 > User.first.posts
  Post Load (0.3ms)  SELECT "posts".* FROM "posts"  WHERE "posts"."published" = 't' AND "posts"."user_id" = ?  [["user_id", 1]]

This looks like expected (make sure to scroll all the way to the right to see the part about the user_id).

Now we want to get the list of all posts - unpublished included - say for the logged in user's view. You'll realise you have to 'overwrite' or 'undo' the effect of default_scope. After a quick google, you'll likely find out about unscoped. See what happens next:

2.1.1 :002 > User.first.posts.unscoped
  Post Load (0.2ms)  SELECT "posts".* FROM "posts"

=> Unscoped removes ALL scopes that might normally apply to your select, including (but not limited to) associations.

There are multiple ways to overwrite the different effects of the default_scope. Getting that right gets complicated very quickly and I would argue not using the default_scope in the first place, would be a safer choice.

Community
  • 1
  • 1
wrtsprt
  • 5,319
  • 4
  • 21
  • 30
  • 2
    To pile on: the only time I found default_scope useful is when you absolutely want to eager load some associations by default. default_scope{eager_load([:category, :comments])} . However!!! If you are doing count query on this model like Product.count, it will eager_load associations for all products. And if you have 50K records, your count query just went from 15ms to 500ms, because while all you want is count, your default_scope will left join everything else. – konung May 12 '15 at 14:41
  • 21
    To me it seems that problem is with `unscoped` instead of `default_scope` in problem #2 – Alma Alma Jan 22 '16 at 18:11
  • 4
    @CaptainFogetti Indeed. I still think it's a good idea to present the drawbacks of unscoped as a possible disadvantage of default_scope. In most non trivial cases using default_scope will lead to you needing to use unscoped. This is a second degree caveat (in lack of a better term), which is easy to miss when researching a method. – wrtsprt Jan 23 '16 at 19:23
  • The problem is not with `default_scope`. The problem is with your use case. It is not a good candidate for `default_scope`. – B Seven Aug 02 '16 at 21:59
  • @BSeven I came across this myself and got recommended the to use `default_scope`. What would be a good candidate for `default_scope`? – wrtsprt Aug 04 '16 at 09:47
  • 2
    When (almost) always you want to use it. A common use case is "deleted" or "inactive" records. – B Seven Aug 04 '16 at 15:12
  • 1
    The problem with the use case in your answer is that there are many cases when you want to find unpublished posts. In fact, I would argue that finding published posts is a special case. The only time you want published posts is when someone is viewing the public page. But there are many times when you want to see unpublished posts. – B Seven Aug 04 '16 at 15:12
  • 7
    I guess a good use of `default_scope` is when you want something to be sorted: `default_scope { order(:name) }`. – user2985898 Aug 25 '16 at 09:33
  • I had experience with ```default_scope { order(:name) }```, I got stuck for few hours because using ```order``` in default_scope, then now I want to use soft_delete feature to my model, but still considering default_scope method – Saiqul Haq Sep 04 '16 at 00:33
  • the problem in unscoped is how you use it, `User.first.posts.unscoped` and `User.unscoped.first.posts` is a different query – endyey Es Feb 07 '17 at 01:31
  • Using default_scope to order means that to change the order you have to either reorder or unscope User.order(:name).to_sql => select * from users order by "users"."other_field", "users"."name" Unless you open up the user.rb file the results would be a surprise. – baash05 Feb 08 '17 at 11:42
  • 1
    Good info here. But not using it means I need to add a scope to dozens of queries, where I only need the query without the scope in 1 or 2 places. Agree with Captain F above as to the major problem being how unscoped works. We need an alternative to Unscoped which acts like one would expect - undo default-scope, and that's all. But being able to create a default-scope that doesn't affect Model.new would also be nice. – JosephK Aug 25 '17 at 11:25
  • Whooo! This is exactly what I want & need :> – heroxav Aug 26 '17 at 20:47
  • is this ok for default scoop? `default_scope { order(name: :asc) }` – Moses Liao GZ Apr 20 '18 at 04:21
  • @MosesLiaoGZ: I think that's one of the safest ways of using it. Only caveat maybe, if you're dealing with huge tables in PostgreSQL apparently ordering gets slow and anything done on that model will get slow unless `unscoped` is used. – wrtsprt Apr 23 '18 at 19:53
  • Instead of using `unscoped` and removing _all_ scopes you can just use the singular `unscope` to remove the `published` scope. E.g. `User.first.posts.unscope(where: :published)` – Robert Nov 05 '20 at 02:07
  • Been bitten horribly by default_scope as well. One good use though is includes if there's a table that is like ridiculously closely connected to the base one. This would harmlessly avoid loads of n+1 queries – saGii Apr 01 '21 at 09:04
20

Another reason to not use default_scope is when you're deleting an instance of a model that has a 1 to many relation with the default_scope model

Consider for example:

    class User < ActiveRecord::Base
      has_many :posts, dependent: :destroy
    end 

    class Post < ActiveRecord::Base
      default_scope { where(published: true) }
      belongs_to :user
    end

Calling user.destroy will delete all the posts that are published, but it won't delete posts that are unpublished. Hence the database will throw a foreign key violation because it contains records that reference the user you want to remove.

Koekenbakker28
  • 241
  • 3
  • 7
10

default_scope is often recommended against because it is sometimes incorrectly used to limit the result set. A good use of default_scope is to order the result set.

I would stay away from using where in default_scope and rather create a scope for that.

nahankid
  • 221
  • 2
  • 8
  • 1
    The second problem "Unscoped removes ALL scopes that might normally apply to your select, including (but not limited to) associations" still exists even if the `default_scope` only contains `order`. This behaviour of `unscoped` is quite unexpected. – Zack Xu Nov 08 '17 at 18:32
3

For me is not a bad idea but must be used with caution!. There is a case where I always wanted to hide certain records when a field is set.

  1. Preferably the default_scope must match with the DB default value (e.g: { where(hidden_id: nil) })
  2. When you are totally sure you want to show those records, there is always the unscoped method that will avoid your default_scope

So it will depend and the real needs.

Sposmen
  • 689
  • 6
  • 7
1

I only find default_scope to be useful only in ordering some parameters to be in asc or desc order in all situation. Otherwise I avoid it like plague

Moses Liao GZ
  • 1,556
  • 5
  • 20
  • 45