Friday, February 4, 2011

Can Ruby convert an acts_as_nested_set to a JSON hash cleanly without recursion?

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
  • 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 ;-)

    hoyhoy : Good answer, but I was looking for more of a iterative or fancy Ruby way to do this.
    From Mike Stone
  • JSONifier! node.to_json(:include=>{:products=>{:include=>:product_parts}})

    From standup75

0 comments:

Post a Comment