How to do conditional eager_loading in ActiveRecord?

569 views Asked by At

I have a model Tag, which is a tree model (using closure_tree) and thanks to the gem I can do stuff like:

Tag.includes(:children).first

to eager load its children. Now, I have an instance method, which also uses children. So when I get a tag by the mentioned query, iterate over the tag and its children and call this instance method, the bullet gem complains that I am doing a N+1 queries and advises me to include(children). This happens because I have not loaded the children of the children, so when calling the instance method, I make a separate query for the children of each sub-tag of the first-level tag.

I can solve this by doing the following:

Tag.includes(children: :children).first

In this case ActiveRecord eagerly loads the tag, its children and the children of its children. This, however, works if there are only 3 generations - the tag is a grandparent and then come the parents (which are children of the root tag) and the grandchildren (which are the children of the children of the root tag). If I have, say, 4 generations, than I must do:

Tag.includes(children: {children: :children}).first

So if I can determine the depth of the farthest grand child of a root tag, is there a way to conditionally construct my query - if depth is 2, use includes(:children), if depth is 3 - use includes(children: :children) and so on?

2

There are 2 answers

7
Typpex On

Judging from the documentation at: https://github.com/mceachen/closure_tree#usage you should use

Tag.descendants
Tag.self_and_descendants

This will retrieves all the children, sub-children, etc.. until it ends on a leaf.

3
Alexander Popov On

The best solution I could come up with is this:

  def included_children(tag_id)
    depth = Tag.find(tag_id).leaves.last.depth
    eval(includes_string(depth))
  end

  def includes_string(depth)
    "{children: #{depth > 1 ? includes_string(depth - 1) : ':children'}}"
  end

I first find the depth of the deepest leave. This tells me how many times I must call :children. Then, I construct a string, based on the depth and evaluate the string to code, which I use in the includes method. Any improvement suggestions are welcomed, because this solution has several flaws and I find it totally unclean. Thank you.