acts_as_treeの使い方(Rails ActiveRecord::Base)

メッセージのやり取りをスレッドで表現したくて、Railsでツリーを扱うacts_as_treeにたどり着いた。が、なんかacts_as_treeに関してはあんまり資料が無いね。。Module: ActiveRecord::Acts::Tree::ClassMethodsにある使い方以外無くて、後はparent_id使うよ〜とか書いてあるとこがあるくらい。

ちょうど下の図みたいなTreeを作りたかったのでいろいろ試した結果、acts_as_treeだけじゃ全然Treeとしてだめだってことが分かったよ。

まずそのままではself.rootとself.rootsはあるけど、@node.rootはできない。このままじゃスレッド表示の各メッセージで、そのルートが取れないじゃないか!!

以下がacts_as_treeのソースだけど、self.rootsとself.rootしか無いので、当然rootがとれない。しかもparentとchildrenはbelongs_toとhas_manyで定義されてるだけだ。

# File /usr/local/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/active_record/acts/tree.rb, line 43
def acts_as_tree(options = {})
  configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
  configuration.update(options) if options.is_a?(Hash)
  
  belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
  has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
  
  class_eval "
    include ActiveRecord::Acts::Tree::InstanceMethods
    def self.roots
      find(:all, :conditions => \"\#{configuration[:foreign_key]} IS NULL\", :order => \#{configuration[:order].nil? ? \"nil\" : %Q{\"\#{configuration[:order]}\"}})
    end
    def self.root
      find(:first, :conditions => \"\#{configuration[:foreign_key]} IS NULL\", :order => \#{configuration[:order].nil? ? \"nil\" : %Q{\"\#{configuration[:order]}\"}})
    end
    "
end

だとすると、acts_as_treeとは別にroot_idというのを各ノードに持たせることで、@node.rootが実現できるじゃないか!!

ということで、実際に簡単なモデルを作ってみた。(あ、あとついでに@node.nodesも)

class Tree < ActiveRecord::Base
  acts_as_tree
  belongs_to :root, :foreign_key => :root_id, :class_name => "Tree"
  
  def is_root?
    !root_id
  end
  
  def nodes
    if self.is_root? 
      # this is root node.
      Tree.find( :all, :conditions =>["root_id=?", id] )
    else
      # this is not root node.
      Tree.find( :all, :conditions =>["root_id=?",  root.id] )
    end
  end
  
  def parent=(parent)
    if parent.is_root?
      self.root = parent
    else
      self.root = parent.root
    end
    super(parent)
  end
  
  def children<<(child)
    if self.is_root?
      child.root = self
    else
      child.root = self.root
    end
    super(child)
  end
  
end

これでどのノードからでも自分自身のRootが取れるようになった。
こんな感じで使えるはず。

@root_1 = Tree.create( :name => 'root_1' )
@root_2 = Tree.create( :name => 'root_2' )
 
node1 = Tree.create( :name => 'node_1' )
node2 = Tree.create( :name => 'node_2' )
node3 = Tree.create( :name => 'node_3' )
node4 = Tree.create( :name => 'node_4' )
node5 = Tree.create( :name => 'node_5' )
 
@root_1.children << node1
@root_2.children << node2
node1.children << node3
node1.children << node4
node2.children << node5
 
node3.root
  # => @root_1
node4.nodes
  # => [ node1, node3, node4 ]