ActiveRecordで多階層カテゴリ という投稿をしたところ、 @jnchito さんに Ancestry
なるgemを教えていただきました。
stefankroes/ancestry
Organise ActiveRecord model into a tree structure
準備
$ rails g model category name:string
$ rake db:migrate
mysql> desc categories;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
+------------+--------------+------+-----+---------+----------------+
Ancestry
README通りに進めます。
$ rails g migration add_ancestry_to_category ancestry:string
# db/migrate/20141229064909_add_ancestry_to_category.rb
class AddAncestryToCategory < ActiveRecord::Migration
def change
add_column :categories, :ancestry, :string
add_index :categories, :ancestry
end
def down
remove_index :categories, :ancestry
remove_column :categories, :ancestry
end
end
$ rake db:migrate
mysql> desc categories;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| ancestry | varchar(255) | YES | MUL | NULL | |
+------------+--------------+------+-----+---------+----------------+
# app/models/category.rb
class Category < ActiveRecord::Base
has_ancestry
end
カテゴリの登録
# db/seed.rb
metal, jazz = Category.create([{name: "metal"}, {name: "jazz"}])
melodic, black = metal.children.create([{name: "melodic"}, {name: "black"}])
melodic.children.create([{name: "melodic-death"}, {name: "melodic-speed"}])
black.children.create([{name: "symphonic-black"}, {name: "melodic-black"}])
swing, modern = jazz.children.create([{name: "swing"}, {name: "modern"}])
$ rake db:seed
mysql> select * from categories;
+----+-----------------+---------------------+---------------------+----------+
| id | name | created_at | updated_at | ancestry |
+----+-----------------+---------------------+---------------------+----------+
| 1 | metal | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | NULL |
| 2 | jazz | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | NULL |
| 3 | melodic | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 1 |
| 4 | black | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 1 |
| 5 | melodic-death | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 1/3 |
| 6 | melodic-speed | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 1/3 |
| 7 | symphonic-black | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 1/4 |
| 8 | melodic-black | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 1/4 |
| 9 | swing | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 2 |
| 10 | modern | 2014-12-30 06:48:28 | 2014-12-30 06:48:28 | 2 |
+----+-----------------+---------------------+---------------------+----------+
カテゴリの取得
$ rails console
> metal = Category.find_by name: "metal"
> Category.children_of metal
Category Load (0.3ms) SELECT `categories`.* FROM `categories` WHERE `categories`.`ancestry` = '1'
=> #<ActiveRecord::Relation [#<Category id: 3, name: "melodic", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1">, #<Category id: 4, name: "black", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1">]>
簡単!
Ancestry では、階層構造のパスをカラム(デフォルトはancestry)に保持しています。
これは、 SQLアンチパターン の
2章 ナイーブツリーで紹介されている 経路列挙(Path Enumeration)
の手法です。
なので、特定のカテゴリ配下を再帰的に取得するのも下記のようにサクッとできます。
> metal.subtree
Category Load (0.4ms) SELECT `categories`.* FROM `categories` WHERE (((`categories`.`id` = 1 OR `categories`.`ancestry` LIKE '1/%') OR `categories`.`ancestry` = '1'))
=> #<ActiveRecord::Relation [
#<Category id: 1, name: "metal", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: nil>,
#<Category id: 3, name: "melodic", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1">,
#<Category id: 4, name: "black", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1">,
#<Category id: 5, name: "melodic-death", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1/3">,
#<Category id: 6, name: "melodic-speed", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1/3">,
#<Category id: 7, name: "symphonic-black", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1/4">,
#<Category id: 8, name: "melodic-black", created_at: "2014-12-30 06:48:28", updated_at: "2014-12-30 06:48:28", ancestry: "1/4">
]>
parent_id を使った既存のデータから Ancestry に移行する機能もあるみたいです。
便利ですね。