Monday, April 16, 2018

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

Leave a Comment

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.

1 Answers

Answers 1

The eager loading API is designed for collections to prevent N+1 queries.

Loading an association of a singular association of a single record is not an N+1 case.

Personally, I don't think saving 1 query is worth the extra cognitive overhead to the programmer in this particular case. Eager loading is more worthwhile if you are working with an array of instantiated records (e.g. users) or a collection association (e.g. children).

If you really 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 the last approach you listed at the top of your question.

However, I think your :child_with_toy approach is cleaner.

To be clear: This is well off the beaten path of typical ActiveRecord usage, and I'm having difficulty imagining a scenario where I would recommend doing this.

It's up to you --- just weigh the costs.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment