Photo by Flickr: amatsuscribbler's Photostream
awesome_nested_setとは、モデルを階層構造に管理できるようにするgemです。
フォルダ階層、カテゴリ階層、コメントへのリプライでの階層などツリー構造を表したい箇所で使うと便利です。
今回は、次のように、モデルを階層構造で表示、作成、更新できるようにします。
動作確認
- Ruby 2.1.2
- Rails 4.2.0
- awesome_nested_set 3.0.2
バージョン3は「Rails 4」をサポート、バージョン2は「Rails 3」をサポート、2.0以前は「Rails 2」をサポートしています。
目次
1. awesome_nested_setのインストール
Gemfile
に追加します。
# Gemfile gem 'awesome_nested_set'
Bundlerを実行します。
bundle install
2. awesome_nested_setの基本的な使い方
awesome_nested_set
を使うためには、モデルにparent_id
、lft
、rgt
という3つのフィールドが必要です。(これらのフィールド名は設定で変更可能です)また、オプションで
depth
、children_count
という2つのフィールドも追加することができます。これらのフィールドを追加するマイグレーションファイル作成します。
bin/rails g migration add_awesome_nested_set_columns_to_categories parent_id:integer lft:integer rgt:integer
DBにINSERT時にrgt
を探すのでadd_index
でrgt
カラムにインデックスをつけておきます。
他のカラムにもインデックスをつけておくことがGitHubのREADMEで推奨されているので、parent_id
とrgt
カラムにもインデックスをつけます。
# db/migrate/YYYYMMDDhhmmss_add_awesome_nested_set_columns_to_categories.rb class AddAwesomeNestedSetColumnsToCategories < ActiveRecord::Migration def change # 必須のフィールド add_column :categories, :parent_id, :integer add_column :categories, :lft, :integer add_column :categories, :rgt, :integer add_index :categories, :parent_id add_index :categories, :lft add_index :categories, :rgt # オプションのフィールド # add_column :categories, :depth, :integer # add_column :categories, :children_count, :integer # # add_index :categories, :depth end end
マイグレーションを実行します。
bin/rake db:migrate
そして、モデル内でacts_as_nested_set
という宣言を追加します。
class Category < ActiveRecord::Base acts_as_nested_set end
これで、さまざまなメソッドが使えます。
# ルートノードを作成 root = Category.create(name: 'root') # 子ノードを作成 child1 = root.children.create(name: 'child1') child2 = root.children.create(name: 'child2') # ノードを作成し、子ノードに接続する grandchild = Category.create(name: 'grandchild1') grandchild.move_to_child_of(child1) # 子ノードを取得 root.children #=> [<Category "child1">, <Category "child2">] # 葉ノードを取得 root.leaves #=> [<Category "child2">, <Category "grandchild1"] # 兄弟ノードを取得 child1.siblings #=> [<Category "child2">] # 階層を取得 child1.level #=> 1 # 作成されたツリー root '-- child1 '-- grandchild '-- child2
また、階層構造に表示するためやセレクトボックスのために、nested_set_options
というビューヘルパーが用意されています。
# form builder 有 <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %> # form builder 無 <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
acts_as_nested_set
のオプションやコールバックやフックなどがあるので、awesome_nested_set - GitHubを参照してください。
3. awesome_nested_setでカテゴリを階層構造にする
この章では、次のようにモデルを階層構造で表示、作成、更新できるようにします。
まずカテゴリを作成します。
rails g scaffold Category name
マイグレーションファイルを作成します。
bin/rails g migration add_awesome_nested_set_columns_to_categories parent_id:integer lft:integer rgt:integer
テーブルが多くなるとパフォーマンスが低くなるので、インデックスをつけておく必要があります。
# db/migrate/YYYYMMDDhhmmss_add_awesome_nested_set_columns_to_categories.rb class AddAwesomeNestedSetColumnsToCategories < ActiveRecord::Migration def change add_column :categories, :parent_id, :integer add_column :categories, :lft, :integer add_column :categories, :rgt, :integer add_index :categories, :parent_id add_index :categories, :lft add_index :categories, :rgt end end
マイグレーションを実行します。
bin/rake db:migrate
Categoryモデルにacts_as_nested_set
を追加します。
# app/models/category.rb class Category < ActiveRecord::Base acts_as_nested_set end
テストデータを追加しておきます。
$ bin/rails c # ルートノードを作成 root = Category.create(name: 'root') # 子ノードを作成 child1 = root.children.create(name: 'child1') child2 = root.children.create(name: 'child2') # ノードを作成し、子ノードに接続する grandchild = Category.create(name: 'grandchild1') grandchild.move_to_child_of(child1)
まず、Categories一覧画面でツリー状に表示するようにします。
awesome_nested_set
のビューヘルパーのnested_set_options
を使います。
<!-- app/views/categories/index.html.erb --> ... <tbody> <% nested_set_options(@categories) { |i| "#{'–' * i.level} #{i.name}" }.each do |name, id| %> <tr> <td><%= name %></td> <td><%= link_to 'Show', category_path(id) %></td> <td><%= link_to 'Edit', edit_category_path(id) %></td> <td><%= link_to 'Destroy', category_path(id), method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> ...
nested_set_options
は次のようにソートされたカテゴリの名前とidの2次元配列を返します。
nested_set_options(@categories) { |i| "#{'–' * i.level} #{i.name}" } #=> [[" root", 1], ["– child1", 2], ["–– grandchild1", 4], ["– child2", 3]]
では、画面を表示して確認してみます。
次に、親ノードを更新できるように、親ノードを選択するセレクトボックスをフォームに追加します。
<!-- app/views/categories/_form.html.erb --> ... <div class="field"> <%= f.label :parent_id %><br> <%= f.select :parent_id, nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" }, { selected: @category.parent_id, include_blank: true } %> </div> ...
CategoriesコントローラーのStrongParametersにparent_id
を追加します。
# app/controllers/categories_controller.rb def category_params params.require(:category).permit(:name, :parent_id) end
これで親ノードを更新ができるようになりましたので、画面で確認してみます。
次にように親のカテゴリーを選択できます。
そして、登録すると一覧画面ではツリー状に追加されます。
もちろん、更新もできます。
以上です。
jsTreeというインタラクティブにツリーの追加や移動、削除といった操作ができるjQueryプラグインをawesome_nested_setに追加する方法も説明しています。
Railsでawesome_nested_setとjsTreeでインタラクティブにツリー構造を操作する