4

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
JoshDoody
  • 417
  • 1
  • 4
  • 14

2 Answers2

1

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.

JoshDoody
  • 417
  • 1
  • 4
  • 14
  • @SteveRobinson Not yet. I'm using workarounds, but the bottom line is I'm not able to preload unscoped stuff right now. Hopefully they're working on it, but the Github issue hasn't seen much action lately. – JoshDoody Apr 09 '14 at 22:24
0

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.

Jim Garvin
  • 4,876
  • 2
  • 23
  • 18