Search code examples
ruby-on-railsactiverecordactivemodel

Restrict eagerly loaded with `where` without another query in Rails


A has many Bs, B has many Cs. C has a property called thing:

class A < ActiveRecord::Base
  has_many :bs
end
class B < ActiveRecord::Base
  belongs_to :a
  has_many :cs
end
class C < ActiveRecord::Base
  belongs_to :b
  attr_accessible :thing
end

I'd like to query for all Bs belonging to an A, and eagerly load Cs that belong to said B:

> a = A.first
  A Load (0.2ms)  SELECT "as".* FROM "as" LIMIT 1
 => #<A id: 1, created_at: "2012-08-21 09:25:18", updated_at: "2012-08-21 09:25:18"> 
> bs = a.bs.includes(:cs)
  B Load (0.2ms)  SELECT "bs".* FROM "bs" WHERE "bs"."a_id" = 1
  C Load (0.1ms)  SELECT "cs".* FROM "cs" WHERE "cs"."b_id" IN (1)
 => [#<B id: 1, a_id: 1, created_at: "2012-08-21 09:25:22", updated_at: "2012-08-21 09:25:22", thing: nil>] 
> 

This works well:

> bs[0]
 => #<B id: 1, a_id: 1, created_at: "2012-08-21 09:25:22", updated_at: "2012-08-21 09:25:22", thing: nil> 
> bs[0].cs
 => [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">] 
> 

—but not in the case where I want to later perform where() searches on the Cs that belong to B instances:

> bs[0].cs.where(:thing => 1)
  C Load (0.2ms)  SELECT "cs".* FROM "cs" WHERE "cs"."b_id" = 1 AND "cs"."thing" = 1
 => [] 
> bs[0].cs.where(:thing => 2)
  C Load (0.2ms)  SELECT "cs".* FROM "cs" WHERE "cs"."b_id" = 1 AND "cs"."thing" = 2
 => [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">] 
> 

Note that queries are re-issued, despite our having the available information.

Of course, I can just use Enumerable#select:

> bs[0].cs.select {|c| c.thing == 2}
 => [#<C id: 1, b_id: 1, thing: 2, created_at: "2012-08-21 09:29:31", updated_at: "2012-08-21 09:29:31">] 
>

This avoids a re-query, but I was sort of hoping Rails could do something similar itself.

The real downside is that I want to use this code where we don't know if the association has been eagerly loaded or not. If it hasn't, then the select method will load all C for B before doing the filter, whereas the where method would produce SQL to get a smaller set of data.

I'm not convinced this matters at all, but if there was something I'm missing about eager loading, I'd love to hear it.


Solution

  • I don't think you're missing anything. I don't believe active record can do anything that smart -- and it would be very difficult to do reliably I think. Like you say, it would have to determine whether you've eager-loaded the association, but it would also have to make a guess as to whether it would be faster to loop through the in-memory collection of Cs (if it's a small collection) or whether it would be faster to go to the database to get all the appropriate Cs in one shot (if it's a very large collection).

    In your case, the best thing might be to just set the default scope to always preload the cs, and maybe even write your own fancy method to get them by thing. Something like this maybe:

    class B < ActiveRecord::Base
      belongs_to :a
      has_many :cs
      default_scope includes(:cs)
    
      def cs_by_thing(thing)
        cs.select{|c|c.thing == thing}
      end
    end
    

    Then you could always know that you never go back to the DB when querying for your cs:

    a = A.first
    [db access]
    a.bs.first
    [db access]
    a.bs.first.cs
    a.bs.first.cs_by_thing(1)
    a.bs.first.cs_by_thing(2)