読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Railsでawesome_nested_setを使って階層構造を作成する

Rails中級 Rails Model

f:id:nipe880324:20150215110242j:plain:w480
Photo by Flickr: amatsuscribbler's Photostream



awesome_nested_setとは、モデルを階層構造に管理できるようにするgemです。
フォルダ階層、カテゴリ階層、コメントへのリプライでの階層などツリー構造を表したい箇所で使うと便利です。


今回は、次のように、モデルを階層構造で表示、作成、更新できるようにします。
f:id:nipe880324:20150215110501j:plain:w480



動作確認

  • 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のインストール
  2. awesome_nested_setの基本的な使い方
  3. awesome_nested_setでカテゴリを階層構造にする



1. awesome_nested_setのインストール

Gemfileに追加します。

# Gemfile
gem 'awesome_nested_set'


Bundlerを実行します。

bundle install


2. awesome_nested_setの基本的な使い方

awesome_nested_setを使うためには、モデルにparent_idlftrgtという3つのフィールドが必要です。(これらのフィールド名は設定で変更可能です)
また、オプションでdepthchildren_countという2つのフィールドも追加することができます。


これらのフィールドを追加するマイグレーションファイル作成します。

bin/rails g migration add_awesome_nested_set_columns_to_categories parent_id:integer lft:integer rgt:integer


DBにINSERT時にrgtを探すのでadd_indexrgtカラムにインデックスをつけておきます。
他のカラムにもインデックスをつけておくことがGitHubのREADMEで推奨されているので、parent_idrgtカラムにもインデックスをつけます。

# 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でカテゴリを階層構造にする

この章では、次のようにモデルを階層構造で表示、作成、更新できるようにします。
f:id:nipe880324:20150215110501j:plain:w480


まずカテゴリを作成します。

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]]


では、画面を表示して確認してみます。
f:id:nipe880324:20150215110501j:plain:w480



次に、親ノードを更新できるように、親ノードを選択するセレクトボックスをフォームに追加します。

<!-- 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


これで親ノードを更新ができるようになりましたので、画面で確認してみます。
次にように親のカテゴリーを選択できます。
f:id:nipe880324:20150215110831j:plain:w480


そして、登録すると一覧画面ではツリー状に追加されます。
f:id:nipe880324:20150215110841j:plain:w480

もちろん、更新もできます。

以上です。

jsTreeというインタラクティブにツリーの追加や移動、削除といった操作ができるjQueryプラグインをawesome_nested_setに追加する方法も説明しています。
Railsでawesome_nested_setとjsTreeでインタラクティブにツリー構造を操作する