rails: Model.unscoped block not removing default scope for joins

Summary:

Suppose Model has a default scope.

Model.unscoped { Foo.joins(:model) }

acts as if Model.unscoped { ... } were not specified. That is, it constructs Foo.joins(:model) as if it weren’t inside Model.unscoped, so that Model.default_scope is mistakenly continuing to apply to the joins, even though unscoped should have removed it.


Consider a User with a default_scope that specifies status = 1:

>> User.all.to_sql
#=> "SELECT `users`.* FROM `users`  WHERE `users`.`status` = 1"

If an Item belongs to User, trying to join the two with Item.joins(:user) seems to always respect User.default_scope, no matter how you try to remove the default scope. So the joins SQL

>> Item.joins(:user).to_sql # includes User.default_scope, CORRECTLY.
#=> "SELECT `items`.* FROM `items` INNER JOIN `users` ON `users`.`id` = `items`.`user_id` AND `users`.`status` = 1

correctly includes

`users`.`status` = 1

as it should, but if you try to remove it with User.unscoped { ... }, the status = 1 default scope on User is still applying, which I believe is a bug:

User.unscoped do
  Item.joins(:user) # User is incorrectly still scoped to status == 1 here
end

As a total aside, it would be nice to have something like joins_unscoped(:user) as an alternative.

About this issue

  • Original URL
  • State: closed
  • Created 10 years ago
  • Reactions: 7
  • Comments: 44 (24 by maintainers)

Commits related to this issue

Most upvoted comments

@miguelgraz I’ve been facing the same issue, and if you don’t write a raw join (which in some cases can be pretty painful), you could go for this which works.

Based on the models:

class User
  default_scope { <some scope> }
end

class Item
  belongs_to :user
end

In order to be able to do a joins on Item to User without being impacted with the default scope, you could do this:

class UnscopedUser < User
  self.default_scopes = []
end

class Item
  belongs_to :user

  # add this association
  belongs_to :unscoped_user, foreign_key: :user_id
end

Then in order to skip the default scope when doing the join you just do:

Item.joins(:unscoped_user)

This is probably the worst bug in active record. It basically makes default scope completely unusable if you have any sort of complexity in your business logic. I wish default_scope came with a warning about this issue.

The only workaround I’ve found is to do the join in raw sql.

@aprescott @miguelgraz @rafaelfranca I just created the following https://github.com/rails/rails/pull/16531 that will allow one to say…

class User
  default_scope { where(status: 1) }
end

class Item
  belongs_to :user
  belongs_to :with_deleted_user, -> { unscope(where: :status) }, class_name: 'User', foreign_key: :user_Id
end

Item.joins(:with_deleted_user).to_sql
#=> "SELECT `items`.* FROM `items` INNER JOIN `users` ON `users`.`id` = `items`.`user_id`

OR

Item.joins(:user).to_sql
#=> "SELECT `items`.* FROM `items` INNER JOIN `users` ON `users`.`id` = `items`.`user_id` AND `users`.`status` = 1

Although, ActiveRecord::Base.unscoped { query_that_should_be_unscoped } still doesn’t work (most likely due to the same reasons I mentioned in the PRs commit), I hope this helps until then.

@miguelgraz I am also facing the same problem while doing join default scope also gets called.

Do you have any solution for this?

I’ve been working on a new feature to potentially resolve this problem, as well as the other issues highlighted in #10643.

I accomplished it by introducing a new “inline scope,” and also standardizing the order in which scopes are prioritized: 1) default, 2) association scope, 3) inline scope.

Say you have a has_many :comments association that applies a scope that hides soft-deletes via a :deleted_at attribute. You can undo the association scope with an inline scope like this:

    Post.includes(:comments => ->{ unscope(where: :deleted_at) })

Nested joins are possible within inline scopes. For the sake of clarity, these two lines are equivalent:

    Comment.joins(:author => :address)
    Comment.joins(:author => -> { joins(:address) })

Code here, still needs to be prepped for a PR but wanted to share first:

https://github.com/sidot3291/rails/commit/1735537aec94b149544bd3d2419d0cd0e395ab9d

If anyone wonder about this issue me and @claptimes5 are already working on it, we managed to write a test and, if we’re doing everything right, the bug still seems to exist on master.

This is our base spec to work on, if you guys want to take a look and/or point something wrong: https://github.com/miguelgraz/rails/commit/8cb2132f7496f10af3f637d7f8342572e299569c