Why is using the rails default_scope often recommend against?
Asked Answered
C

5

152

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?

Conklin answered 1/8, 2014 at 19:26 Comment(0)
C
218

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.

Conklin answered 1/8, 2014 at 19:26 Comment(17)
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.Yellow
To me it seems that problem is with unscoped instead of default_scope in problem #2Selfwinding
@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.Conklin
The problem is not with default_scope. The problem is with your use case. It is not a good candidate for default_scope.Woolgrower
@BSeven I came across this myself and got recommended the to use default_scope. What would be a good candidate for default_scope?Conklin
When (almost) always you want to use it. A common use case is "deleted" or "inactive" records.Woolgrower
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.Woolgrower
I guess a good use of default_scope is when you want something to be sorted: default_scope { order(:name) }.Kazim
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 methodBroadloom
the problem in unscoped is how you use it, User.first.posts.unscoped and User.unscoped.first.posts is a different queryCakewalk
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.Berry
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.Ketti
Whooo! This is exactly what I want & need :>Alliteration
is this ok for default scoop? default_scope { order(name: :asc) }Eichelberger
@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.Conklin
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)Canada
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 queriesCerebrovascular
S
21

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.

Swainson answered 4/7, 2018 at 7:39 Comment(0)
F
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.

Foretoken answered 16/2, 2017 at 15:38 Comment(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.Neaten
B
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.

Belsen answered 6/3, 2019 at 22:57 Comment(0)
E
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

Eichelberger answered 30/4, 2018 at 6:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.