acts_as_〜

昨日は、acts_as_listを使ってリストのソートをしてみたのですが、別件で階層化した構造を扱いたい部分があるので、acts_as_〜のプラグインを使うとどんな感じになるのか調べています。

プラグイン

とりあえず、RailsのWikiにあるプラグインをリストアップしました。

結構いろいろあるかと思いきや、BetterNestedSet と Acts_as_threaded は ActsAsNestedSetの拡張のようです。ついでにはハウトゥー系も。

acts_as_tree

まず、acts_as_treeを使ってみます。APIこちら
プラグインをインストールします。

script/plugin install acts_as_tree

テスト用のモデルを作成してマイグレーションを実行します。

script/generate model TreeItem name:string parent_id:integer
rake db:migrate

ドキュメントにあるとおりですが、こんな感じで使います。

# ルートの追加
root      = Category.create("name" => "root")

# 子の追加
child1    = root.children.create("name" => "child1")
child2    = root.children.create("name" => "child2")

# 孫の追加
subchild1  = child1.children.create("name" => "subchild1")
subchild2  = child1.children.create("name" => "subchild2")

root.children # => child1, child2

child1.parent # => root
child1.siblings # => child2
child1.self_and_siblings # => child1, child2

subchild1.root # => root
subchild1.ancestors # => child1, root

Category.root # => root 

プラグインのフォルダにあるtree.rbを見る方がてっとり早いですが、

  • "belongs_to :parent","has_many :children"という、自身についての1対多の関連を使ってできてます。なので、parentとchildrenはメソッドではなくフィールドですね。
  • ancestors と root は、parentを繰り返したどります(ancestorsの場合は配列に追加してきます)。
  • self_and_siblings は、parent.children です。
  • クラスメソッドでrootを取得する場合には、"parent_id IS NULL"というSQLが使われています。

基本的にツリーを上下にたどりながら処理するので、ツリー全体や、特定のcategoryを含む部分ををまとめて取得したり、というのはちょっと大変そうです。そういう時にはacts_as_nested_setがよいみたいです。

acts_as_nested_set

acts_as_nested_setがどういうものかは、APIドキュメントの説明を見るとわかりやすいです。あるツリーの要素の子要素の範囲を指定するためにlftとrgtという左右の境界を意味する列をテーブルに追加しています。lftとrgtを指定してデータを取得ことでこれでツリー全体やすべての子要素などを一括で選択することができます。

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

script/plugin install acts_as_nested_set

テスト用のモデルを作成してマイグレーションを実行します。

script/generate model NestedCategory name:string parent_id:integer lft:integer rgt:integer
rake db:migrate

こんな感じで使います。

# ルートを作る
root      = NestedCategory.create(:name => "root")

# 子の追加
child1    = NestedCategory.create(:name => "child1")
root.add_child child1
child2    = NestedCategory.create(:name => "child2")
root.add_child child2

# 孫の追加
subchild1  = NestedCategory.create(:name => "subchild1")
child1.add_child subchild1
subchild2  = NestedCategory.create(:name => "subchild2")
child1.add_child subchild2

root.direct_children # => child1, child2
root.full_set # => root, child1, child2 , subchild1, subchild2

child1.direct_children # => child1, child2
child1.all_children # => child1, child2 , subchild1, subchild2

のはずなのですが、これだと期待通りに動かなかったです。child1を"NestedCategory.find_by_name('child1')"の様にすると動いたのですが、うまくDBに保存されていないのかよくわからないです。
これを実行するとデータの追加で以下のようなSQLが発行されてました。結構な量のSQLです。

-- ルートの追加
INSERT INTO "nested_categories" ("name", "updated_at", "lft", "parent_id", "rgt", "created_at") VALUES('root', '2008-07-30 10:43:37', NULL, NULL, NULL, '2008-07-30 10:43:37')

-- 子1の追加
INSERT INTO "nested_categories" ("name", "updated_at", "lft", "parent_id", "rgt", "created_at") VALUES('child1', '2008-07-30 10:43:37', NULL, NULL, NULL, '2008-07-30 10:43:37')
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332878) 
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332879) 
UPDATE "nested_categories" SET "lft" = 1, "rgt" = 4, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332878
UPDATE "nested_categories" SET "lft" = 2, "rgt" = 3, "parent_id" = 996332878, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332879
-- 子2の追加
INSERT INTO "nested_categories" ("name", "updated_at", "lft", "parent_id", "rgt", "created_at") VALUES('child2', '2008-07-30 10:43:37', NULL, NULL, NULL, '2008-07-30 10:43:37')
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332878) 
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332880) 
UPDATE "nested_categories" SET lft = (lft + 2) WHERE (1 = 1 AND lft >= 4) 
UPDATE "nested_categories" SET rgt = (rgt + 2) WHERE (1 = 1 AND rgt >= 4) 
UPDATE "nested_categories" SET "rgt" = 6, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332878
UPDATE "nested_categories" SET "lft" = 4, "rgt" = 5, "parent_id" = 996332878, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332880

-- 孫1の追加
INSERT INTO "nested_categories" ("name", "updated_at", "lft", "parent_id", "rgt", "created_at") VALUES('subchild1', '2008-07-30 10:43:37', NULL, NULL, NULL, '2008-07-30 10:43:37')
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332879) 
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332881) 
UPDATE "nested_categories" SET lft = (lft + 2) WHERE (1 = 1 AND lft >= 3) 
UPDATE "nested_categories" SET rgt = (rgt + 2) WHERE (1 = 1 AND rgt >= 3) 
UPDATE "nested_categories" SET "rgt" = 5, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332879
UPDATE "nested_categories" SET "lft" = 3, "rgt" = 4, "parent_id" = 996332879, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332881

-- 孫2の追加
INSERT INTO "nested_categories" ("name", "updated_at", "lft", "parent_id", "rgt", "created_at") VALUES('subchild2', '2008-07-30 10:43:37', NULL, NULL, NULL, '2008-07-30 10:43:37')
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332879) 
SELECT * FROM "nested_categories" WHERE ("nested_categories"."id" = 996332882) 
UPDATE "nested_categories" SET lft = (lft + 2) WHERE (1 = 1 AND lft >= 5) 
UPDATE "nested_categories" SET rgt = (rgt + 2) WHERE (1 = 1 AND rgt >= 5) 
UPDATE "nested_categories" SET "rgt" = 7, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332879
UPDATE "nested_categories" SET "lft" = 5, "rgt" = 6, "parent_id" = 996332879, "updated_at" = '2008-07-30 10:43:37' WHERE "id" = 996332882

また、acts_as_treeにあった↓のようなインスタンスメソッドがなくなっており、これらを追加して便利にしたのがBetterNestedSetです。

  • ancestors
  • siblings
  • siblings_and_self

BetterNestedSetもActs_as_threadedもacts_as_nested_setを使いやすいように拡張したプラグインだと思います。ただ、acts_as_nested_setのテーブル構造は、私の今回の用途に向いていないようなのでこの辺までにしておこうと思います。