2
votes

I have an organizational hierarchy represented via awesome nested set on a Node model. Great, works great, the updates are expensive but the finds are super efficient.

Each Node model has_many on other models, let's call them Foo and Bar.

class Node < ActiveRecord::Base
  acts_as_nested_set

  has_many :foos
  has_many :bars
end

class Foo < ActiveRecord::Base
  belongs_to :node
end

Frequently, I want to find all of the foos or bars for a given subtree, looking down from a current node. Naively, I could do:

@foos = @node.self_and_descendants.collect(&:foos).compact  

I could even use an ActiveRecord .includes(:foos) to avoid N+1 queries. What I really want is just to ask for @node.all_foos so I implement something like this:

class Node < ActiveRecord::Base
  def all_foos
    Foo.where(node_id: self_and_descendants.pluck(:id))
  end

  # More efficient?
  def all_foos_alternately
    Foo.where(node_id: self_and_descendants.includes(:foos).pluck(:id))
  end
end

But, let's say that I want to “collect” more than just foos and bars, let's say that I have half a dozen or a dozen of this models. Now I'm littering my Node class or some lib with a bunch of all_* methods.

Should I be defining class methods on the foos and bars that accept a node as an argument and return all of the foos/bars for that subtree? But then the foos and bars need to understand/be aware of node.self_and_descendants.

Or the Foo class method could accept a collection of nodes, not needing to be aware of the nested set methods, but then I lose the easy interface via node.all_foos or the like.

What's the pattern or method I'm missing here? I've played with implementing a catch-all for node.all_* via method_missing but don't like the performance hit. What I'm trying to execute here is, at its core, a database lookup and, as such, it should be efficient and elegant.

2

2 Answers

1
votes

Thank you to @adamsanderson for pointing me in the right direction with regards to joins and merge. Merge allows you to filter a joined association via another scope or relation. Brilliant!

def all_foos
  Foo.joins(:node).merge(self_and_descendants)
end

I verified tests on one of my all_* methods in my application, rewrote the method using this joins.merge technique, and reran my tests. All green!

This answer does not address the design issue of how to apply this method across many different has_many relations but I will leave that as an exercise for the reader (me).

See also, with regards to merge: http://blog.mitchcrowe.com/blog/2012/04/14/10-most-underused-activerecord-relation-methods

0
votes

I am going to propose a solution that I don't think is very good in order to attempt to kick off discussion...

class Node < ActiveRecord::Base
  acts_as_nested_set

  has_many :foos
  has_many :tree_foos, :through => :children

  def all_foos
    # Shoot, how do I write this so I still get a relation back?
    foos + tree_foos
    # Nope, how about...
    Foo.where(id: foo_ids + tree_foo_ids)
  end
end

Blech. I don't like that. And I'll still have to repeat this code across all the has_many model associations.