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.
0 comments:
Post a Comment