Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eager loading an association of a singular association from an instantiated ActiveRecord object in Rails 5

I have a model User, that has_one :child, and the Child model has_one :toy.

If I have a single instance of the User class user, how can I load both the child and toy in one query?

Here's what doesn't work:

user.child.toy # 2 queries
user.includes(child: :toy) # can't call includes on a single record
user.child.includes(:toy) # same as above
user.association(:child).scope.eager_load(:toy).first # makes the appropriate query with both child and toy... but doesn't set the object on the user model.
user.child = user.association(:child).scope.eager_load(:toy).first # makes the appropriate query, but also calls another query before setting the child :(

Is there any way to do this that doesn't involve re-querying the user model. ie. I want to avoid this

User.where(id: user.id).eager_load(child: :toy).first

Relavant model declarations:

class User < ActiveRecord::Base
  has_one :child
  has_one :toy, through: :child
end

class Child < ActiveRecord::Base
  has_one :toy
  belongs_to :user
end

class Toy < ActiveRecord::Base
  belongs_to :child
end

Update

This works, but isn't ideal. I don't think I should have to declare another relation solely for this reason.

class User < ActiveRecord::Base
  has_one :child
  has_one :toy, through: :child
  has_one :child_with_toy, ->{ eager_loads(:toy) }, class_name: "Child", foreign_key: :parent_id
end

which allows me to call user.child_with_toy to get the Child object, and user.child_with_toy.toy to get the Toy object, while only triggering one SQL query.

like image 914
you786 Avatar asked Apr 05 '18 16:04

you786


2 Answers

It can be done easier with Preloader:

ActiveRecord::Associations::Preloader.new.preload(user, child: [:toy])
like image 97
Dmitry Polushkin Avatar answered Oct 04 '22 04:10

Dmitry Polushkin


If you want to eager load an association of a singular association for an already-instantiated record, you can do this:

user.association(:child).target = user.association(:child).scope.eager_load(:toy).first

This is similar to one of the approaches you listed at the top of your question. In particular, notice the .target part.

ActiveRecord isn't optimized for this particular scenario, so the code is pretty ugly. For that reason, I would strongly lean toward your :child_with_toy approach if you really need to save the query.

like image 24
Nathan Avatar answered Oct 04 '22 05:10

Nathan