Question

My question

It seems like Rails 4 is ignoring nested unscoped blocks (whereas they were ok in Rails 3). I've been Googling like crazy and can't find anything indicating a change here. Any ideas how I can get this working in Rails 4?

What I'm doing

I'm using default_scope for multi-tenancy as shown in #388 Multitenancy with Scopes. Some admins will be admin of multiple tenants, and I want to show them aggregated data in a report. I'm doing this by using unscoped blocks. I'm also preloading associated objects because 1) It's more efficient and 2) I need to get all the associated objects in one place so I don't have to keep using unscoped blocks in my views. To preload the associated objects, I'm using nested unscoped blocks.

I had this working in my app on Rails 3.2.17, but now that I've upgraded to Rails 4.0.1.rc1, it no longer works.

A simple example to illustrate the difference

Below, I'll show what I'm getting in the Console. This example is much simpler than what I actually want to do, but I think it's the easiest way to show what's going on.

Loading development environment (Rails 3.2.17)
1.9.3-p374 :001 > s = nil
   => nil 
1.9.3-p374 :002 > Submission.unscoped { Checklist.unscoped { s = Submission.preload(:checklist).find(3269) }}
  Submission Load (27.8ms)  SELECT "submissions".* FROM "submissions" WHERE "submissions"."id" = $1 LIMIT 1  [["id", 3269]]
  Checklist Load (0.6ms)  SELECT "checklists".* FROM "checklists" WHERE "checklists"."id" IN (17)
 => #<Submission id: 3269, user_id: nil, workday_id: 17, checklist_id: 17, note: nil, submitted: false, submitted_at: nil, created_at: "2014-03-12 01:06:03", updated_at: "2014-03-12 01:06:03", for_date: "2014-03-11", tenant_id: 2, position: 1, department_id: nil, status: "blank"> 
1.9.3-p374 :003 > s
 => #<Submission id: 3269, user_id: nil, workday_id: 17, checklist_id: 17, note: nil, submitted: false, submitted_at: nil, created_at: "2014-03-12 01:06:03", updated_at: "2014-03-12 01:06:03", for_date: "2014-03-11", tenant_id: 2, position: 1, department_id: nil, status: "blank"> 
1.9.3-p374 :004 > s.checklist
 => #<Checklist id: 17, name: "Open", description: "Chores Required To Open Store Front", creator_id: 2, created_at: "2013-09-23 21:55:23", updated_at: "2013-09-23 21:55:23", archived: false, archived_at: nil, ancestry: nil, tenant_id: 2>

So, I'm loading a submission and preloading its associated checklist. Then I can confirm that both the submission and its associated checklist are available.

If I switch to my Rails 4 environment (both working on the same database), here is what I see:

 Loading development environment (Rails 4.1.0.rc1)
 1.9.3-p374 :001 > s = nil
  => nil 
 1.9.3-p374 :002 > Submission.unscoped { Checklist.unscoped { s = Submission.preload(:checklist).find(3269) }}
   Submission Load (0.4ms)  SELECT  "submissions".* FROM "submissions"  WHERE "submissions"."id" = $1 LIMIT 1  [["id", 3269]]
   Checklist Load (0.6ms)  SELECT "checklists".* FROM "checklists"  WHERE "checklists"."tenant_id" IS NULL AND "checklists"."id" IN (17)
  => #<Submission id: 3269, user_id: nil, workday_id: 17, checklist_id: 17, note: nil, submitted: false, submitted_at: nil, created_at: "2014-03-12 01:06:03", updated_at: "2014-03-12 01:06:03", for_date: "2014-03-11", tenant_id: 2, position: 1, department_id: nil, status: "blank"> 
 1.9.3-p374 :003 > s
  => #<Submission id: 3269, user_id: nil, workday_id: 17, checklist_id: 17, note: nil, submitted: false, submitted_at: nil, created_at: "2014-03-12 01:06:03", updated_at: "2014-03-12 01:06:03", for_date: "2014-03-11", tenant_id: 2, position: 1, department_id: nil, status: "blank"> 
 1.9.3-p374 :004 > s.checklist
  => nil

I'm running exactly the same code, but only the Submission is available - its associated checklist is nil.

