11

I'm trying to eager load an association from an instantiated object, i.e. instead of reading the associations together with parent object...

User.includes(:characters).first

...defer it until I decide it's really needed and do something like:

u = User.first
# Other stuff ...
u.eager_load(:characters)

In Rails 3 I enhanced ActiveRecord with this method:

def eager_load(*args)
  ActiveRecord::Associations::Preloader.new(self, *args).run
end

And it worked fine. Rails 4 changed this part a bit and I updated the method to:

def eager_load(*args)
  ActiveRecord::Associations::Preloader.new.preload(self, *args)
end

Unfortunately, it now does something weird. Take a look:

2.1.2 :001 > u = User.first
[2015-01-06 23:18:03] DEBUG ActiveRecord::Base :   User Load (0.3ms)  SELECT  `users`.* FROM `users`  ORDER BY `users`.`id` ASC LIMIT 1
 => #<User id: 1, ...> 
2.1.2 :002 > u.eager_load :characters
[2015-01-06 23:18:07] DEBUG ActiveRecord::Base :   Character Load (0.2ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`user_id` IN (1)
[2015-01-06 23:18:07] DEBUG ActiveRecord::Base :   Character Load (0.3ms)  SELECT `characters`.* FROM `characters`
[2015-01-06 23:18:07] DEBUG ActiveRecord::Base :   Character Load (0.2ms)  SELECT `characters`.* FROM `characters`
 => [#<ActiveRecord::Associations::Preloader::HasMany:0x00000007c26d28 @klass=Character(id: integer, ...), @owners=[#<User id: ...], @reflection=#<ActiveRecord::Reflection::HasManyReflection:0x0000000496aa60 @name=:characters, ...(LOTS of stuff here)...] 

Note especially the double SELECT of all records. Is there a way to fix this behaviour or some other method to do what I want?

Kombajn zbożowy
  • 8,755
  • 3
  • 28
  • 60
  • 1
    In your example, why do you not want to read the associations together with the parent object? And why can you just call `u.characters` - will this meet the case of getting the characters only when you need them? – Nona Sep 02 '15 at 00:06
  • This was only to demonstrate the issue. I might want to preload deeper structure (e.g .`characters: { items: :attributes }`) to prevent N+1 queries. – Kombajn zbożowy Sep 02 '15 at 06:55
  • 3
    I can't understand the use case where `includes(characters: { items: :attributes })` isn't sufficient? – omarvelous Sep 21 '15 at 22:31
  • I might only need characters, items and attributes if there is certain condition fulfilled. I still need to load user alone to check for this condition. – Kombajn zbożowy Nov 08 '15 at 21:44
  • I have the same problem on Rails 4.2.6, in what version are you? The correct query happens followed by two loading the entire tables (dropping the WHERE clause). – Leonel Galán Sep 27 '16 at 17:20
  • @Kombajnzbożowy, are you seeing this on server logs or on your console? – Leonel Galán Sep 27 '16 at 17:34
  • All versions of Rails are affected from 4.1 to current edge (5.x). It worked normally in 4.0.13. It only happens in console. – Kombajn zbożowy Sep 27 '16 at 21:17

2 Answers2

1

I had the same problem and tracing back my steps I noticed this could be an artifact of running this code on the console. Every expression gets evaluated, but we are only interested in the side product: If this is the case try:

u.eager_load(:characters); nil

I hope it help, I'm currently working on this, so this is my only observation so far.

Leonel Galán
  • 6,993
  • 2
  • 41
  • 60
  • Incredible. Yes, this helps and unneeded queries are gone. And yes, I checked that in fact in server logs this doesn't happend at all. But I cannot understand what happens here. *What* gets evaluated with and without `; nil`...? – Kombajn zbożowy Sep 27 '16 at 21:15
  • My (educated) guess is that the preloader object (returned when calling `.preload`) is similar to an `ActiveRecord::Relation` (relation from now on). In the console, if you build a relation step by step, the query is executed every time (simply because every statement returns in the console), but in the server you build a relation and only run it when you "request" the results (looping through them, etc). Just to be clear, you don't need the `; nil` on the server, just the console. – Leonel Galán Sep 29 '16 at 15:18
  • This does not work on Rails 5 apparently. Is there any alternative? – Leticia Esperon Sep 11 '19 at 00:53
  • I don't, but if the method is not there anymore, you should track (in Rails's Github) when it was deleted. I'm sure you could find an alternative that way. It will help if you tell us exactly what doesn't work? Are you getting an error? What error? – Leonel Galán Sep 11 '19 at 14:28
  • If your setup is too different, you might want to consider posting your own question. – Leonel Galán Sep 11 '19 at 14:33
0

To prevend double queries, you should actually eager load everything that you think you'll use. So, you should nest your include command like this example on the documentation:

users = User.includes(:address, friends: [:address, :followers])

You also can add even deeper relations like:

users = User.includes(:address, {friends: [:address, :followers]})
Hamdan
  • 183
  • 15
  • This is exactly what I want to avoid - I want to defer loading dependent objects until it turns out I need them. – Kombajn zbożowy Aug 15 '16 at 21:08
  • So, the only thing you need is to call them by the association method: User.first.address for example. – Hamdan Aug 16 '16 at 04:17
  • 1
    @Hamdam, he isn't actually using a single User, but many; in your example, without eager loading N addional queries will happen loading the address – Leonel Galán Sep 27 '16 at 17:22