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のテーブル構造は、私の今回の用途に向いていないようなのでこの辺までにしておこうと思います。