FWIW, I can duplicate this in Rails 3.2.17 if I run this in the Console. NOTE: The difference here is I'm only using a Submission.unscoped {} block and not nesting the Checklist.unscoped {} block.

  Loading development environment (Rails 3.2.17)
  1.9.3-p374 :001 > s = nil
   => nil 
  1.9.3-p374 :002 > Submission.unscoped { s = Submission.preload(:checklist).find(3269) }
    Submission Load (3.6ms)  SELECT "submissions".* FROM "submissions" WHERE "submissions"."id" = $1 LIMIT 1  [["id", 3269]]
    Checklist Load (0.4ms)  SELECT "checklists".* FROM "checklists" WHERE "checklists"."tenant_id" IS NULL AND "checklists"."id" IN (17)
   => #<Submission id: 3269, user_id: nil, workday_id: 17, checklist_id: 17, note: nil, submitted: false, submitted_at: nil, created_at: "2014-03-12 01:06:03", updated_at: "2014-03-12 01:06:03", for_date: "2014-03-11", tenant_id: 2, position: 1, department_id: nil, status: "blank"> 
  1.9.3-p374 :003 > s
   => #<Submission id: 3269, user_id: nil, workday_id: 17, checklist_id: 17, note: nil, submitted: false, submitted_at: nil, created_at: "2014-03-12 01:06:03", updated_at: "2014-03-12 01:06:03", for_date: "2014-03-11", tenant_id: 2, position: 1, department_id: nil, status: "blank"> 
  1.9.3-p374 :004 > s.checklist
   => nil

UPDATE

I went digging into the Rails source and I can see that the "unscoped" method is the same in 3.2-stable as current master, BUT that method calls "relation.scoping" and that method is substantially different between the two branches. I'm not sure if there's been a bug introduced, or if the expected behavior has changed.

For reference:

current master

def unscoped
  block_given? ? relation.scoping { yield } : relation
end

relation.scoping

def scoping
  previous, klass.current_scope = klass.current_scope, self
  yield
ensure
  klass.current_scope = previous
end

current_scope

def current_scope #:nodoc:
  ScopeRegistry.value_for(:current_scope, base_class.to_s)
end

3.2-stable

def unscoped #:nodoc:
  block_given? ? relation.scoping { yield } : relation
end

relation.scoping

def scoping
  @klass.with_scope(self, :overwrite) { yield }
end

Link to with_scope method...

def with_scope
  # Pretty long and involved method
  # See Line 60 in the linked doc
end
Was it helpful?

Solution

It looks like I've stumbled into a Rails 4 bug. As far as I can tell, using unscoped and eager loading together is currently not possible in Rails 4 as it seems that 'unscoped' essentially doesn't nest or chain.

Here's the issue on Github:

Using Includes and Unscoped #11036

What I ended up doing (for now) is to create unscoped associations (now available in Rails 4) where I needed them. Here's an example (from the gist I added to the Github issue):

class Submission < ActiveRecord::Base
  default_scope { where(tenant_id: Tenant.current_id) }

  belongs_to :checklist
  ###### This unscoped association is my workaround in Rails 4 
  ###### You'll want comment out if testing in Rails 3
  belongs_to :unscoped_checklist, -> { unscope(where: :tenant_id) }, foreign_key: :checklist_id, class_name: "Checklist"
  belongs_to :tenant
end

So, I can just call submission.unscoped_checklist to get around the default_scope. Note, however, that this will not work in conjunction with eager loading. For example, this will not work:

Submission.unscoped.preload(:unscoped_checklist).where(id: 1)

The Submission will be returned, but subsequently calling submission.unscoped_checklist will return nil.

So I'm currently not able to eager load unscoped associations, but I have figured out how to get to the unscoped associations when I need them.

OTHER TIPS

I ran into the same issue on a Spree-based Rails project, and ran through a bunch of failed approaches before implementing this crutch:

https://gist.github.com/coderifous/33e24f7e63800e169b03a16eb7eebb5b

I preferred this to other approaches because it doesn't involved monkey-patching, or any other clever tricks. It simply clears (and later restores) the default_scopes array.

In my project, dropped that file in lib/default_scope_crutch.rb, and then wrapped my code with something like this:

disable_default_scopes(Product, User) {
  @orders = Order.preload(:product, :user).page(1).per(10)
  @orders.load
}

I would much prefer an ActiveRecord-provided way of doing this, but I'm not aware of one. Hopefully this can be useful for others that run into this issue.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top