acts_as_ordered_tree

昨日、ツリー構造のデータのためのプラグインを見ていたのですが、ツリー全体の一括での取得が不要であれば、acts_as_ordered_treeがシンプルで使いやすそうでした。acts_as_ordered_treeでは、acts_as_treeと同じく親への参照によりツリー構造を管理し、さらに子同士の間の並び順も保持します(acts_as_listと同じ)。

プラグインのインストールをインストールします。

script/plugin install http://ordered-tree.rubyforge.org/svn/acts_as_ordered_tree/

テスト用のモデルとテーブルを作成します。

script/generate model Item name:string

マイグレーションファイルを編集します。

class Items < ActiveRecord::Migration
  def self.up
    create_table :items do |t|
      t.string :name

      t.integer :parent_id ,:integer ,:null => false ,:default => 0 # 親への参照
      t.integer :position  ,:integer # ソート用

      t.timestamps
    end
    add_index(:items, :parent_id) # インデックス
  end

  def self.down
    drop_table :items
  end
end

item.rbに記述を追加します。

class Item < ActiveRecord::Base
  acts_as_ordered_tree
end

いろいろ操作してみます。

require 'test_helper'

class ItemTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  def test_ordered_tree


    # ルートの追加
    root = Item.create(:name => 'root-1')

    # 子の追加
    root.children.create(:name => 'item-1-1')
    root.children << Item.create(:name => 'item-1-2')
    root.children << Item.create(:name => 'item-1-3')

    # 孫の追加
    root.children[0].children.create(:name => 'item-1-1-1')
    root.children[0].children.create(:name => 'item-1-1-2')

    # 子を取得する
    root = Item.find_by_name('root-1')
    assert_equal 3, root.children.size
    assert_equal 'item-1-1', root.children[0].name
    assert_equal 'item-1-2', root.children[1].name
    assert_equal 'item-1-3', root.children[2].name
    root.children.each_with_index { |element, idx| element.position = idx }

    # 子を並び替える(3 <-> 2)
    root.children[2].move_higher
    assert_equal 'item-1-1', root.children[0].name
    assert_equal 'item-1-3', root.children[1].name
    assert_equal 'item-1-2', root.children[2].name
    root.children.each_with_index { |element, idx| element.position = idx }

    # 子を並び替える(1 <-> 2)
    root.children[0].move_lower
    assert_equal 'item-1-3', root.children[0].name
    assert_equal 'item-1-1', root.children[1].name
    assert_equal 'item-1-2', root.children[2].name
    root.children.each_with_index { |element, idx| element.position = idx }

    # 子を並び替える(item-1-3 => 末尾)
    root.children[0].move_to_bottom
    assert_equal 'item-1-1', root.children[0].name
    assert_equal 'item-1-2', root.children[1].name
    assert_equal 'item-1-3', root.children[2].name
    root.children.each_with_index { |element, idx| element.position = idx }

    # 孫を取得する
    subitems = root.children[0].children
    assert_equal 2, subitems.size
    assert_equal 'item-1-1-1', subitems[0].name
    assert_equal 'item-1-1-2', subitems[1].name
    subitems.each_with_index { |element, idx| element.position = idx }

    # 先祖を取得する
    ancestors = subitems[0].ancestors
    assert_equal 2, subitems.size
    assert_equal 'item-1-1', ancestors[0].name
    assert_equal 'root-1', ancestors[1].name
    # item-1-1-1を一つ上に移動
    subitems[0].shift_to(root)
    assert_equal 4, root.children.size
    assert_equal 'item-1-1', root.children[0].name
    assert_equal 'item-1-2', root.children[1].name
    assert_equal 'item-1-3', root.children[2].name
    assert_equal 'item-1-1-1', root.children[3].name # 移動していること
  end
end

他にもツリーを操作するメソッドはいろいろあります。APIを参照してみてください。