views:

780

answers:

2

Is there a fast and clean way of returning a JSON hash back from any node in a Ruby on Rails' acts_as_nested_set without using recursion?

Here's the recursive solution for reference:

class Node < ActiveRecord::Base
  has_many :products
  def json_hash
    if children.size > 0
      children.collect { |node| { node.name => node.json_hash }.to_json
    else
      { node.name => node.products.find(:all).collect(&:name) }.to_json
    end
  end
end
+1  A: 

There is a wikipedia article on tree traversal which shows different alternatives to the recursive solution you are using. It may be tricky to use them in your specific case, but it should be possible.

However, my question to you is, is there a specific reason you want to use iteration instead of recursion? I don't think any of the iterative solutions will be nearly as clean. Are your trees so big that you are running out of stack space (they would have to be pretty big)? Otherwise, I'm not so sure an iterative solution will really be faster.

I see one potential for improvement though, if you are seeing performance issues... but I don't know rails, so I'm not sure if it is accurate:

Does the find method return a new array? If so, you probably want to invoke .collect! instead of .collect, because if find creates an array, you are just creating an array and then throwing it away to the call to collect (which also creates an array), which surely is not going to be very efficient and may slow you down a lot if you have a big tree there.

So

{ node.name => node.products.find(:all).collect(&:name) }.to_json

might become

{ node.name => node.products.find(:all).collect!(&:name) }.to_json

EDIT: Also, it may be more efficient to create your hash of hashes, and then convert the whole thing to json in 1 fell swoop, rather than converting it piecemail like you are doing.

So

class Node < ActiveRecord::Base
  has_many :products
  def json_hash
    if children.size > 0
      children.collect { |node| { node.name => node.json_hash }.to_json
    else
      { node.name => node.products.find(:all).collect!(&:name) }.to_json
    end
  end
end

might become

class Node < ActiveRecord::Base
  has_many :products
  def json_hash
    to_hash.to_json
  end

  def to_hash
    if children.size > 0
      children.collect { |node| { node.name => node.to_hash }
    else
      { node.name => node.products.find(:all).collect!(&:name) }
    end
  end
end

Whether this works and is more efficient I leave as an exercise for you ;-)

Mike Stone
Good answer, but I was looking for more of a iterative or fancy Ruby way to do this.
hoyhoy
A: 

JSONifier! node.to_json(:include=>{:products=>{:include=>:product_parts}})

standup75